Compare commits
2 Commits
14f0be8921
...
15a82b4892
Author | SHA1 | Date |
---|---|---|
Ryan Heywood | 15a82b4892 | |
Ryan Heywood | 6703a5b3ce |
|
@ -1,3 +1,45 @@
|
||||||
|
//! Solana support for Icepick.
|
||||||
|
//!
|
||||||
|
//! # Command Line Arguments
|
||||||
|
//!
|
||||||
|
//! 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,
|
||||||
|
//! which Icepick will interpret upon subsequent invocations and replace with the derived key -
|
||||||
|
//! after appending the valid `bip44` path. You can then pipe the output of the command to
|
||||||
|
//! `icepick sol get-wallet-address`, which will take the derived key and format it as a Solana
|
||||||
|
//! address. In full, the commands look like:
|
||||||
|
//!
|
||||||
|
//! ```sh
|
||||||
|
//! icepick sol generate-wallet | icepick sol get-wallet-address
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Next, you'll want to either airdrop some funds into the wallet if on devnet, or transfer some
|
||||||
|
//! funds using another source if on mainnet.
|
||||||
|
//!
|
||||||
|
//!
|
||||||
|
//! To transfer funds out of the wallet, you'll need a recent blockhash. The command
|
||||||
|
//! `icepick sol get-blockhash` can be used to get a recent blockhash. This blockhash must be used
|
||||||
|
//! within 150 blocks, or about 1 minute, or transactions won't be accepted.
|
||||||
|
//!
|
||||||
|
//! The blockhash is then used in the next command, `icepick sol transfer`, which also requires an
|
||||||
|
//! amount, a to-address, and a from-address. The output of this command can then be saved to
|
||||||
|
//! removable media, then transferred to an offline signer system where `icepick sol sign` is used
|
||||||
|
//! to sign the transaction, with the signed result also persisted to removable media. Once the
|
||||||
|
//! signed transaction is back on an online system, the transaction can be broadcasted using
|
||||||
|
//! `icepick sol broadcast`.
|
||||||
|
//!
|
||||||
|
//! ```sh
|
||||||
|
//! # On an online system
|
||||||
|
//! blockhash=$(icepick sol get-blockhash | jq -r .blob)
|
||||||
|
//! icepick sol transfer $amount $to_address $from_address $blockhash > sdcard/unsigned.json
|
||||||
|
//!
|
||||||
|
//! # On an offline system
|
||||||
|
//! icepick sol sign < sdcard/unsigned.json > sdcard/signed.json
|
||||||
|
//!
|
||||||
|
//! # On the online system, again
|
||||||
|
//! icepick sol broadcast < sdcard/signed.json
|
||||||
|
//! ```
|
||||||
|
|
||||||
use icepick_module::{
|
use icepick_module::{
|
||||||
help::{Argument, ArgumentType},
|
help::{Argument, ArgumentType},
|
||||||
Module,
|
Module,
|
||||||
|
@ -14,12 +56,32 @@ pub enum Error {}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct GetBlockhash {}
|
pub enum Cluster {
|
||||||
|
Devnet,
|
||||||
|
Testnet,
|
||||||
|
Mainnet,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Cluster {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Cluster::Devnet => f.write_str("devnet"),
|
||||||
|
Cluster::Testnet => f.write_str("testnet"),
|
||||||
|
Cluster::Mainnet => f.write_str("mainnet"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct GetBlockhash {
|
||||||
|
cluster: Option<Cluster>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct GenerateWallet {
|
pub struct GenerateWallet {
|
||||||
account: String,
|
account: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
@ -45,7 +107,9 @@ pub struct Sign {}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct Broadcast {}
|
pub struct Broadcast {
|
||||||
|
cluster: Option<Cluster>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
@ -92,6 +156,11 @@ impl Module for Solana {
|
||||||
type Request = Request;
|
type Request = Request;
|
||||||
|
|
||||||
fn describe_operations() -> Vec<icepick_module::help::Operation> {
|
fn describe_operations() -> Vec<icepick_module::help::Operation> {
|
||||||
|
let cluster = Argument {
|
||||||
|
name: "cluster".to_string(),
|
||||||
|
description: "The cluster to interact with (mainnet, testnet, devnet)".to_string(),
|
||||||
|
r#type: ArgumentType::Optional,
|
||||||
|
};
|
||||||
let account = Argument {
|
let account = Argument {
|
||||||
name: "from-account".to_string(),
|
name: "from-account".to_string(),
|
||||||
description: "The derivation account used for the transaction.".to_string(),
|
description: "The derivation account used for the transaction.".to_string(),
|
||||||
|
@ -130,7 +199,7 @@ impl Module for Solana {
|
||||||
icepick_module::help::Operation {
|
icepick_module::help::Operation {
|
||||||
name: "get-blockhash".to_string(),
|
name: "get-blockhash".to_string(),
|
||||||
description: "Get the latest blockhash".to_string(),
|
description: "Get the latest blockhash".to_string(),
|
||||||
arguments: vec![],
|
arguments: vec![cluster.clone()],
|
||||||
},
|
},
|
||||||
icepick_module::help::Operation {
|
icepick_module::help::Operation {
|
||||||
name: "generate-wallet".to_string(),
|
name: "generate-wallet".to_string(),
|
||||||
|
@ -195,23 +264,24 @@ impl Module for Solana {
|
||||||
icepick_module::help::Operation {
|
icepick_module::help::Operation {
|
||||||
name: "broadcast".to_string(),
|
name: "broadcast".to_string(),
|
||||||
description: "Broadcast a signed transaction".to_string(),
|
description: "Broadcast a signed transaction".to_string(),
|
||||||
arguments: vec![],
|
arguments: vec![cluster.clone()],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_request(request: Self::Request) -> Result<serde_json::Value, Self::Error> {
|
fn handle_request(request: Self::Request) -> Result<serde_json::Value, Self::Error> {
|
||||||
match request.operation {
|
match request.operation {
|
||||||
Operation::GetBlockhash(_) => {
|
Operation::GetBlockhash(GetBlockhash { cluster }) => {
|
||||||
let devnet = "https://api.devnet.solana.com";
|
let cluster = cluster.unwrap_or(Cluster::Mainnet);
|
||||||
let client = solana_rpc_client::rpc_client::RpcClient::new(devnet);
|
let cluster_url = format!("https://api.{cluster}.solana.com");
|
||||||
|
let client = solana_rpc_client::rpc_client::RpcClient::new(cluster_url);
|
||||||
let response = client.get_latest_blockhash().unwrap();
|
let response = client.get_latest_blockhash().unwrap();
|
||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
"blob": response.to_string(),
|
"blob": response.to_string(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
Operation::GenerateWallet(GenerateWallet { account }) => {
|
Operation::GenerateWallet(GenerateWallet { account }) => {
|
||||||
let account = u32::from_str(&account).expect("account index");
|
let account = u32::from_str(account.as_deref().unwrap_or("0")).unwrap();
|
||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
"blob": null,
|
"blob": null,
|
||||||
"derivation-accounts": [(account | 1 << 31)],
|
"derivation-accounts": [(account | 1 << 31)],
|
||||||
|
@ -305,18 +375,18 @@ impl Module for Solana {
|
||||||
"blob": transaction,
|
"blob": transaction,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
Operation::Broadcast(_) => {
|
Operation::Broadcast(Broadcast { cluster }) => {
|
||||||
|
let cluster = cluster.unwrap_or(Cluster::Mainnet);
|
||||||
|
let cluster_url = format!("https://api.{cluster}.solana.com");
|
||||||
|
|
||||||
let blob = request.blob.expect("passed in instruction blob");
|
let blob = request.blob.expect("passed in instruction blob");
|
||||||
let transaction: solana_sdk::transaction::Transaction =
|
let transaction: solana_sdk::transaction::Transaction =
|
||||||
serde_json::from_value(blob).expect("valid message blob");
|
serde_json::from_value(blob).expect("valid message blob");
|
||||||
transaction.verify().expect("invalid signatures");
|
transaction.verify().expect("invalid signatures");
|
||||||
// TODO: make this a CLI option
|
let client = solana_rpc_client::rpc_client::RpcClient::new(cluster_url);
|
||||||
let devnet = "https://api.devnet.solana.com";
|
|
||||||
let client = solana_rpc_client::rpc_client::RpcClient::new(devnet);
|
|
||||||
let _simulated_response = client.simulate_transaction(&transaction).unwrap();
|
let _simulated_response = client.simulate_transaction(&transaction).unwrap();
|
||||||
let response = client.send_and_confirm_transaction(&transaction);
|
let response = client.send_and_confirm_transaction(&transaction);
|
||||||
Ok(
|
Ok(match response {
|
||||||
match response {
|
|
||||||
Ok(s) => {
|
Ok(s) => {
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"blob": {
|
"blob": {
|
||||||
|
|
Loading…
Reference in New Issue