use icepick_module::{ help::{Argument, ArgumentType}, Module, }; use serde::{Deserialize, Serialize}; use std::str::FromStr; // How does this not exist in solana_sdk. const LAMPORTS_PER_SOL: u64 = 1_000_000_000; #[derive(thiserror::Error, Debug)] pub enum Error {} #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "kebab-case")] pub struct Transfer { amount: String, blockhash: String, to_address: String, from_account: Option, from_address: String, fee: Option, fee_payer: Option, fee_payer_address: Option, } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "kebab-case")] pub struct Sign {} #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "kebab-case")] pub struct Request { // NOTE: Can't use the proper XPrv type from Keyfork because Solana's a big stinky // and adds in its own derivation constructs that cause type conflicts. derived_keys: Option>, // NOTE: This is an opaque type that can be deserialized inside an Operation blob: Option, #[serde(flatten)] operation: Operation, } #[derive(Serialize, Deserialize, Debug)] #[serde(tag = "operation", content = "values", rename_all = "kebab-case")] pub enum Operation { Transfer(Transfer), Sign(Sign), } pub struct Solana; impl Module for Solana { type Error = Error; type Request = Request; fn describe_operations() -> Vec { let account = Argument { name: "from-account".to_string(), description: "The derivation account used for the transaction.".to_string(), r#type: ArgumentType::Optional, }; let fee = Argument { name: "fee".to_string(), description: "A custom fee for the transaction".to_string(), r#type: ArgumentType::Optional, }; let fee_payer_address = Argument { name: "fee-payer-address".to_string(), description: "The address used to pay the fee.".to_string(), r#type: ArgumentType::Optional, }; let fee_payer = Argument { name: "fee-payer".to_string(), description: "The derivation account used to pay the fee.".to_string(), r#type: ArgumentType::Optional, }; let blockhash = Argument { name: "blockhash".to_string(), description: "A recent blockhash".to_string(), r#type: ArgumentType::Required, }; let from_address = Argument { name: "from-address".to_string(), description: concat!( "The address to send SOL from; will be used to verify ", "the derivation account." ) .to_string(), r#type: ArgumentType::Required, }; vec![ 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(), blockhash.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(), 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(), arguments: vec![], }, ] } fn handle_request(request: Self::Request) -> Result { match request.operation { Operation::Transfer(Transfer { amount, from_account, to_address, from_address, blockhash, fee: _, fee_payer, fee_payer_address, }) => { // TODO: // parse address for to_address let amount = f64::from_str(&amount).expect("float amount"); let amount: u64 = (amount * LAMPORTS_PER_SOL as f64) as u64; use solana_sdk::pubkey::Pubkey; 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((payer.clone(), Pubkey::from_str_const(address))) } (None, None) => { // Use the transaction account None } _ => panic!("Invalid combination of fee_payer and fee_payer_address"), } }; let instruction = solana_sdk::system_instruction::transfer(&from_pk, &to_pk, amount); 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, ); let transaction = solana_sdk::transaction::Transaction::new_unsigned(message); let mut required_derivation_indices = vec![]; // TODO: error handling from_str let from_account = from_account.and_then(|a| u32::from_str(&a).ok()).unwrap_or(0); required_derivation_indices.push(from_account); Ok(serde_json::json!({ "blob": transaction, })) } Operation::Sign(Sign {}) => { let blob = request.blob.expect("passed in instruction blob"); let transaction: solana_sdk::transaction::Transaction = serde_json::from_value(blob).expect("valid message blob"); dbg!(transaction); Ok(serde_json::json!({ "blob": [] })) } } } }