Compare commits

..

No commits in common. "85e9d34fa8c43b89ace889cad1af431d5da8afb8" and "c336079b05a4d62faaaee37125e5bb3d8abd4d73" have entirely different histories.

3 changed files with 3 additions and 197 deletions

2
Cargo.lock generated
View File

@ -1476,7 +1476,6 @@ dependencies = [
name = "icepick-solana" name = "icepick-solana"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"base64 0.22.1",
"ed25519-dalek 1.0.1", "ed25519-dalek 1.0.1",
"icepick-module", "icepick-module",
"serde", "serde",
@ -1485,7 +1484,6 @@ dependencies = [
"solana-sdk", "solana-sdk",
"spl-associated-token-account", "spl-associated-token-account",
"spl-token", "spl-token",
"spl-token-2022",
"thiserror 2.0.3", "thiserror 2.0.3",
] ]

View File

@ -4,7 +4,6 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
base64 = "0.22.1"
ed25519-dalek = "=1.0.1" ed25519-dalek = "=1.0.1"
icepick-module = { version = "0.1.0", path = "../../icepick-module" } icepick-module = { version = "0.1.0", path = "../../icepick-module" }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
@ -13,5 +12,4 @@ solana-rpc-client = { version = "2.1.1", default-features = false }
solana-sdk = { version = "2.1.1" } solana-sdk = { version = "2.1.1" }
spl-associated-token-account = "6.0.0" spl-associated-token-account = "6.0.0"
spl-token = "7.0.0" spl-token = "7.0.0"
spl-token-2022 = "6.0.0"
thiserror = "2.0.3" thiserror = "2.0.3"

View File

@ -1,6 +1,6 @@
//! Solana support for Icepick. //! Solana support for Icepick.
//! //!
//! # Command Line Operations //! # Command Line Arguments
//! //!
//! The first thing you'll want is a wallet. For this, you can run the command //! 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, //! `icepick sol generate-wallet`. The output of the command will be a request to derive a key,
@ -34,28 +34,10 @@
//! //!
//! # On an offline system //! # On an offline system
//! blockhash=$(jq -r .blob sdcard/blockhash.json) //! blockhash=$(jq -r .blob sdcard/blockhash.json)
//! icepick sol transfer $amount $to_address $from_address $blockhash | icepick sol sign > sdcard/transfer.json //! icepick sol transfer $amount $to_address $from_address $blockhash | icepick sol sign > sdcard/signed.json
//! //!
//! # On the online system, again //! # On the online system, again
//! icepick sol broadcast --cluster devnet < sdcard/transfer.json //! icepick sol broadcast < sdcard/signed.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::{ use icepick_module::{
@ -119,12 +101,6 @@ pub struct GenerateWallet {
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct GetWalletAddress {} pub struct GetWalletAddress {}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
pub struct GetTokenAddress {
token: String,
}
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct Transfer { pub struct Transfer {
@ -147,20 +123,6 @@ pub struct CreateTokenAccount {
blockhash: String, 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)] #[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct Sign {} pub struct Sign {}
@ -191,10 +153,8 @@ pub enum Operation {
GetBlockhash(GetBlockhash), GetBlockhash(GetBlockhash),
GenerateWallet(GenerateWallet), GenerateWallet(GenerateWallet),
GetWalletAddress(GetWalletAddress), GetWalletAddress(GetWalletAddress),
GetTokenAddress(GetTokenAddress),
Transfer(Transfer), Transfer(Transfer),
CreateTokenAccount(CreateTokenAccount), CreateTokenAccount(CreateTokenAccount),
TransferToken(TransferToken),
Sign(Sign), Sign(Sign),
Broadcast(Broadcast), Broadcast(Broadcast),
} }
@ -279,15 +239,6 @@ impl Module for Solana {
description: "Get the address for a given wallet.".to_string(), description: "Get the address for a given wallet.".to_string(),
arguments: vec![], 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 { icepick_module::help::Operation {
name: "transfer".to_string(), name: "transfer".to_string(),
description: "Transfer SOL from a Keyfork wallet to an external wallet." description: "Transfer SOL from a Keyfork wallet to an external wallet."
@ -351,38 +302,6 @@ 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 { icepick_module::help::Operation {
name: "sign".to_string(), name: "sign".to_string(),
description: "Sign a previously-generated transaction.".to_string(), description: "Sign a previously-generated transaction.".to_string(),
@ -423,20 +342,6 @@ impl Module for Solana {
"blob": pubkey.to_string(), "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 { Operation::Transfer(Transfer {
amount, amount,
from_account, from_account,
@ -531,101 +436,6 @@ impl Module for Solana {
"derivation-accounts": [0u32 | 1 << 31], "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(_) => { Operation::Sign(_) => {
let blob = request.blob.expect("passed in instruction blob"); let blob = request.blob.expect("passed in instruction blob");
let mut transaction: solana_sdk::transaction::Transaction = let mut transaction: solana_sdk::transaction::Transaction =