icepick/crates/by-chain/icepick-solana/src/lib.rs

208 lines
7.7 KiB
Rust

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<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 {}
#[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<Vec<[u8; 32]>>,
// NOTE: This is an opaque type that can be deserialized inside an Operation
blob: Option<serde_json::Value>,
#[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<icepick_module::help::Operation> {
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<serde_json::Value, Self::Error> {
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": []
}))
}
}
}
}