Compare commits
6 Commits
ryansquare
...
main
Author | SHA1 | Date |
---|---|---|
Ryan Heywood | dd7736f5c8 | |
Ryan Heywood | da5126b664 | |
Ryan Heywood | c6736ba678 | |
Ryan Heywood | 7febc8c1b1 | |
Ryan Heywood | ca0fc3eef9 | |
Ryan Heywood | be5f9a9fa0 |
File diff suppressed because it is too large
Load Diff
|
@ -29,6 +29,11 @@ pub enum Request {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
values: serde_json::Value,
|
values: serde_json::Value,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Cat {
|
||||||
|
#[serde(flatten)]
|
||||||
|
values: serde_json::Value,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
@ -58,6 +63,12 @@ impl Module for Internal {
|
||||||
description: "Save data from a JSON file.".to_string(),
|
description: "Save data from a JSON file.".to_string(),
|
||||||
arguments: vec![filename.clone()],
|
arguments: vec![filename.clone()],
|
||||||
},
|
},
|
||||||
|
icepick_module::help::Operation {
|
||||||
|
name: "cat".to_string(),
|
||||||
|
description: "Return all inputs. Usable in workflows to sum up all desired outputs"
|
||||||
|
.to_string(),
|
||||||
|
arguments: vec![],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,6 +111,11 @@ impl Module for Internal {
|
||||||
"blob": {},
|
"blob": {},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
Request::Cat { values } => {
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"blob": values,
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,18 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
|
bincode = "1.3.3"
|
||||||
|
bs58 = "0.5.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"] }
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
solana-rpc-client = { version = "2.1.1", default-features = false }
|
solana-rpc-client = { version = "2.1.1", default-features = false }
|
||||||
|
solana-rpc-client-api = "2.1.7"
|
||||||
|
solana-rpc-client-nonce-utils = "2.1.7"
|
||||||
solana-sdk = { version = "2.1.1" }
|
solana-sdk = { version = "2.1.1" }
|
||||||
|
solana-transaction-status = "2.1.1"
|
||||||
|
solana-transaction-status-client-types = "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"
|
spl-token-2022 = "6.0.0"
|
||||||
|
|
|
@ -63,12 +63,39 @@ use icepick_module::{
|
||||||
Module,
|
Module,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use solana_sdk::signer::Signer;
|
use solana_rpc_client::rpc_client::SerializableTransaction;
|
||||||
use std::str::FromStr;
|
use solana_rpc_client_api::client_error::Result as ClientResult;
|
||||||
|
use solana_sdk::{
|
||||||
|
pubkey::Pubkey,
|
||||||
|
signer::{keypair::Keypair, Signer},
|
||||||
|
system_instruction,
|
||||||
|
transaction::TransactionError,
|
||||||
|
};
|
||||||
|
use std::{collections::HashSet, str::FromStr};
|
||||||
|
|
||||||
// How does this not exist in solana_sdk.
|
// How does this not exist in solana_sdk.
|
||||||
const LAMPORTS_PER_SOL: u64 = 1_000_000_000;
|
const LAMPORTS_PER_SOL: u64 = 1_000_000_000;
|
||||||
|
|
||||||
|
fn get_account(
|
||||||
|
account_index: impl Into<Option<u8>>,
|
||||||
|
account_keys: &[String],
|
||||||
|
instruction_keys: &[u8],
|
||||||
|
) -> Pubkey {
|
||||||
|
let instruction_index: usize = account_index
|
||||||
|
.into()
|
||||||
|
.expect("account index did not exist")
|
||||||
|
.into();
|
||||||
|
let account_index: usize = instruction_keys
|
||||||
|
.get(instruction_index)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or_else(|| panic!("instruction account {instruction_index} did not exist"))
|
||||||
|
.into();
|
||||||
|
let account_string = account_keys
|
||||||
|
.get(account_index)
|
||||||
|
.unwrap_or_else(|| panic!("account at index {account_index} did not exist"));
|
||||||
|
Pubkey::from_str(account_string).expect("could not parse account from string")
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum Error {}
|
pub enum Error {}
|
||||||
|
|
||||||
|
@ -105,6 +132,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)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct GetBlockhash {
|
pub struct GetBlockhash {
|
||||||
cluster: Option<Cluster>,
|
cluster: Option<Cluster>,
|
||||||
|
@ -118,11 +160,37 @@ pub struct GenerateWallet {
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct GetWalletAddress {}
|
pub struct GetWalletAddress {}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct AwaitFunds {
|
||||||
|
address: String,
|
||||||
|
lamports: String,
|
||||||
|
cluster: Option<Cluster>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct GetTokenInfo {
|
pub struct GetTokenInfo {
|
||||||
token: String,
|
token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct CreateNonceAccountAndSigningKey {
|
||||||
|
authorization_address: String,
|
||||||
|
from_account: Option<String>,
|
||||||
|
from_address: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct GetNonceAccountData {
|
||||||
|
nonce_address: String,
|
||||||
|
cluster: Option<Cluster>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct FindNonceAccounts {
|
||||||
|
authorization_address: String,
|
||||||
|
cluster: Option<Cluster>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct Transfer {
|
pub struct Transfer {
|
||||||
amount: String,
|
amount: String,
|
||||||
|
@ -154,10 +222,25 @@ pub struct TransferToken {
|
||||||
fee_payer_address: Option<String>,
|
fee_payer_address: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct Compile {
|
||||||
|
#[serde(flatten)]
|
||||||
|
hashable: Hashable,
|
||||||
|
derivation_accounts: Vec<u32>,
|
||||||
|
instructions: Vec<solana_sdk::instruction::Instruction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct Inspect {
|
||||||
|
transaction: solana_sdk::transaction::Transaction,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct Sign {
|
pub struct Sign {
|
||||||
blockhash: String,
|
blockhash: String,
|
||||||
transaction: solana_sdk::transaction::Transaction,
|
transaction: solana_sdk::transaction::Transaction,
|
||||||
|
#[serde(default)]
|
||||||
|
signing_keys: Vec<[u8; Keypair::SECRET_KEY_LENGTH]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
@ -182,10 +265,16 @@ pub enum Operation {
|
||||||
GetBlockhash(GetBlockhash),
|
GetBlockhash(GetBlockhash),
|
||||||
GenerateWallet(GenerateWallet),
|
GenerateWallet(GenerateWallet),
|
||||||
GetWalletAddress(GetWalletAddress),
|
GetWalletAddress(GetWalletAddress),
|
||||||
|
AwaitFunds(AwaitFunds),
|
||||||
GetTokenInfo(GetTokenInfo),
|
GetTokenInfo(GetTokenInfo),
|
||||||
|
CreateNonceAccountAndSigningKey(CreateNonceAccountAndSigningKey),
|
||||||
|
GetNonceAccountData(GetNonceAccountData),
|
||||||
|
FindNonceAccounts(FindNonceAccounts),
|
||||||
Transfer(Transfer),
|
Transfer(Transfer),
|
||||||
CreateTokenAccount(CreateTokenAccount),
|
CreateTokenAccount(CreateTokenAccount),
|
||||||
TransferToken(TransferToken),
|
TransferToken(TransferToken),
|
||||||
|
Compile(Compile),
|
||||||
|
Inspect(Inspect),
|
||||||
Sign(Sign),
|
Sign(Sign),
|
||||||
Broadcast(Broadcast),
|
Broadcast(Broadcast),
|
||||||
}
|
}
|
||||||
|
@ -193,14 +282,13 @@ pub enum Operation {
|
||||||
pub struct Solana;
|
pub struct Solana;
|
||||||
|
|
||||||
impl Solana {
|
impl Solana {
|
||||||
fn keypair_from_bytes(given_bytes: [u8; 32]) -> solana_sdk::signer::keypair::Keypair {
|
fn keypair_from_bytes(given_bytes: [u8; 32]) -> Keypair {
|
||||||
use ed25519_dalek::{PublicKey, SecretKey};
|
use ed25519_dalek::{PublicKey, SecretKey};
|
||||||
let secret_key = SecretKey::from_bytes(&given_bytes).expect("key should be 32 bytes");
|
let secret_key = SecretKey::from_bytes(&given_bytes).expect("key should be 32 bytes");
|
||||||
let mut bytes = [0u8; 64];
|
let mut bytes = [0u8; 64];
|
||||||
bytes[..32].clone_from_slice(&given_bytes);
|
bytes[..32].clone_from_slice(&given_bytes);
|
||||||
bytes[32..].clone_from_slice(PublicKey::from(&secret_key).as_bytes());
|
bytes[32..].clone_from_slice(PublicKey::from(&secret_key).as_bytes());
|
||||||
solana_sdk::signer::keypair::Keypair::from_bytes(&bytes)
|
Keypair::from_bytes(&bytes).expect("solana sdk should expect 64 bytes")
|
||||||
.expect("solana sdk should expect 64 bytes")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -244,28 +332,42 @@ impl Module for Solana {
|
||||||
.to_string(),
|
.to_string(),
|
||||||
r#type: ArgumentType::Required,
|
r#type: ArgumentType::Required,
|
||||||
};
|
};
|
||||||
vec![
|
let get_blockhash = 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![cluster.clone()],
|
arguments: vec![cluster.clone()],
|
||||||
},
|
};
|
||||||
icepick_module::help::Operation {
|
let generate_wallet = icepick_module::help::Operation {
|
||||||
name: "generate-wallet".to_string(),
|
name: "generate-wallet".to_string(),
|
||||||
description: "Generate the derivation index for a wallet.".to_string(),
|
description: "Generate the derivation index for a wallet.".to_string(),
|
||||||
arguments: vec![Argument {
|
arguments: vec![Argument {
|
||||||
name: "account".to_string(),
|
name: "account".to_string(),
|
||||||
description: "The derivation account used for generating the wallet."
|
description: "The derivation account used for generating the wallet.".to_string(),
|
||||||
.to_string(),
|
|
||||||
r#type: ArgumentType::Optional,
|
r#type: ArgumentType::Optional,
|
||||||
}],
|
}],
|
||||||
},
|
};
|
||||||
icepick_module::help::Operation {
|
let get_wallet_address = icepick_module::help::Operation {
|
||||||
name: "get-wallet-address".to_string(),
|
name: "get-wallet-address".to_string(),
|
||||||
description: "Get the address for a given wallet.".to_string(),
|
description: "Get the address for a given wallet.".to_string(),
|
||||||
arguments: vec![],
|
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,
|
||||||
},
|
},
|
||||||
icepick_module::help::Operation {
|
Argument {
|
||||||
|
name: "amount".to_string(),
|
||||||
|
description: "The amount of lamports to await".to_string(),
|
||||||
|
r#type: ArgumentType::Required,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let get_token_info = icepick_module::help::Operation {
|
||||||
name: "get-token-info".to_string(),
|
name: "get-token-info".to_string(),
|
||||||
description: "Get the address for a given token.".to_string(),
|
description: "Get the address for a given token.".to_string(),
|
||||||
arguments: vec![Argument {
|
arguments: vec![Argument {
|
||||||
|
@ -273,11 +375,48 @@ impl Module for Solana {
|
||||||
description: "The token to look up".to_string(),
|
description: "The token to look up".to_string(),
|
||||||
r#type: ArgumentType::Required,
|
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 {
|
],
|
||||||
|
};
|
||||||
|
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(),
|
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.".to_string(),
|
||||||
.to_string(),
|
|
||||||
arguments: vec![
|
arguments: vec![
|
||||||
Argument {
|
Argument {
|
||||||
name: "amount".to_string(),
|
name: "amount".to_string(),
|
||||||
|
@ -295,8 +434,8 @@ impl Module for Solana {
|
||||||
fee_payer.clone(),
|
fee_payer.clone(),
|
||||||
fee_payer_address.clone(),
|
fee_payer_address.clone(),
|
||||||
],
|
],
|
||||||
},
|
};
|
||||||
icepick_module::help::Operation {
|
let stake = icepick_module::help::Operation {
|
||||||
name: "stake".to_string(),
|
name: "stake".to_string(),
|
||||||
description: "Stake SOL to earn rewards.".to_string(),
|
description: "Stake SOL to earn rewards.".to_string(),
|
||||||
arguments: vec![
|
arguments: vec![
|
||||||
|
@ -311,9 +450,9 @@ impl Module for Solana {
|
||||||
fee_payer.clone(),
|
fee_payer.clone(),
|
||||||
fee_payer_address.clone(),
|
fee_payer_address.clone(),
|
||||||
],
|
],
|
||||||
},
|
};
|
||||||
// kinda BS that you have to make an account for a token, but ok.
|
// kinda BS that you have to make an account for a token, but ok.
|
||||||
icepick_module::help::Operation {
|
let create_token_account = icepick_module::help::Operation {
|
||||||
name: "create-token-account".to_string(),
|
name: "create-token-account".to_string(),
|
||||||
description: "Create an account for a given token".to_string(),
|
description: "Create an account for a given token".to_string(),
|
||||||
arguments: vec![
|
arguments: vec![
|
||||||
|
@ -333,8 +472,8 @@ impl Module for Solana {
|
||||||
r#type: ArgumentType::Optional,
|
r#type: ArgumentType::Optional,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
};
|
||||||
icepick_module::help::Operation {
|
let transfer_token = icepick_module::help::Operation {
|
||||||
name: "transfer-token".to_string(),
|
name: "transfer-token".to_string(),
|
||||||
description: "Transfer tokens from a Keyfork wallet to an external wallet."
|
description: "Transfer tokens from a Keyfork wallet to an external wallet."
|
||||||
.to_string(),
|
.to_string(),
|
||||||
|
@ -369,8 +508,31 @@ impl Module for Solana {
|
||||||
fee_payer.clone(),
|
fee_payer.clone(),
|
||||||
fee_payer_address.clone(),
|
fee_payer_address.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,
|
||||||
},
|
},
|
||||||
icepick_module::help::Operation {
|
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(),
|
name: "sign".to_string(),
|
||||||
description: "Sign a previously-generated transaction.".to_string(),
|
description: "Sign a previously-generated transaction.".to_string(),
|
||||||
arguments: vec![Argument {
|
arguments: vec![Argument {
|
||||||
|
@ -378,12 +540,29 @@ impl Module for Solana {
|
||||||
description: "A recent blockhash".to_string(),
|
description: "A recent blockhash".to_string(),
|
||||||
r#type: ArgumentType::Required,
|
r#type: ArgumentType::Required,
|
||||||
}],
|
}],
|
||||||
},
|
};
|
||||||
icepick_module::help::Operation {
|
let broadcast = 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![cluster.clone()],
|
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,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -418,6 +597,51 @@ impl Module for Solana {
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
Operation::AwaitFunds(AwaitFunds {
|
||||||
|
address,
|
||||||
|
lamports,
|
||||||
|
cluster,
|
||||||
|
}) => {
|
||||||
|
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 account_pk = Pubkey::from_str(&address).unwrap();
|
||||||
|
let minimum_balance = u64::from_str(&lamports).unwrap();
|
||||||
|
|
||||||
|
let sleep = || {
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(10));
|
||||||
|
};
|
||||||
|
|
||||||
|
let account_balance = loop {
|
||||||
|
let account = match client.get_account(&account_pk) {
|
||||||
|
Ok(account) => account,
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!("Waiting for account to be created and funded: {account_pk}");
|
||||||
|
sleep();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let account_size = account.data.len();
|
||||||
|
let rent = client
|
||||||
|
.get_minimum_balance_for_rent_exemption(account_size)
|
||||||
|
.unwrap();
|
||||||
|
let balance = account.lamports;
|
||||||
|
if balance
|
||||||
|
.checked_sub(rent)
|
||||||
|
.is_some_and(|bal| bal > minimum_balance)
|
||||||
|
{
|
||||||
|
break balance;
|
||||||
|
}
|
||||||
|
eprintln!("Waiting for {minimum_balance} + rent ({rent}) in {account_pk}");
|
||||||
|
sleep();
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"blob": {
|
||||||
|
"lamports": account_balance,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
Operation::GetTokenInfo(GetTokenInfo { token }) => {
|
Operation::GetTokenInfo(GetTokenInfo { token }) => {
|
||||||
let values = match token.as_str() {
|
let values = match token.as_str() {
|
||||||
// Only exists on devnet
|
// Only exists on devnet
|
||||||
|
@ -443,63 +667,224 @@ impl Module for Solana {
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Operation::CreateNonceAccountAndSigningKey(CreateNonceAccountAndSigningKey {
|
||||||
|
authorization_address,
|
||||||
|
from_account,
|
||||||
|
from_address,
|
||||||
|
}) => {
|
||||||
|
// NOTE: Since this transaction is meant to be run on an online system with a
|
||||||
|
// freshly generated mnemonic, only designed to live to make the nonce account, we
|
||||||
|
// are going to assume we're not using a separate fee payer. It's a stretch having
|
||||||
|
// a `--from-account` option, really, but it is probably to be expected given the
|
||||||
|
// `from-address` variable. In truth, we will likely have the account randomly
|
||||||
|
// generated using `generate-wallet | get-wallet-address`.
|
||||||
|
|
||||||
|
// NOTE: new() calls generate() which requires CryptoRng. By default,
|
||||||
|
// this uses OsRng, which sources from getrandom() if available, which pulls from
|
||||||
|
// /dev/urandom, or sources from `/dev/urandom` directly.
|
||||||
|
let keypair = Keypair::new();
|
||||||
|
|
||||||
|
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(),
|
||||||
|
&authorization_pk,
|
||||||
|
// just above the approximate rent necessary for a nonce account
|
||||||
|
1500000,
|
||||||
|
);
|
||||||
|
|
||||||
|
#[allow(clippy::identity_op)]
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"blob": {
|
||||||
|
"nonce_pubkey": keypair.pubkey().to_string(),
|
||||||
|
"nonce_privkey": [keypair.secret().to_bytes()],
|
||||||
|
"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,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
Operation::FindNonceAccounts(FindNonceAccounts {
|
||||||
|
authorization_address,
|
||||||
|
cluster,
|
||||||
|
}) => {
|
||||||
|
use solana_sdk::{
|
||||||
|
instruction::CompiledInstruction, system_instruction::SystemInstruction,
|
||||||
|
};
|
||||||
|
use solana_transaction_status_client_types::{
|
||||||
|
EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction,
|
||||||
|
EncodedTransactionWithStatusMeta, UiMessage, UiRawMessage, UiTransaction,
|
||||||
|
};
|
||||||
|
|
||||||
|
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 authorized_pk = Pubkey::from_str(&authorization_address).unwrap();
|
||||||
|
|
||||||
|
let mut nonced_accounts: HashSet<Pubkey> = HashSet::new();
|
||||||
|
|
||||||
|
let transaction_statuses =
|
||||||
|
client.get_signatures_for_address(&authorized_pk).unwrap();
|
||||||
|
|
||||||
|
for status in transaction_statuses
|
||||||
|
/*.iter().rev()*/
|
||||||
|
{
|
||||||
|
let signature = solana_sdk::signature::Signature::from_str(&status.signature)
|
||||||
|
.expect("cluster provided invalid signature");
|
||||||
|
let transaction = client
|
||||||
|
.get_transaction_with_config(&signature, Default::default())
|
||||||
|
.unwrap();
|
||||||
|
let EncodedConfirmedTransactionWithStatusMeta {
|
||||||
|
slot: _,
|
||||||
|
block_time: _,
|
||||||
|
transaction:
|
||||||
|
EncodedTransactionWithStatusMeta {
|
||||||
|
meta: _,
|
||||||
|
version: _,
|
||||||
|
transaction:
|
||||||
|
EncodedTransaction::Json(UiTransaction {
|
||||||
|
signatures: _,
|
||||||
|
message:
|
||||||
|
UiMessage::Raw(UiRawMessage {
|
||||||
|
header: _,
|
||||||
|
account_keys,
|
||||||
|
recent_blockhash: _,
|
||||||
|
address_table_lookups: _,
|
||||||
|
instructions,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}: EncodedConfirmedTransactionWithStatusMeta = transaction
|
||||||
|
else {
|
||||||
|
eprintln!("Unable to destructure transaction");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
// search for program based on the following:
|
||||||
|
// * program is SystemProgram
|
||||||
|
// * instruction is
|
||||||
|
for ui_instruction in &instructions {
|
||||||
|
let instruction = CompiledInstruction {
|
||||||
|
program_id_index: ui_instruction.program_id_index,
|
||||||
|
accounts: ui_instruction.accounts.clone(),
|
||||||
|
data: bs58::decode(ui_instruction.data.as_bytes())
|
||||||
|
.into_vec()
|
||||||
|
.unwrap(),
|
||||||
|
};
|
||||||
|
let program_pk = account_keys
|
||||||
|
.get(instruction.program_id_index as usize)
|
||||||
|
.map(|k| &**k)
|
||||||
|
.map(Pubkey::from_str)
|
||||||
|
.transpose()
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.expect("could not get program key from transaction");
|
||||||
|
if solana_sdk::system_program::check_id(&program_pk) {
|
||||||
|
let parsed_instruction: SystemInstruction =
|
||||||
|
bincode::deserialize(&instruction.data).unwrap();
|
||||||
|
match parsed_instruction {
|
||||||
|
SystemInstruction::InitializeNonceAccount(pubkey) => {
|
||||||
|
// [Nonce, RecentBlockhashes, Rent]
|
||||||
|
// Argument is new authority
|
||||||
|
let nonce_account =
|
||||||
|
get_account(0, &account_keys, &instruction.accounts);
|
||||||
|
if authorized_pk == pubkey {
|
||||||
|
nonced_accounts.insert(nonce_account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SystemInstruction::AuthorizeNonceAccount(pubkey) => {
|
||||||
|
// [Nonce, Authority]
|
||||||
|
// Argument is new authority
|
||||||
|
let nonce_account =
|
||||||
|
get_account(0, &account_keys, &instruction.accounts);
|
||||||
|
let authorizing_pk =
|
||||||
|
get_account(1, &account_keys, &instruction.accounts);
|
||||||
|
if authorized_pk == pubkey {
|
||||||
|
// we are given it
|
||||||
|
nonced_accounts.insert(nonce_account);
|
||||||
|
} else if authorizing_pk == pubkey {
|
||||||
|
// we are giving it
|
||||||
|
nonced_accounts.remove(&nonce_account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SystemInstruction::WithdrawNonceAccount(_lamports) => {
|
||||||
|
// [Nonce, Recipient, RecentBlockhashes, Rent, Authority]
|
||||||
|
// Because the nonce account will be deleted due to nonpayment
|
||||||
|
// of rent, we do not re-insert into created accounts.
|
||||||
|
let nonce_account =
|
||||||
|
get_account(0, &account_keys, &instruction.accounts);
|
||||||
|
nonced_accounts.remove(&nonce_account);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let nonced_accounts = nonced_accounts
|
||||||
|
.iter()
|
||||||
|
.map(|account| account.to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"blob": {
|
||||||
|
"nonced_accounts": nonced_accounts,
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
Operation::Transfer(Transfer {
|
Operation::Transfer(Transfer {
|
||||||
amount,
|
amount,
|
||||||
from_account,
|
from_account,
|
||||||
to_address,
|
to_address,
|
||||||
from_address,
|
from_address,
|
||||||
fee: _,
|
fee,
|
||||||
fee_payer,
|
fee_payer,
|
||||||
fee_payer_address,
|
fee_payer_address,
|
||||||
}) => {
|
}) => {
|
||||||
// TODO:
|
if from_account.is_some() {
|
||||||
// parse address for to_address
|
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 = f64::from_str(&amount).expect("float amount");
|
||||||
let amount: u64 = (amount * LAMPORTS_PER_SOL as f64) as u64;
|
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 to_pk = Pubkey::from_str(&to_address).unwrap();
|
||||||
let from_pk = Pubkey::from_str(&from_address).unwrap();
|
let from_pk = Pubkey::from_str(&from_address).unwrap();
|
||||||
let payer_account_and_pk = {
|
let instruction = system_instruction::transfer(&from_pk, &to_pk, amount);
|
||||||
// If a fee payer is given, a fee payer address must also be given, since the
|
#[allow(clippy::identity_op)]
|
||||||
// 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 =
|
|
||||||
solana_sdk::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);
|
|
||||||
}
|
|
||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
"blob": {
|
"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 {
|
Operation::CreateTokenAccount(CreateTokenAccount {
|
||||||
|
@ -508,9 +893,9 @@ impl Module for Solana {
|
||||||
token_address,
|
token_address,
|
||||||
}) => {
|
}) => {
|
||||||
// TODO: allow changing derivation account of funder_address
|
// TODO: allow changing derivation account of funder_address
|
||||||
use sata::instruction::create_associated_token_account;
|
|
||||||
use solana_sdk::pubkey::Pubkey;
|
|
||||||
use spl_associated_token_account as sata;
|
use spl_associated_token_account as sata;
|
||||||
|
|
||||||
|
use sata::instruction::create_associated_token_account;
|
||||||
use spl_token::ID as TOKEN_ID;
|
use spl_token::ID as TOKEN_ID;
|
||||||
let funder_address = funder_address.unwrap_or_else(|| wallet_address.clone());
|
let funder_address = funder_address.unwrap_or_else(|| wallet_address.clone());
|
||||||
let funder_pubkey = Pubkey::from_str(&funder_address).unwrap();
|
let funder_pubkey = Pubkey::from_str(&funder_address).unwrap();
|
||||||
|
@ -544,7 +929,12 @@ impl Module for Solana {
|
||||||
fee_payer,
|
fee_payer,
|
||||||
fee_payer_address,
|
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 amount = f64::from_str(&amount).expect("float amount");
|
||||||
let decimals = u8::from_str(&decimals).expect("decimals");
|
let decimals = u8::from_str(&decimals).expect("decimals");
|
||||||
|
@ -555,29 +945,7 @@ impl Module for Solana {
|
||||||
let to_pk = Pubkey::from_str(&to_address).unwrap();
|
let to_pk = Pubkey::from_str(&to_address).unwrap();
|
||||||
let from_pk = Pubkey::from_str(&from_address).unwrap();
|
let from_pk = Pubkey::from_str(&from_address).unwrap();
|
||||||
let token_pk = Pubkey::from_str(&token_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 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 from_token_address = get_associated_token_address(&from_pk, &token_pk);
|
||||||
let to_token_address = get_associated_token_address(&to_pk, &token_pk);
|
let to_token_address = get_associated_token_address(&to_pk, &token_pk);
|
||||||
|
@ -594,46 +962,87 @@ impl Module for Solana {
|
||||||
decimals, // decimals
|
decimals, // decimals
|
||||||
)
|
)
|
||||||
.unwrap();
|
.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
|
// 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
|
// 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
|
// payer is not provided. The transactional account must be writable for the
|
||||||
// computation fee to be paid.
|
// computation fee to be paid.
|
||||||
if payer_account_and_pk.is_none() {
|
|
||||||
for account in instruction.accounts.iter_mut() {
|
for account in instruction.accounts.iter_mut() {
|
||||||
if account.pubkey == from_pk {
|
if account.pubkey == from_pk {
|
||||||
account.is_writable = true;
|
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)]
|
#[allow(clippy::identity_op)]
|
||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
"blob": {
|
"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,
|
"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 {
|
Operation::Sign(Sign {
|
||||||
blockhash,
|
blockhash,
|
||||||
mut transaction,
|
mut transaction,
|
||||||
|
signing_keys,
|
||||||
}) => {
|
}) => {
|
||||||
let keys = request
|
let keys = request
|
||||||
.derived_keys
|
.derived_keys
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.iter()
|
.iter()
|
||||||
|
.chain(&signing_keys)
|
||||||
.map(|k| Self::keypair_from_bytes(*k))
|
.map(|k| Self::keypair_from_bytes(*k))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
@ -675,7 +1084,37 @@ impl Module for Solana {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(_) => {
|
||||||
|
let signature = transaction.get_signature();
|
||||||
|
let status = client.get_signature_status(signature);
|
||||||
|
blob_for_signature_status(status, signature, &cluster_suffix)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blob_for_signature_status(
|
||||||
|
status: ClientResult<Option<Result<(), TransactionError>>>,
|
||||||
|
signature: &solana_sdk::signature::Signature,
|
||||||
|
cluster_suffix: &str,
|
||||||
|
) -> serde_json::Value {
|
||||||
|
match status {
|
||||||
|
Ok(Some(Ok(()))) => {
|
||||||
|
// transaction passed.
|
||||||
|
eprintln!("An error occurred while broadcasting the transaction, but the transaction was confirmed manually.");
|
||||||
|
serde_json::json!({
|
||||||
|
"blob": {
|
||||||
|
"status": "send_and_confirm",
|
||||||
|
"succcess": signature.to_string(),
|
||||||
|
"url": format!("https://explorer.solana.com/tx/{signature}{cluster_suffix}"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Ok(Some(Err(e))) => {
|
||||||
|
// transaction failed on-cluster
|
||||||
|
eprintln!("The transaction failed on-chain: {e}");
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"blob": {
|
"blob": {
|
||||||
"status": "send_and_confirm",
|
"status": "send_and_confirm",
|
||||||
|
@ -683,8 +1122,25 @@ impl Module for Solana {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
// transaction may not have been broadcast
|
||||||
|
eprintln!("The transaction was possibly not received by the cluster.");
|
||||||
|
serde_json::json!({
|
||||||
|
"blob": {
|
||||||
|
"status": "send_and_confirm",
|
||||||
|
"error": format!("Transaction {signature} does not exist on-cluster"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// RPC request failed
|
||||||
|
eprintln!("An error occurred while interacting with the cluster: {e}");
|
||||||
|
serde_json::json!({
|
||||||
|
"blob": {
|
||||||
|
"status": "send_and_confirm",
|
||||||
|
"error": e.to_string(),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -12,6 +12,8 @@ use super::{derive_keys, get_command, Commands, ModuleConfig, Operation};
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct Workflow {
|
pub struct Workflow {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
pub inputs: Vec<String>,
|
pub inputs: Vec<String>,
|
||||||
|
|
||||||
#[serde(rename = "step")]
|
#[serde(rename = "step")]
|
||||||
|
|
|
@ -4,6 +4,9 @@ read -r from_address
|
||||||
printf "%s" "Public key of the recipient address: "
|
printf "%s" "Public key of the recipient address: "
|
||||||
read -r to_address
|
read -r to_address
|
||||||
|
|
||||||
|
printf "%s" "Publick ey of the nonce account: "
|
||||||
|
read -r nonce_address
|
||||||
|
|
||||||
printf "%s" "Name of the token to transfer: "
|
printf "%s" "Name of the token to transfer: "
|
||||||
read -r token_name
|
read -r token_name
|
||||||
|
|
||||||
|
@ -21,4 +24,4 @@ cat <<EOF > /data/input.json
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
icepick workflow sol broadcast --cluster devnet
|
icepick workflow sol broadcast --cluster devnet --nonce-address "$nonce_address"
|
||||||
|
|
135
icepick.toml
135
icepick.toml
|
@ -29,16 +29,17 @@ inputs = { token = "token_name" }
|
||||||
# we want to store, and the value is the name to be assigned in storage.
|
# we want to store, and the value is the name to be assigned in storage.
|
||||||
outputs = { token_address = "token_address", token_decimals = "token_decimals" }
|
outputs = { token_address = "token_address", token_decimals = "token_decimals" }
|
||||||
|
|
||||||
# Load the Blockhash from the SD card
|
# Load the transaction nonce from the SD card
|
||||||
[[module.workflow.step]]
|
[[module.workflow.step]]
|
||||||
type = "internal-load-file"
|
type = "internal-load-file"
|
||||||
|
|
||||||
# Pre-defined values to be passed to the module
|
# Pre-defined values to be passed to the module.
|
||||||
values = { filename = "blockhash.json" }
|
# In this case, the `filename` field is reserved for marking which file to load.
|
||||||
|
values = { filename = "nonce.json" }
|
||||||
|
|
||||||
# This value is marked to be saved in-memory, and can be used as an input for
|
# This value is marked to be saved in-memory, and can be used as an input for
|
||||||
# later steps.
|
# later steps.
|
||||||
outputs = { blockhash = "blockhash" }
|
outputs = { nonce_authority = "nonce_authority", nonce_data = "nonce_data", nonce_address = "nonce_address" }
|
||||||
|
|
||||||
[[module.workflow.step]]
|
[[module.workflow.step]]
|
||||||
# Generate an unsigned Transaction
|
# Generate an unsigned Transaction
|
||||||
|
@ -58,6 +59,20 @@ decimals = "token_decimals"
|
||||||
to_address = "to_address"
|
to_address = "to_address"
|
||||||
from_address = "from_address"
|
from_address = "from_address"
|
||||||
|
|
||||||
|
[module.workflow.step.outputs]
|
||||||
|
instructions = "instructions"
|
||||||
|
derivation_accounts = "derivation_accounts"
|
||||||
|
|
||||||
|
[[module.workflow.step]]
|
||||||
|
type = "sol-compile"
|
||||||
|
|
||||||
|
[module.workflow.step.inputs]
|
||||||
|
instructions = "instructions"
|
||||||
|
derivation_accounts = "derivation_accounts"
|
||||||
|
nonce_address = "nonce_address"
|
||||||
|
nonce_authority = "nonce_authority"
|
||||||
|
nonce_data = "nonce_data"
|
||||||
|
|
||||||
[module.workflow.step.outputs]
|
[module.workflow.step.outputs]
|
||||||
transaction = "unsigned_transaction"
|
transaction = "unsigned_transaction"
|
||||||
|
|
||||||
|
@ -67,7 +82,7 @@ type = "sol-sign"
|
||||||
|
|
||||||
[module.workflow.step.inputs]
|
[module.workflow.step.inputs]
|
||||||
transaction = "unsigned_transaction"
|
transaction = "unsigned_transaction"
|
||||||
blockhash = "blockhash"
|
blockhash = "nonce_data"
|
||||||
|
|
||||||
[module.workflow.step.outputs]
|
[module.workflow.step.outputs]
|
||||||
transaction = "signed_transaction"
|
transaction = "signed_transaction"
|
||||||
|
@ -84,19 +99,22 @@ values = { filename = "transaction.json" }
|
||||||
# contain the signed transaction.
|
# contain the signed transaction.
|
||||||
inputs = { transaction = "signed_transaction" }
|
inputs = { transaction = "signed_transaction" }
|
||||||
|
|
||||||
|
# NOTE: To get a nonce address, the `generate-nonce-account` workflow should be
|
||||||
|
# run. It is the only workflow that uses a blockhash, which is why a
|
||||||
|
# `broadcast-with-blockhash` or similar is not, and should not be, implemented.
|
||||||
[[module.workflow]]
|
[[module.workflow]]
|
||||||
name = "broadcast"
|
name = "broadcast"
|
||||||
inputs = ["cluster"]
|
inputs = ["nonce_address", "cluster"]
|
||||||
|
|
||||||
[[module.workflow.step]]
|
[[module.workflow.step]]
|
||||||
type = "sol-get-blockhash"
|
type = "sol-get-nonce-account-data"
|
||||||
inputs = { cluster = "cluster" }
|
inputs = { nonce_address = "nonce_address", cluster = "cluster" }
|
||||||
outputs = { blockhash = "blockhash" }
|
outputs = { authority = "nonce_authority", durable_nonce = "nonce" }
|
||||||
|
|
||||||
[[module.workflow.step]]
|
[[module.workflow.step]]
|
||||||
type = "internal-save-file"
|
type = "internal-save-file"
|
||||||
values = { filename = "blockhash.json" }
|
values = { filename = "nonce.json" }
|
||||||
inputs = { blockhash = "blockhash" }
|
inputs = { nonce_authority = "nonce_authority", nonce_data = "nonce", nonce_address = "nonce_address" }
|
||||||
|
|
||||||
[[module.workflow.step]]
|
[[module.workflow.step]]
|
||||||
type = "internal-load-file"
|
type = "internal-load-file"
|
||||||
|
@ -106,4 +124,99 @@ outputs = { transaction = "transaction" }
|
||||||
[[module.workflow.step]]
|
[[module.workflow.step]]
|
||||||
type = "sol-broadcast"
|
type = "sol-broadcast"
|
||||||
inputs = { cluster = "cluster", transaction = "transaction" }
|
inputs = { cluster = "cluster", transaction = "transaction" }
|
||||||
|
outputs = { status = "status", url = "url", error = "error" }
|
||||||
|
|
||||||
|
[[module.workflow]]
|
||||||
|
name = "generate-nonce-account"
|
||||||
|
inputs = ["cluster", "authorization_address"]
|
||||||
|
|
||||||
|
[[module.workflow.step]]
|
||||||
|
type = "sol-generate-wallet"
|
||||||
|
|
||||||
|
[[module.workflow.step]]
|
||||||
|
type = "sol-get-wallet-address"
|
||||||
|
outputs = { pubkey = "wallet_pubkey" }
|
||||||
|
|
||||||
|
[[module.workflow.step]]
|
||||||
|
type = "sol-await-funds"
|
||||||
|
inputs = { address = "wallet_pubkey", cluster = "cluster" }
|
||||||
|
# enough to cover two signatures and the 1_500_000 approx. rent fee
|
||||||
|
values = { lamports = "1510000" }
|
||||||
|
|
||||||
|
[[module.workflow.step]]
|
||||||
|
type = "sol-get-blockhash"
|
||||||
|
inputs = { cluster = "cluster" }
|
||||||
|
outputs = { blockhash = "blockhash" }
|
||||||
|
|
||||||
|
[[module.workflow.step]]
|
||||||
|
type = "sol-create-nonce-account-and-signing-key"
|
||||||
|
|
||||||
|
[module.workflow.step.inputs]
|
||||||
|
from_address = "wallet_pubkey"
|
||||||
|
authorization_address = "authorization_address"
|
||||||
|
|
||||||
|
[module.workflow.step.outputs]
|
||||||
|
transaction = "unsigned_transaction"
|
||||||
|
nonce_pubkey = "nonce_pubkey"
|
||||||
|
nonce_privkey = "private_keys"
|
||||||
|
|
||||||
|
[[module.workflow.step]]
|
||||||
|
type = "sol-sign"
|
||||||
|
|
||||||
|
[module.workflow.step.inputs]
|
||||||
|
blockhash = "blockhash"
|
||||||
|
signing_keys = "private_keys"
|
||||||
|
transaction = "unsigned_transaction"
|
||||||
|
|
||||||
|
[module.workflow.step.outputs]
|
||||||
|
transaction = "signed_transaction"
|
||||||
|
|
||||||
|
[[module.workflow.step]]
|
||||||
|
type = "sol-broadcast"
|
||||||
|
inputs = { cluster = "cluster", transaction = "signed_transaction" }
|
||||||
outputs = { status = "status", url = "url" }
|
outputs = { status = "status", url = "url" }
|
||||||
|
|
||||||
|
[[module.workflow.step]]
|
||||||
|
type = "internal-cat"
|
||||||
|
inputs = { status = "status", url = "url", nonce_account = "nonce_pubkey" }
|
||||||
|
outputs = { status = "status", url = "url", nonce_account = "nonce_account" }
|
||||||
|
|
||||||
|
[[module.workflow]]
|
||||||
|
# Transfer SOL from one address to another.
|
||||||
|
name = "transfer"
|
||||||
|
inputs = ["to_address", "from_address", "amount"]
|
||||||
|
|
||||||
|
[[module.workflow.step]]
|
||||||
|
type = "internal-load-file"
|
||||||
|
values = { filename = "nonce.json" }
|
||||||
|
outputs = { nonce_authority = "nonce_authority", nonce_data = "nonce_data", nonce_address = "nonce_address" }
|
||||||
|
|
||||||
|
[[module.workflow.step]]
|
||||||
|
type = "sol-transfer"
|
||||||
|
inputs = { from_address = "from_address", to_address = "to_address", amount = "amount" }
|
||||||
|
outputs = { instructions = "instructions", derivation_accounts = "derivation_accounts" }
|
||||||
|
|
||||||
|
[[module.workflow.step]]
|
||||||
|
type = "sol-compile"
|
||||||
|
|
||||||
|
[module.workflow.step.inputs]
|
||||||
|
instructions = "instructions"
|
||||||
|
derivation_accounts = "derivation_accounts"
|
||||||
|
nonce_address = "nonce_address"
|
||||||
|
nonce_authority = "nonce_authority"
|
||||||
|
nonce_data = "nonce_data"
|
||||||
|
|
||||||
|
[module.workflow.step.outputs]
|
||||||
|
transaction = "unsigned_transaction"
|
||||||
|
|
||||||
|
[[module.workflow.step]]
|
||||||
|
type = "sol-sign"
|
||||||
|
|
||||||
|
inputs = { blockhash = "nonce_data", transaction = "unsigned_transaction" }
|
||||||
|
outputs = { transaction = "signed_transaction" }
|
||||||
|
|
||||||
|
[[module.workflow.step]]
|
||||||
|
type = "internal-save-file"
|
||||||
|
|
||||||
|
values = { filename = "transaction.json" }
|
||||||
|
inputs = { transaction = "signed_transaction" }
|
||||||
|
|
Loading…
Reference in New Issue