|
|
|
@ -1,6 +1,6 @@
|
|
|
|
|
//! Solana support for Icepick.
|
|
|
|
|
//!
|
|
|
|
|
//! # Command Line Arguments
|
|
|
|
|
//! # Command Line Operations
|
|
|
|
|
//!
|
|
|
|
|
//! The first thing you'll want is a wallet. For this, you can run the command
|
|
|
|
|
//! `icepick sol generate-wallet`. The output of the command will be a request to derive a key,
|
|
|
|
@ -34,10 +34,28 @@
|
|
|
|
|
//!
|
|
|
|
|
//! # On an offline system
|
|
|
|
|
//! blockhash=$(jq -r .blob sdcard/blockhash.json)
|
|
|
|
|
//! icepick sol transfer $amount $to_address $from_address $blockhash | icepick sol sign > sdcard/signed.json
|
|
|
|
|
//! icepick sol transfer $amount $to_address $from_address $blockhash | icepick sol sign > sdcard/transfer.json
|
|
|
|
|
//!
|
|
|
|
|
//! # On the online system, again
|
|
|
|
|
//! icepick sol broadcast < sdcard/signed.json
|
|
|
|
|
//! icepick sol broadcast --cluster devnet < sdcard/transfer.json
|
|
|
|
|
//! ```
|
|
|
|
|
//!
|
|
|
|
|
//! You may also want to transfer tokens on the Solana blockchain. You'll first need an account for
|
|
|
|
|
//! the token (let's use IPDBG, a devnet token I built for testing):
|
|
|
|
|
//!
|
|
|
|
|
//! ```sh
|
|
|
|
|
//! # On an online system
|
|
|
|
|
//! # Assume we have the wallet address as $wallet_address
|
|
|
|
|
//! icepick sol get-token-address IPDBG > sdcard/ipdbg.json
|
|
|
|
|
//! icepick sol get-blockhash > sdcard/blockhash.json
|
|
|
|
|
//!
|
|
|
|
|
//! # On an offline system
|
|
|
|
|
//! blockhash=$(jq -r .blob sdcard/blockhash.json)
|
|
|
|
|
//! token_address=$(jq -r .blob sdcard/ipdbg.json)
|
|
|
|
|
//! icepick sol create-token-account $wallet_address $token_address $blockhash | icepick sol sign > sdcard/create-account.json
|
|
|
|
|
//!
|
|
|
|
|
//! # On an online system
|
|
|
|
|
//! icepick sol broadcast --cluster devnet < sdcard/create-account.json
|
|
|
|
|
//! ```
|
|
|
|
|
|
|
|
|
|
use icepick_module::{
|
|
|
|
@ -101,6 +119,12 @@ pub struct GenerateWallet {
|
|
|
|
|
#[serde(rename_all = "kebab-case")]
|
|
|
|
|
pub struct GetWalletAddress {}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
|
|
|
#[serde(rename_all = "kebab-case")]
|
|
|
|
|
pub struct GetTokenAddress {
|
|
|
|
|
token: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
|
|
|
#[serde(rename_all = "kebab-case")]
|
|
|
|
|
pub struct Transfer {
|
|
|
|
@ -123,6 +147,20 @@ pub struct CreateTokenAccount {
|
|
|
|
|
blockhash: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
|
|
|
#[serde(rename_all = "kebab-case")]
|
|
|
|
|
pub struct TransferToken {
|
|
|
|
|
amount: String,
|
|
|
|
|
token_address: String,
|
|
|
|
|
blockhash: String,
|
|
|
|
|
to_address: String,
|
|
|
|
|
from_account: Option<String>,
|
|
|
|
|
from_address: String,
|
|
|
|
|
fee: Option<String>,
|
|
|
|
|
fee_payer: Option<String>,
|
|
|
|
|
fee_payer_address: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
|
|
|
#[serde(rename_all = "kebab-case")]
|
|
|
|
|
pub struct Sign {}
|
|
|
|
@ -153,8 +191,10 @@ pub enum Operation {
|
|
|
|
|
GetBlockhash(GetBlockhash),
|
|
|
|
|
GenerateWallet(GenerateWallet),
|
|
|
|
|
GetWalletAddress(GetWalletAddress),
|
|
|
|
|
GetTokenAddress(GetTokenAddress),
|
|
|
|
|
Transfer(Transfer),
|
|
|
|
|
CreateTokenAccount(CreateTokenAccount),
|
|
|
|
|
TransferToken(TransferToken),
|
|
|
|
|
Sign(Sign),
|
|
|
|
|
Broadcast(Broadcast),
|
|
|
|
|
}
|
|
|
|
@ -239,6 +279,15 @@ impl Module for Solana {
|
|
|
|
|
description: "Get the address for a given wallet.".to_string(),
|
|
|
|
|
arguments: vec![],
|
|
|
|
|
},
|
|
|
|
|
icepick_module::help::Operation {
|
|
|
|
|
name: "get-token-address".to_string(),
|
|
|
|
|
description: "Get the address for a given token.".to_string(),
|
|
|
|
|
arguments: vec![Argument {
|
|
|
|
|
name: "token".to_string(),
|
|
|
|
|
description: "The token to look up".to_string(),
|
|
|
|
|
r#type: ArgumentType::Required,
|
|
|
|
|
}],
|
|
|
|
|
},
|
|
|
|
|
icepick_module::help::Operation {
|
|
|
|
|
name: "transfer".to_string(),
|
|
|
|
|
description: "Transfer SOL from a Keyfork wallet to an external wallet."
|
|
|
|
@ -302,6 +351,38 @@ impl Module for Solana {
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
icepick_module::help::Operation {
|
|
|
|
|
name: "transfer-token".to_string(),
|
|
|
|
|
description: "Transfer tokens from a Keyfork wallet to an external wallet."
|
|
|
|
|
.to_string(),
|
|
|
|
|
arguments: vec![
|
|
|
|
|
Argument {
|
|
|
|
|
name: "amount".to_string(),
|
|
|
|
|
description: "The amount of tokens to transfer.".to_string(),
|
|
|
|
|
r#type: ArgumentType::Required,
|
|
|
|
|
},
|
|
|
|
|
Argument {
|
|
|
|
|
name: "token-address".to_string(),
|
|
|
|
|
description: "The address of the token.".to_string(),
|
|
|
|
|
r#type: ArgumentType::Required,
|
|
|
|
|
},
|
|
|
|
|
account.clone(),
|
|
|
|
|
Argument {
|
|
|
|
|
name: "to-address".to_string(),
|
|
|
|
|
description: "The address to send the tokens to.".to_string(),
|
|
|
|
|
r#type: ArgumentType::Required,
|
|
|
|
|
},
|
|
|
|
|
Argument {
|
|
|
|
|
name: "from-address".to_string(),
|
|
|
|
|
description: "The address to send the tokens from; will be used to verify the derivation account.".to_string(),
|
|
|
|
|
r#type: ArgumentType::Required,
|
|
|
|
|
},
|
|
|
|
|
blockhash.clone(),
|
|
|
|
|
fee.clone(),
|
|
|
|
|
fee_payer.clone(),
|
|
|
|
|
fee_payer_address.clone(),
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
icepick_module::help::Operation {
|
|
|
|
|
name: "sign".to_string(),
|
|
|
|
|
description: "Sign a previously-generated transaction.".to_string(),
|
|
|
|
@ -342,6 +423,20 @@ impl Module for Solana {
|
|
|
|
|
"blob": pubkey.to_string(),
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
Operation::GetTokenAddress(GetTokenAddress { token }) => {
|
|
|
|
|
let addr = match token.as_str() {
|
|
|
|
|
// Only exists on devnet
|
|
|
|
|
"IPDBG" => Some("3V6hm5ifSLSWLZ86NpTxo5iVguGq9qCUtry6bn5PtT23"),
|
|
|
|
|
// Only exists on mainnet
|
|
|
|
|
"PYTH" => Some("HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3"),
|
|
|
|
|
_ => None,
|
|
|
|
|
};
|
|
|
|
|
addr.map(|v| serde_json::Value::String(v.to_string()))
|
|
|
|
|
.unwrap_or(serde_json::Value::Null);
|
|
|
|
|
Ok(serde_json::json!({
|
|
|
|
|
"blob": addr,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
Operation::Transfer(Transfer {
|
|
|
|
|
amount,
|
|
|
|
|
from_account,
|
|
|
|
@ -436,6 +531,101 @@ impl Module for Solana {
|
|
|
|
|
"derivation-accounts": [0u32 | 1 << 31],
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
Operation::TransferToken(TransferToken {
|
|
|
|
|
amount,
|
|
|
|
|
token_address,
|
|
|
|
|
blockhash,
|
|
|
|
|
to_address,
|
|
|
|
|
from_account,
|
|
|
|
|
from_address,
|
|
|
|
|
fee,
|
|
|
|
|
fee_payer,
|
|
|
|
|
fee_payer_address,
|
|
|
|
|
}) => {
|
|
|
|
|
// TODO: deduplicate code used in Transfer
|
|
|
|
|
|
|
|
|
|
// no transfer between types of currency, the only amount is the amount
|
|
|
|
|
// of the lowest denomination. no floats, like with SOL / lamports.
|
|
|
|
|
let amount = u64::from_str(&amount).expect("integer amount");
|
|
|
|
|
|
|
|
|
|
use solana_sdk::pubkey::Pubkey;
|
|
|
|
|
use spl_associated_token_account::get_associated_token_address;
|
|
|
|
|
let to_pk = Pubkey::from_str(&to_address).unwrap();
|
|
|
|
|
let from_pk = Pubkey::from_str(&from_address).unwrap();
|
|
|
|
|
let token_pk = Pubkey::from_str(&token_address).unwrap();
|
|
|
|
|
let payer_account_and_pk = {
|
|
|
|
|
// If a fee payer is given, a fee payer address must also be given, since the
|
|
|
|
|
// address must be known before signing the transaction.
|
|
|
|
|
match (&fee_payer, &fee_payer_address) {
|
|
|
|
|
(Some(payer), Some(address)) => {
|
|
|
|
|
// Use the provided account
|
|
|
|
|
Some((
|
|
|
|
|
u32::from_str(payer).unwrap(),
|
|
|
|
|
Pubkey::from_str(address).unwrap(),
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
(None, None) => {
|
|
|
|
|
// Use the transaction account
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
_ => panic!("Invalid combination of fee_payer and fee_payer_address"),
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let token_program_id = spl_token::ID;
|
|
|
|
|
let mut signers = vec![&from_pk];
|
|
|
|
|
if let Some((_, pk)) = payer_account_and_pk.as_ref() {
|
|
|
|
|
signers.push(pk);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let from_token_address = get_associated_token_address(&from_pk, &token_pk);
|
|
|
|
|
let to_token_address = get_associated_token_address(&to_pk, &token_pk);
|
|
|
|
|
let decimals = 9u8;
|
|
|
|
|
let mut instruction = spl_token_2022::instruction::transfer_checked(
|
|
|
|
|
&token_program_id, // token program id
|
|
|
|
|
&from_token_address, // source, as token address
|
|
|
|
|
&token_pk, // mint
|
|
|
|
|
&to_token_address, // destination, as token address
|
|
|
|
|
&from_pk, // authority, as source sol address
|
|
|
|
|
// TODO: signers should be [] when not using multisig
|
|
|
|
|
// but should contain all signers when multisig
|
|
|
|
|
&[], // signers
|
|
|
|
|
// TODO: make amount floatable
|
|
|
|
|
amount * 10u64.pow(decimals as u32), // amount
|
|
|
|
|
decimals, // decimals
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
// TODO: check if this works with payer
|
|
|
|
|
// this is required because the Solana SDK does not set the primary transactional
|
|
|
|
|
// key as writable (the one that would be paying computation fees) in the event a
|
|
|
|
|
// payer is not provided. The transactional account must be writable for the
|
|
|
|
|
// computation fee to be paid.
|
|
|
|
|
if payer_account_and_pk.is_none() {
|
|
|
|
|
for account in instruction.accounts.iter_mut() {
|
|
|
|
|
if account.pubkey == from_pk {
|
|
|
|
|
account.is_writable = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let hash = solana_sdk::hash::Hash::from_str(&blockhash).unwrap();
|
|
|
|
|
let message = solana_sdk::message::Message::new_with_blockhash(
|
|
|
|
|
&[instruction],
|
|
|
|
|
payer_account_and_pk.map(|v| v.1).as_ref(),
|
|
|
|
|
&hash,
|
|
|
|
|
);
|
|
|
|
|
// message.header.num_readonly_signed_accounts = 0;
|
|
|
|
|
let transaction =
|
|
|
|
|
solana_sdk::transaction::Transaction::new_unsigned(message.clone());
|
|
|
|
|
/*
|
|
|
|
|
use base64::prelude::*;
|
|
|
|
|
eprintln!("{}", BASE64_STANDARD.encode(transaction.message_data()));
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#[allow(clippy::identity_op)]
|
|
|
|
|
Ok(serde_json::json!({
|
|
|
|
|
"blob": transaction,
|
|
|
|
|
"derivation-accounts": [0u32 | 1 << 31],
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
Operation::Sign(_) => {
|
|
|
|
|
let blob = request.blob.expect("passed in instruction blob");
|
|
|
|
|
let mut transaction: solana_sdk::transaction::Transaction =
|
|
|
|
|