From ca0fc3eef9be1ddace455063facad1b8757e555b Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 27 Dec 2024 02:21:09 -0500 Subject: [PATCH] icepick-solana: add support for utilizing durable nonces --- crates/by-chain/icepick-solana/Cargo.toml | 1 + crates/by-chain/icepick-solana/src/lib.rs | 554 +++++++++++++--------- 2 files changed, 331 insertions(+), 224 deletions(-) diff --git a/crates/by-chain/icepick-solana/Cargo.toml b/crates/by-chain/icepick-solana/Cargo.toml index 5ca6197..f07f7a6 100644 --- a/crates/by-chain/icepick-solana/Cargo.toml +++ b/crates/by-chain/icepick-solana/Cargo.toml @@ -12,6 +12,7 @@ icepick-module = { version = "0.1.0", path = "../../icepick-module" } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true solana-rpc-client = { version = "2.1.1", default-features = false } +solana-rpc-client-nonce-utils = "2.1.7" solana-sdk = { version = "2.1.1" } solana-transaction-status = "2.1.1" solana-transaction-status-client-types = "2.1.1" diff --git a/crates/by-chain/icepick-solana/src/lib.rs b/crates/by-chain/icepick-solana/src/lib.rs index 5e02f98..328926d 100644 --- a/crates/by-chain/icepick-solana/src/lib.rs +++ b/crates/by-chain/icepick-solana/src/lib.rs @@ -129,6 +129,21 @@ impl std::fmt::Display for Cluster { } } +// NOTE: While, technically, they both fit in the same width, it is _important_ to have different +// functionality based on which is provided, as Nonce requires an incremention instruction. +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "snake_case", untagged)] +pub enum Hashable { + Nonce { + nonce_data: String, + nonce_address: String, + nonce_authority: String, + }, + Blockhash { + blockhash: String, + }, +} + #[derive(Serialize, Deserialize, Debug)] pub struct GetBlockhash { cluster: Option, @@ -161,6 +176,12 @@ pub struct CreateNonceAccountAndSigningKey { from_address: String, } +#[derive(Serialize, Deserialize, Debug)] +pub struct GetNonceAccountData { + nonce_address: String, + cluster: Option, +} + #[derive(Serialize, Deserialize, Debug)] pub struct FindNonceAccounts { authorization_address: String, @@ -198,6 +219,19 @@ pub struct TransferToken { fee_payer_address: Option, } +#[derive(Serialize, Deserialize, Debug)] +pub struct Compile { + #[serde(flatten)] + hashable: Hashable, + derivation_accounts: Vec, + instructions: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Inspect { + transaction: solana_sdk::transaction::Transaction, +} + #[derive(Serialize, Deserialize, Debug)] pub struct Sign { blockhash: String, @@ -231,10 +265,13 @@ pub enum Operation { AwaitFunds(AwaitFunds), GetTokenInfo(GetTokenInfo), CreateNonceAccountAndSigningKey(CreateNonceAccountAndSigningKey), + GetNonceAccountData(GetNonceAccountData), FindNonceAccounts(FindNonceAccounts), Transfer(Transfer), CreateTokenAccount(CreateTokenAccount), TransferToken(TransferToken), + Compile(Compile), + Inspect(Inspect), Sign(Sign), Broadcast(Broadcast), } @@ -292,135 +329,148 @@ impl Module for Solana { .to_string(), r#type: ArgumentType::Required, }; - vec![ - icepick_module::help::Operation { - name: "get-blockhash".to_string(), - description: "Get the latest blockhash".to_string(), - arguments: vec![cluster.clone()], - }, - icepick_module::help::Operation { - name: "generate-wallet".to_string(), - description: "Generate the derivation index for a wallet.".to_string(), - arguments: vec![Argument { - name: "account".to_string(), - description: "The derivation account used for generating the wallet." - .to_string(), - r#type: ArgumentType::Optional, - }], - }, - icepick_module::help::Operation { - name: "get-wallet-address".to_string(), - description: "Get the address for a given wallet.".to_string(), - arguments: vec![], - }, - icepick_module::help::Operation { - name: "await-funds".to_string(), - description: "Await a minimum amount of funds in an account".to_string(), - arguments: vec![Argument { + let get_blockhash = icepick_module::help::Operation { + name: "get-blockhash".to_string(), + description: "Get the latest blockhash".to_string(), + arguments: vec![cluster.clone()], + }; + let generate_wallet = icepick_module::help::Operation { + name: "generate-wallet".to_string(), + description: "Generate the derivation index for a wallet.".to_string(), + arguments: vec![Argument { + name: "account".to_string(), + description: "The derivation account used for generating the wallet.".to_string(), + r#type: ArgumentType::Optional, + }], + }; + let get_wallet_address = icepick_module::help::Operation { + name: "get-wallet-address".to_string(), + description: "Get the address for a given wallet.".to_string(), + arguments: vec![], + }; + let await_funds = icepick_module::help::Operation { + name: "await-funds".to_string(), + description: "Await a minimum amount of funds in an account".to_string(), + arguments: vec![ + Argument { name: "address".to_string(), description: "The address to monitor".to_string(), r#type: ArgumentType::Required, - }, Argument { + }, + Argument { name: "amount".to_string(), description: "The amount of lamports to await".to_string(), r#type: ArgumentType::Required, - }], - }, - icepick_module::help::Operation { - name: "get-token-info".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(), + }, + ], + }; + let get_token_info = icepick_module::help::Operation { + name: "get-token-info".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, + }], + }; + let create_nonce_account_and_signing_key = icepick_module::help::Operation { + name: "create-nonce-account-and-signing-key".to_string(), + description: "Create a nonce account for signing durable transactions".to_string(), + arguments: vec![ + account.clone(), + from_address.clone(), + Argument { + name: "authorization_address".to_string(), + description: "The account authorized to use and advance the nonce.".to_string(), r#type: ArgumentType::Required, - }], - }, - icepick_module::help::Operation { - name: "create-nonce-account-and-signing-key".to_string(), - description: "Create a nonce account for signing durable transactions".to_string(), - arguments: vec![ - account.clone(), - from_address.clone(), - Argument { - name: "authorization_address".to_string(), - description: "The account authorized to use and advance the nonce.".to_string(), - r#type: ArgumentType::Required, - }, - ], - }, - icepick_module::help::Operation { - name: "find-nonce-accounts".to_string(), - description: "Find all nonce accounts for an authorized address".to_string(), - arguments: vec![ - cluster.clone(), - Argument { - name: "authorization_address".to_string(), - description: "The account authorized to use and advance nonces.".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." - .to_string(), - arguments: vec![ - Argument { - name: "amount".to_string(), - description: "The amount of SOL to transfer.".to_string(), - r#type: ArgumentType::Required, - }, - account.clone(), - Argument { - name: "to_address".to_string(), - description: "The address to send SOL to.".to_string(), - r#type: ArgumentType::Required, - }, - from_address.clone(), - fee.clone(), - fee_payer.clone(), - fee_payer_address.clone(), - ], - }, - icepick_module::help::Operation { - name: "stake".to_string(), - description: "Stake SOL to earn rewards.".to_string(), - arguments: vec![ - Argument { - name: "amount".to_string(), - description: "The amount of SOL to stake.".to_string(), - r#type: ArgumentType::Required, - }, - account.clone(), - from_address.clone(), - fee.clone(), - fee_payer.clone(), - fee_payer_address.clone(), - ], - }, - // kinda BS that you have to make an account for a token, but ok. - icepick_module::help::Operation { - name: "create-token-account".to_string(), - description: "Create an account for a given token".to_string(), - arguments: vec![ - Argument { - name: "wallet_address".to_string(), - description: "The address of the token.".to_string(), - r#type: ArgumentType::Required, - }, - Argument { - name: "token_address".to_string(), - description: "The address of the token.".to_string(), - r#type: ArgumentType::Required, - }, - Argument { - name: "funder_address".to_string(), - description: "The address of the funder (signer).".to_string(), - r#type: ArgumentType::Optional, - }, - ], - }, - icepick_module::help::Operation { + }, + ], + }; + let get_nonce_account_data = icepick_module::help::Operation { + name: "get-nonce-account-data".to_string(), + description: "Get the data for a nonce account".to_string(), + arguments: vec![ + cluster.clone(), + Argument { + name: "nonce_address".to_string(), + description: "The address of the nonce account.".to_string(), + r#type: ArgumentType::Required, + }, + ], + }; + let find_nonce_accounts = icepick_module::help::Operation { + name: "find-nonce-accounts".to_string(), + description: "Find all nonce accounts for an authorized address".to_string(), + arguments: vec![ + cluster.clone(), + Argument { + name: "authorization_address".to_string(), + description: "The account authorized to use and advance nonces.".to_string(), + r#type: ArgumentType::Required, + }, + ], + }; + + let transfer = icepick_module::help::Operation { + name: "transfer".to_string(), + description: "Transfer SOL from a Keyfork wallet to an external wallet.".to_string(), + arguments: vec![ + Argument { + name: "amount".to_string(), + description: "The amount of SOL to transfer.".to_string(), + r#type: ArgumentType::Required, + }, + account.clone(), + Argument { + name: "to_address".to_string(), + description: "The address to send SOL to.".to_string(), + r#type: ArgumentType::Required, + }, + from_address.clone(), + fee.clone(), + fee_payer.clone(), + fee_payer_address.clone(), + ], + }; + let stake = icepick_module::help::Operation { + name: "stake".to_string(), + description: "Stake SOL to earn rewards.".to_string(), + arguments: vec![ + Argument { + name: "amount".to_string(), + description: "The amount of SOL to stake.".to_string(), + r#type: ArgumentType::Required, + }, + account.clone(), + from_address.clone(), + fee.clone(), + fee_payer.clone(), + fee_payer_address.clone(), + ], + }; + // kinda BS that you have to make an account for a token, but ok. + let create_token_account = icepick_module::help::Operation { + name: "create-token-account".to_string(), + description: "Create an account for a given token".to_string(), + arguments: vec![ + Argument { + name: "wallet_address".to_string(), + description: "The address of the token.".to_string(), + r#type: ArgumentType::Required, + }, + Argument { + name: "token_address".to_string(), + description: "The address of the token.".to_string(), + r#type: ArgumentType::Required, + }, + Argument { + name: "funder_address".to_string(), + description: "The address of the funder (signer).".to_string(), + r#type: ArgumentType::Optional, + }, + ], + }; + let transfer_token = icepick_module::help::Operation { name: "transfer-token".to_string(), description: "Transfer tokens from a Keyfork wallet to an external wallet." .to_string(), @@ -455,21 +505,61 @@ impl Module for Solana { fee_payer.clone(), fee_payer_address.clone(), ], - }, - icepick_module::help::Operation { - name: "sign".to_string(), - description: "Sign a previously-generated transaction.".to_string(), - arguments: vec![Argument { - name: "blockhash".to_string(), - description: "A recent blockhash".to_string(), - r#type: ArgumentType::Required, - }], - }, - icepick_module::help::Operation { - name: "broadcast".to_string(), - description: "Broadcast a signed transaction".to_string(), - arguments: vec![cluster.clone()], - }, + }; + let compile = icepick_module::help::Operation { + name: "compile".to_string(), + description: "Compile instructions into a transaction".to_string(), + arguments: vec![ + Argument { + name: "blockhash".to_string(), + description: "A recent blockhash, must be provided in place of nonce" + .to_string(), + r#type: ArgumentType::Optional, + }, + Argument { + name: "nonce".to_string(), + description: "A durable nonce, must be provided in place of blockhash" + .to_string(), + r#type: ArgumentType::Optional, + }, + ], + }; + let inspect = icepick_module::help::Operation { + name: "inspect".to_string(), + description: "Print a transaction using base64.".to_string(), + arguments: vec![], + }; + let sign = icepick_module::help::Operation { + name: "sign".to_string(), + description: "Sign a previously-generated transaction.".to_string(), + arguments: vec![Argument { + name: "blockhash".to_string(), + description: "A recent blockhash".to_string(), + r#type: ArgumentType::Required, + }], + }; + let broadcast = icepick_module::help::Operation { + name: "broadcast".to_string(), + description: "Broadcast a signed transaction".to_string(), + arguments: vec![cluster.clone()], + }; + vec![ + get_blockhash, + generate_wallet, + get_wallet_address, + await_funds, + get_token_info, + create_nonce_account_and_signing_key, + get_nonce_account_data, + find_nonce_accounts, + transfer, + stake, + create_token_account, + transfer_token, + compile, + inspect, + sign, + broadcast, ] } @@ -594,6 +684,10 @@ impl Module for Solana { let from_pk = Pubkey::from_str(&from_address).unwrap(); let authorization_pk = Pubkey::from_str(&authorization_address).unwrap(); + if from_account.is_some() { + unimplemented!("alternative derivation accounts are not yet implemented"); + } + let instructions = system_instruction::create_nonce_account( &from_pk, &keypair.pubkey(), @@ -602,20 +696,35 @@ impl Module for Solana { 1500000, ); - let message = solana_sdk::message::Message::new(&instructions, None); - let transaction = solana_sdk::transaction::Transaction::new_unsigned(message); - let from_account = from_account - .and_then(|a| u32::from_str(&a).ok()) - .unwrap_or(0); - let requested_accounts = vec![from_account | 1 << 31]; - + #[allow(clippy::identity_op)] Ok(serde_json::json!({ "blob": { "nonce_pubkey": keypair.pubkey().to_string(), "nonce_privkey": [keypair.secret().to_bytes()], - "transaction": transaction, + "transaction": instructions, + }, + "derivation_accounts": [0u32 | 1 << 31], + })) + } + Operation::GetNonceAccountData(GetNonceAccountData { + nonce_address, + cluster, + }) => { + let nonce_pk = Pubkey::from_str(&nonce_address).unwrap(); + let cluster = cluster.unwrap_or(Cluster::MainnetBeta); + let cluster_url = format!("https://api.{cluster}.solana.com"); + let client = solana_rpc_client::rpc_client::RpcClient::new(cluster_url); + + let nonce_account = client.get_account(&nonce_pk).unwrap(); + let nonce = + solana_rpc_client_nonce_utils::data_from_account(&nonce_account).unwrap(); + + Ok(serde_json::json!({ + "blob": { + "authority": nonce.authority.to_string(), + "durable_nonce": nonce.durable_nonce.as_hash().to_string(), + "lamports_per_signature": nonce.fee_calculator.lamports_per_signature, }, - "derivation_accounts": requested_accounts, })) } Operation::FindNonceAccounts(FindNonceAccounts { @@ -749,56 +858,30 @@ impl Module for Solana { from_account, to_address, from_address, - fee: _, + fee, fee_payer, fee_payer_address, }) => { - // TODO: - // parse address for to_address + if from_account.is_some() { + unimplemented!("from_account"); + } + if fee.is_some() | fee_payer.is_some() | fee_payer_address.is_some() { + unimplemented!("fee") + } let amount = f64::from_str(&amount).expect("float amount"); let amount: u64 = (amount * LAMPORTS_PER_SOL as f64) as u64; let to_pk = Pubkey::from_str(&to_address).unwrap(); let from_pk = Pubkey::from_str(&from_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 instruction = system_instruction::transfer(&from_pk, &to_pk, amount); - let message = solana_sdk::message::Message::new( - &[instruction], - payer_account_and_pk.map(|v| v.1).as_ref(), - ); - let transaction = solana_sdk::transaction::Transaction::new_unsigned(message); - // TODO: error handling from_str - let from_account = from_account - .and_then(|a| u32::from_str(&a).ok()) - .unwrap_or(0); - let mut requested_accounts = vec![]; - requested_accounts.push(from_account | 1 << 31); - if let Some((account, _)) = &payer_account_and_pk { - requested_accounts.push(*account | 1 << 31); - } + #[allow(clippy::identity_op)] Ok(serde_json::json!({ "blob": { - "transaction": transaction, + "instructions": [instruction], + // This is done in blob since it's compiled in the next step + "derivation_accounts": [0u32 | 1 << 31], }, - "derivation_accounts": requested_accounts, })) } Operation::CreateTokenAccount(CreateTokenAccount { @@ -843,39 +926,23 @@ impl Module for Solana { fee_payer, fee_payer_address, }) => { - // TODO: deduplicate code used in Transfer + if from_account.is_some() { + unimplemented!("from_account"); + } + if fee.is_some() | fee_payer.is_some() | fee_payer_address.is_some() { + unimplemented!("fee") + } let amount = f64::from_str(&amount).expect("float amount"); let decimals = u8::from_str(&decimals).expect("decimals"); let amount: u64 = (amount * 10u64.pow(decimals as u32) as f64) as u64; + 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); @@ -892,36 +959,75 @@ impl Module for Solana { decimals, // decimals ) .unwrap(); - // TODO: check if this works with payer + + // TODO: check if this works with multisig // 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; - } + for account in instruction.accounts.iter_mut() { + if account.pubkey == from_pk { + account.is_writable = true; } } - let message = solana_sdk::message::Message::new( - &[instruction], - payer_account_and_pk.map(|v| v.1).as_ref(), - ); - // 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": { + "instructions": [instruction], + "derivation_accounts": [0u32 | 1 << 31], + }, + })) + } + Operation::Compile(Compile { + hashable, + derivation_accounts, + mut instructions, + }) => { + use solana_sdk::{hash::Hash, message::Message, transaction::Transaction}; + + let (hash, transaction) = match hashable { + // We already have the account from GetNonceAccountData, + // which also gives us the authority and the nonce itself. + Hashable::Nonce { + nonce_data, + nonce_address, + nonce_authority, + } => { + let account_pk = Pubkey::from_str(&nonce_address).unwrap(); + let authority_pk = Pubkey::from_str(&nonce_authority).unwrap(); + + let hash = Hash::from_str(&nonce_data).unwrap(); + let increment_nonce = + system_instruction::advance_nonce_account(&account_pk, &authority_pk); + + instructions.insert(0, increment_nonce); + let message = Message::new(&instructions, None); + let transaction = Transaction::new_unsigned(message); + (hash, transaction) + } + Hashable::Blockhash { blockhash } => { + let blockhash = Hash::from_str(&blockhash).unwrap(); + let message = Message::new(&instructions, None); + let transaction = Transaction::new_unsigned(message); + (blockhash, transaction) + } + }; + Ok(serde_json::json!({ + "blob": { + "hash": hash, "transaction": transaction, }, - "derivation_accounts": [0u32 | 1 << 31], + "derivation_accounts": derivation_accounts, + })) + } + Operation::Inspect(Inspect { transaction }) => { + use base64::prelude::*; + + Ok(serde_json::json!({ + "blob": { + "formatted_transaction": BASE64_STANDARD.encode(transaction.message_data()) + } })) } Operation::Sign(Sign {