diff --git a/Cargo.lock b/Cargo.lock index 23a4b0ed..c39f886a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -277,12 +277,14 @@ dependencies = [ "bs58", "candid", "getrandom 0.2.15", - "http 1.3.1", "ic-cdk", "ic-ed25519", "num 0.4.3", "serde", "serde_json", + "sol_rpc_client", + "sol_rpc_types", + "solana-account-decoder-client-types", "solana-hash", "solana-instruction", "solana-message", diff --git a/examples/basic_solana/Cargo.toml b/examples/basic_solana/Cargo.toml index c6a53b9f..b2fe143a 100644 --- a/examples/basic_solana/Cargo.toml +++ b/examples/basic_solana/Cargo.toml @@ -11,15 +11,17 @@ base64 = "0.22.1" bincode = { workspace = true } bs58 = { workspace = true } candid = { workspace = true } -# Transitive dependency of ic-ed25519 +# Transitive dependency # See https://forum.dfinity.org/t/module-imports-function-wbindgen-describe-from-wbindgen-placeholder-that-is-not-exported-by-the-runtime/11545/8 getrandom = { workspace = true, default-features = false, features = ["custom"] } ic-cdk = { workspace = true } ic-ed25519 = { workspace = true } -http = { workspace = true } num = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +sol_rpc_client = { path = "../../libs/client" } +sol_rpc_types = { path = "../../libs/types" } +solana-account-decoder-client-types = { workspace = true } solana-hash = { workspace = true } solana-instruction = { workspace = true } solana-message = { workspace = true } diff --git a/examples/basic_solana/README.md b/examples/basic_solana/README.md index 3eee6ab3..ef93469d 100644 --- a/examples/basic_solana/README.md +++ b/examples/basic_solana/README.md @@ -46,14 +46,14 @@ equivalent of "gas" on other blockchains). ### Deploy the smart contract to the Internet Computer ```bash -dfx deploy --ic basic_solana --argument '(record {solana_network = opt variant {Devnet}; ed25519_key_name = opt variant {TestKey1}})' +dfx deploy --ic basic_solana --argument (opt record { solana_network = opt variant {Devnet}; ed25519_key_name = opt variant {TestKey1}; sol_rpc_canister_id = null }) ``` #### What this does - `dfx deploy` tells the command line interface to `deploy` the smart contract - `--ic` tells the command line to deploy the smart contract to the mainnet ICP blockchain -- `--argument (opt record {solana_network = opt variant {Devnet}; ed25519_key_name = opt variant {TestKey1}})` +- `--argument (opt record { solana_network = opt variant {Devnet}; ed25519_key_name = opt variant {TestKey1}; sol_rpc_canister_id = null })` initializes the smart contract with the provided arguments: - `solana_network = opt variant {Devnet}`: the canister uses the [Solana Devnet](https://solana.com/docs/core/clusters) @@ -62,6 +62,9 @@ dfx deploy --ic basic_solana --argument '(record {solana_network = opt variant { available on the ICP mainnet. See [signing messages](https://internetcomputer.org/docs/current/developer-docs/smart-contracts/encryption/signing-messages#signing-messages-1) for more details. + - `sol_rpc_canister_id = null`: the canister makes RPC requests to the Solana network via the standard SOL-RPC canister on the ICP ( + canister ID: `tghme-zyaaa-aaaar-qarca-cai`). This can be replaced by the canister ID of another SOL-RPC canister, e.g. a + locally deployed one. If successful, you should see an output that looks like this: diff --git a/examples/basic_solana/basic_solana.did b/examples/basic_solana/basic_solana.did index a394bcbb..2fd8bf64 100644 --- a/examples/basic_solana/basic_solana.did +++ b/examples/basic_solana/basic_solana.did @@ -1,8 +1,13 @@ type InitArg = record { // The canister will interact with this Solana network. + // If not specified, the value is set to `Devnet`. solana_network : opt SolanaNetwork; // EdDSA keys will be derived from this key. + // If not specified, the value is set to `TestKeyLocalDevelopment`. ed25519_key_name : opt Ed25519KeyName; + // The canister will interact with this SOL RPC canister. + // If not specified, the value is set to `tghme-zyaaa-aaaar-qarca-cai`. + sol_rpc_canister_id : opt principal; }; type SolanaNetwork = variant { diff --git a/examples/basic_solana/src/lib.rs b/examples/basic_solana/src/lib.rs index 934a472a..836a175e 100644 --- a/examples/basic_solana/src/lib.rs +++ b/examples/basic_solana/src/lib.rs @@ -1,29 +1,30 @@ mod ed25519; -#[allow(deprecated)] -mod solana_rpc_canister; mod solana_wallet; mod spl; mod state; +use solana_nonce::{state::State, versions::Versions as NonceVersions}; use crate::{ - solana_rpc_canister::{transform_http_request, SolanaRpcCanister}, solana_wallet::SolanaWallet, state::{init_state, read_state}, }; +use base64::{prelude::BASE64_STANDARD, Engine}; use candid::{CandidType, Deserialize, Nat, Principal}; -use ic_cdk::{ - api::management_canister::http_request::{HttpResponse, TransformArgs}, - init, post_upgrade, query, update, -}; +use ic_cdk::{init, post_upgrade, update}; use num::{BigUint, ToPrimitive}; +use serde_json::json; +use sol_rpc_client::{IcRuntime, SolRpcClient}; +use sol_rpc_types::{ + GetAccountInfoEncoding, GetAccountInfoParams, MultiRpcResult, RpcSources, SolanaCluster, +}; +use solana_account_decoder_client_types::{UiAccountData, UiAccountEncoding}; +use solana_hash::Hash; use solana_message::Message; use solana_program::system_instruction; use solana_pubkey::Pubkey; use solana_transaction::Transaction; use std::{fmt::Display, str::FromStr}; -const SOL_RPC: SolanaRpcCanister = SolanaRpcCanister; - #[init] pub fn init(init_arg: InitArg) { init_state(init_arg) @@ -60,63 +61,87 @@ pub async fn associated_token_account(owner: Option, mint_account: St #[update] pub async fn get_balance(account: Option) -> Nat { - let solana_network = read_state(|s| s.solana_network()); - let max_response_size_bytes = 500_u64; - let num_cycles = 1_000_000_000u128; - let account = account.unwrap_or(solana_account(None).await); - let json = format!( - r#"{{ "jsonrpc": "2.0", "method": "getBalance", "params": ["{}"], "id": 1 }}"#, - account - ); - - let response = SOL_RPC - .json_rpc_request(solana_network, json, num_cycles, max_response_size_bytes) - .await; + // TODO XC-346: use `getBalance` method from client + let response = client() + .json_request(json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "getBalance", + "params": [ account ] + })) + .send() + .await + .expect_consistent() + .expect("Call to `getBalance` failed"); // The response to a successful `getBalance` call has the following format: // { "id": "[ID]", "jsonrpc": "2.0", "result": { "context": { "slot": [SLOT] } }, "value": [BALANCE] }, } - let balance = response["result"]["value"].as_u64().unwrap(); + let balance = serde_json::to_value(response) + .expect("`getBalance` response is not a valid JSON")["result"]["value"] + .as_u64() + .unwrap(); Nat(BigUint::from(balance)) } #[update] pub async fn get_nonce(account: Option) -> String { - let solana_network = read_state(|s| s.solana_network()); - let max_response_size_bytes = 500_u64; - let num_cycles = 1_000_000_000u128; - let account = account.unwrap_or(nonce_account(None).await); - let blockhash = SOL_RPC - .get_nonce_account_blockhash(solana_network, num_cycles, max_response_size_bytes, account) - .await; - - blockhash.to_string() + // Fetch the account info with the data encoded in base64 format + // TODO XC-347: use method from client to retrieve nonce + let mut params = GetAccountInfoParams::from_encoded_pubkey(account); + params.encoding = Some(GetAccountInfoEncoding::Base64); + let account_data = client() + .get_account_info(params) + .send() + .await + .expect_consistent() + .expect("Call to `getAccountInfo` failed") + .expect("Account not found for given pubkey") + .data; + + // Extract the nonce from the account data + let account_data = if let UiAccountData::Binary(blob, UiAccountEncoding::Base64) = account_data + { + BASE64_STANDARD + .decode(blob) + .expect("Unable to base64 decode account data") + } else { + panic!("Invalid response format"); + }; + match bincode::deserialize::(account_data.as_slice()) + .expect("Failed to deserialize nonce account data") + .state() + { + State::Uninitialized => panic!("Nonce account is uninitialized"), + State::Initialized(data) => data.blockhash().to_string(), + } } #[update] pub async fn get_spl_token_balance(account: Option, mint_account: String) -> String { - let solana_network = read_state(|s| s.solana_network()); - let max_response_size_bytes = 500_u64; - let num_cycles = 1_000_000_000u128; - let account = account.unwrap_or(associated_token_account(None, mint_account).await); - let json = format!( - r#"{{ "jsonrpc": "2.0", "method": "getTokenAccountBalance", "params": ["{}"], "id": 1 }}"#, - account - ); - - let response = SOL_RPC - .json_rpc_request(solana_network, json, num_cycles, max_response_size_bytes) - .await; + // TODO XC-325: use `getTokenAccountBalance` method from client + let response = client() + .json_request(json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "getTokenAccountBalance", + "params": [ account ] + })) + .send() + .await + .expect_consistent() + .expect("Call to `getTokenAccountBalance` failed"); // The response to a successful `getTokenAccountBalance` call has the following format: // { "id": "[ID]", "jsonrpc": "2.0", "result": { "context": { "slot": [SLOT] } }, "value": [ { "uiAmountString": "FORMATTED AMOUNT" } ] }, } - response["result"]["value"]["uiAmountString"] + serde_json::to_value(response).expect("`getTokenAccountBalance` response is not a valid JSON") + ["result"]["value"]["uiAmountString"] .as_str() .unwrap() .to_string() @@ -124,9 +149,7 @@ pub async fn get_spl_token_balance(account: Option, mint_account: String #[update] pub async fn create_nonce_account(owner: Option) -> String { - let solana_network = read_state(|s| s.solana_network()); - let max_response_size_bytes = 500_u64; - let num_cycles = 1_000_000_000u128; + let client = client(); let owner = owner.unwrap_or_else(validate_caller_not_anonymous); let wallet = SolanaWallet::new(owner).await; @@ -140,12 +163,12 @@ pub async fn create_nonce_account(owner: Option) -> String { payer.as_ref(), 1_500_000, ); - let blockhash = SOL_RPC - .get_latest_blockhash(solana_network, num_cycles, max_response_size_bytes) - .await; - let instruction = instructions.as_slice(); - let message = Message::new_with_blockhash(instruction, Some(payer.as_ref()), &blockhash); + let message = Message::new_with_blockhash( + instructions.as_slice(), + Some(payer.as_ref()), + &get_recent_blockhash(&client).await, + ); let signatures = vec![ wallet.sign_with_ed25519(&message, &payer).await, @@ -156,14 +179,13 @@ pub async fn create_nonce_account(owner: Option) -> String { signatures, }; - SOL_RPC - .send_transaction( - solana_network, - num_cycles, - max_response_size_bytes, - transaction, - ) + client + .send_transaction(transaction) + .send() .await + .expect_consistent() + .expect("Call to `sendTransaction` failed") + .to_string() } #[update] @@ -171,9 +193,7 @@ pub async fn create_associated_token_account( owner: Option, mint_account: String, ) -> String { - let solana_network = read_state(|s| s.solana_network()); - let max_response_size_bytes = 500_u64; - let num_cycles = 1_000_000_000u128; + let client = client(); let owner = owner.unwrap_or_else(validate_caller_not_anonymous); let wallet = SolanaWallet::new(owner).await; @@ -183,11 +203,12 @@ pub async fn create_associated_token_account( let instruction = spl::create_associated_token_account_instruction(payer.as_ref(), payer.as_ref(), &mint); - let blockhash = SOL_RPC - .get_latest_blockhash(solana_network, num_cycles, max_response_size_bytes) - .await; - let message = Message::new_with_blockhash(&[instruction], Some(payer.as_ref()), &blockhash); + let message = Message::new_with_blockhash( + &[instruction], + Some(payer.as_ref()), + &get_recent_blockhash(&client).await, + ); let signatures = vec![wallet.sign_with_ed25519(&message, &payer).await]; let transaction = Transaction { @@ -195,21 +216,18 @@ pub async fn create_associated_token_account( signatures, }; - SOL_RPC - .send_transaction( - solana_network, - num_cycles, - max_response_size_bytes, - transaction, - ) + client + .send_transaction(transaction) + .send() .await + .expect_consistent() + .expect("Call to `sendTransaction` failed") + .to_string() } #[update] pub async fn send_sol(owner: Option, to: String, amount: Nat) -> String { - let solana_network = read_state(|s| s.solana_network()); - let max_response_size_bytes = 500_u64; - let num_cycles = 1_000_000_000u128; + let client = client(); let owner = owner.unwrap_or_else(validate_caller_not_anonymous); let wallet = SolanaWallet::new(owner).await; @@ -219,25 +237,25 @@ pub async fn send_sol(owner: Option, to: String, amount: Nat) -> Stri let amount = amount.0.to_u64().unwrap(); let instruction = system_instruction::transfer(payer.as_ref(), &recipient, amount); - let blockhash = SOL_RPC - .get_latest_blockhash(solana_network, num_cycles, max_response_size_bytes) - .await; - let message = Message::new_with_blockhash(&[instruction], Some(payer.as_ref()), &blockhash); + let message = Message::new_with_blockhash( + &[instruction], + Some(payer.as_ref()), + &get_recent_blockhash(&client).await, + ); let signatures = vec![wallet.sign_with_ed25519(&message, &payer).await]; let transaction = Transaction { message, signatures, }; - SOL_RPC - .send_transaction( - solana_network, - num_cycles, - max_response_size_bytes, - transaction, - ) + client + .send_transaction(transaction) + .send() .await + .expect_consistent() + .expect("Call to `sendTransaction` failed") + .to_string() } #[update] @@ -246,9 +264,7 @@ pub async fn send_sol_with_durable_nonce( to: String, amount: Nat, ) -> String { - let solana_network = read_state(|s| s.solana_network()); - let max_response_size_bytes = 500_u64; - let num_cycles = 1_000_000_000u128; + let client = client(); let owner = owner.unwrap_or_else(validate_caller_not_anonymous); let wallet = SolanaWallet::new(owner).await; @@ -262,14 +278,9 @@ pub async fn send_sol_with_durable_nonce( system_instruction::advance_nonce_account(nonce_account.as_ref(), payer.as_ref()), system_instruction::transfer(payer.as_ref(), &recipient, amount), ]; - let blockhash = SOL_RPC - .get_nonce_account_blockhash( - solana_network, - num_cycles, - max_response_size_bytes, - nonce_account.to_string(), - ) - .await; + + let blockhash = Hash::from_str(&get_nonce(Some(nonce_account.to_string())).await) + .expect("Unable to parse nonce as blockhash"); let message = Message::new_with_blockhash(instructions, Some(payer.as_ref()), &blockhash); let signatures = vec![wallet.sign_with_ed25519(&message, &payer).await]; @@ -278,14 +289,13 @@ pub async fn send_sol_with_durable_nonce( signatures, }; - SOL_RPC - .send_transaction( - solana_network, - num_cycles, - max_response_size_bytes, - transaction, - ) + client + .send_transaction(transaction) + .send() .await + .expect_consistent() + .expect("Call to `sendTransaction` failed") + .to_string() } #[update] @@ -295,9 +305,7 @@ pub async fn send_spl_token( to: String, amount: Nat, ) -> String { - let solana_network = read_state(|s| s.solana_network()); - let max_response_size_bytes = 500_u64; - let num_cycles = 1_000_000_000u128; + let client = client(); let owner = owner.unwrap_or_else(validate_caller_not_anonymous); let wallet = SolanaWallet::new(owner).await; @@ -311,35 +319,83 @@ pub async fn send_spl_token( let to = spl::get_associated_token_address(&recipient, &mint); let instruction = spl::transfer_instruction(&from, &to, payer.as_ref(), amount); - let blockhash = SOL_RPC - .get_latest_blockhash(solana_network, num_cycles, max_response_size_bytes) - .await; - let message = Message::new_with_blockhash(&[instruction], Some(payer.as_ref()), &blockhash); + let message = Message::new_with_blockhash( + &[instruction], + Some(payer.as_ref()), + &get_recent_blockhash(&client).await, + ); let signatures = vec![wallet.sign_with_ed25519(&message, &payer).await]; let transaction = Transaction { message, signatures, }; - SOL_RPC - .send_transaction( - solana_network, - num_cycles, - max_response_size_bytes, - transaction, - ) + client + .send_transaction(transaction) + .send() .await + .expect_consistent() + .expect("Call to `sendTransaction` failed") + .to_string() +} + +// Fetch a recent blockhash using the Solana `getSlot` and `getBlock` methods. +// Since the `getSlot` method might fail due to Solana's fast blocktime, and some slots do not +// have blocks, we retry the RPC calls several times in case of failure to find a recent block. +async fn get_recent_blockhash(rpc_client: &SolRpcClient) -> Hash { + let num_tries = 3; + let mut errors = Vec::with_capacity(num_tries); + loop { + if errors.len() >= num_tries { + panic!("Failed to get recent block hash after {num_tries} tries: {errors:?}"); + } + match rpc_client.get_slot().send().await { + MultiRpcResult::Consistent(Ok(slot)) => match rpc_client.get_block(slot).send().await { + MultiRpcResult::Consistent(Ok(Some(block))) => { + return Hash::from_str(&block.blockhash).expect("Unable to parse blockhash") + } + MultiRpcResult::Consistent(Ok(None)) => { + errors.push(format!("No block for slot {slot}")); + continue; + } + MultiRpcResult::Inconsistent(results) => { + errors.push(format!( + "Inconsistent results for block with slot {slot}: {:?}", + results + )); + continue; + } + MultiRpcResult::Consistent(Err(e)) => { + errors.push(format!("Failed to get block with slot {slot}: {:?}", e)); + continue; + } + }, + MultiRpcResult::Inconsistent(results) => { + errors.push(format!("Failed to retrieved last slot: {:?}", results)); + continue; + } + MultiRpcResult::Consistent(Err(e)) => { + errors.push(format!("Failed to retrieve slot: {:?}", e)); + continue; + } + } + } } -// TODO: Remove! -#[query(name = "__transform_json_rpc", hidden = true)] -fn transform(args: TransformArgs) -> HttpResponse { - transform_http_request(args) +fn client() -> SolRpcClient { + read_state(|state| state.sol_rpc_canister_id()) + .map(|canister_id| SolRpcClient::builder(IcRuntime, canister_id)) + .unwrap_or(SolRpcClient::builder_for_ic()) + .with_rpc_sources(RpcSources::Default( + read_state(|state| state.solana_network()).into(), + )) + .build() } #[derive(CandidType, Deserialize, Debug, Default, PartialEq, Eq)] pub struct InitArg { + pub sol_rpc_canister_id: Option, pub solana_network: Option, pub ed25519_key_name: Option, } @@ -352,7 +408,17 @@ pub enum SolanaNetwork { Testnet, } -#[derive(CandidType, Deserialize, Debug, Default, PartialEq, Eq, Clone)] +impl From for SolanaCluster { + fn from(network: SolanaNetwork) -> Self { + match network { + SolanaNetwork::Mainnet => Self::Mainnet, + SolanaNetwork::Devnet => Self::Devnet, + SolanaNetwork::Testnet => Self::Testnet, + } + } +} + +#[derive(CandidType, Deserialize, Debug, Default, PartialEq, Eq, Clone, Copy)] pub enum Ed25519KeyName { #[default] TestKeyLocalDevelopment, diff --git a/examples/basic_solana/src/solana_rpc_canister.rs b/examples/basic_solana/src/solana_rpc_canister.rs deleted file mode 100644 index 49099626..00000000 --- a/examples/basic_solana/src/solana_rpc_canister.rs +++ /dev/null @@ -1,148 +0,0 @@ -use crate::SolanaNetwork; -use ic_cdk::api::management_canister::http_request::{ - CanisterHttpRequestArgument, HttpHeader, HttpMethod, HttpResponse, TransformArgs, - TransformContext, -}; -use serde_json::Value; -use solana_hash::Hash; -use solana_nonce::{state::State, versions::Versions as NonceVersions}; -use solana_transaction::Transaction; - -const CONTENT_TYPE_HEADER_LOWERCASE: &str = "content-type"; -const CONTENT_TYPE_VALUE: &str = "application/json"; - -pub struct SolanaRpcCanister; - -impl SolanaRpcCanister { - pub async fn get_nonce_account_blockhash( - &self, - solana_network: SolanaNetwork, - num_cycles: u128, - max_response_size_bytes: u64, - account: String, - ) -> Hash { - let json = format!( - r#"{{ "jsonrpc": "2.0", "method": "getAccountInfo", "params": ["{}", {{ "encoding": "base64" }}], "id": 1 }}"#, - account - ); - - let response = self - .json_rpc_request(solana_network, json, num_cycles, max_response_size_bytes) - .await; - - // The response to a successful `getAccountInfo` call has the following format: - // { "id": "[ID]", "jsonrpc": "2.0", "result": { ..., "value": { "data": [DATA, "base64"], ... } }, } - let account_data = response["result"]["value"]["data"].as_array().unwrap()[0] - .as_str() - .unwrap(); - - let account_data = bincode::deserialize::( - base64::decode(account_data) - .expect("Failed to decode account data") - .as_slice(), - ) - .expect("Failed to deserialize nonce account"); - - match account_data.state() { - State::Uninitialized => panic!("Nonce account is uninitialized"), - State::Initialized(data) => data.blockhash(), - } - } - - pub async fn get_latest_blockhash( - &self, - solana_network: SolanaNetwork, - num_cycles: u128, - max_response_size_bytes: u64, - ) -> Hash { - let json = r#"{ "jsonrpc": "2.0", "method": "getLatestBlockhash", "params": [], "id": 1 }"# - .to_string(); - let response = self - .json_rpc_request(solana_network, json, num_cycles, max_response_size_bytes) - .await; - // The response to a successful `getLatestBlockHash` call has the following format: - // { "id": "[ID]", "jsonrpc": "2.0", "result": { "context": { "slot": [SLOT] } }, "value": { "blockhash": [BLOCKHASH], "latestValidBlockHeight": [HEIGHT] }, } - response["result"]["value"]["blockhash"] - .as_str() - .expect("Failed to extract blockhash") - .to_string() - .parse() - .unwrap() - } - - pub async fn send_transaction( - &self, - solana_network: SolanaNetwork, - num_cycles: u128, - max_response_size_bytes: u64, - transaction: Transaction, - ) -> String { - let transaction = - bincode::serialize(&transaction).expect("Failed to serialize transaction"); - let json = format!( - r#"{{ "jsonrpc": "2.0", "method": "sendTransaction", "params": ["{}", {{ "encoding": "base64" }}], "id": 1 }}"#, - base64::encode(transaction) - ); - let response = self - .json_rpc_request(solana_network, json, num_cycles, max_response_size_bytes) - .await; - // The response to a successful `sendTransaction` call has the following format: - // { "id": "[ID]", "jsonrpc": "2.0", "result": [TXID], } - response["result"] - .as_str() - .unwrap_or_else(|| panic!("Failed to extract transaction ID: {:?}", response)) - .to_string() - } - - pub async fn json_rpc_request( - &self, - solana_network: SolanaNetwork, - json: String, - num_cycles: u128, - _max_response_size_bytes: u64, - ) -> Value { - use ic_cdk::api::management_canister::http_request::http_request; - let url = match solana_network { - SolanaNetwork::Devnet => "https://api.devnet.solana.com", - _ => panic!("Unsupported Solana network: {:?}", solana_network), - }; - let request = CanisterHttpRequestArgument { - url: url.to_string(), - max_response_bytes: None, - method: HttpMethod::POST, - headers: vec![HttpHeader { - name: CONTENT_TYPE_HEADER_LOWERCASE.to_string(), - value: CONTENT_TYPE_VALUE.to_string(), - }], - body: Some(json.as_bytes().to_vec()), - transform: Some(TransformContext::from_name( - "__transform_json_rpc".to_string(), - vec![], - )), - }; - match http_request(request, num_cycles).await { - Ok((response,)) => serde_json::from_str( - &String::from_utf8(response.body).expect("Failed to extract body"), - ) - .expect("Failed to parse JSON"), - Err((code, string)) => panic!( - "Received an error response with code {:?}: {:?}", - code, string - ), - } - } -} - -pub fn transform_http_request(args: TransformArgs) -> HttpResponse { - HttpResponse { - status: args.response.status, - body: canonicalize_json(&args.response.body).unwrap_or(args.response.body), - // Remove headers (which may contain a timestamp) for consensus - headers: vec![], - } -} - -fn canonicalize_json(text: &[u8]) -> Option> { - let json = serde_json::from_slice::(text).ok()?; - serde_json::to_vec(&json).ok() -} diff --git a/examples/basic_solana/src/state.rs b/examples/basic_solana/src/state.rs index 9c55a1b0..9b5f2c2f 100644 --- a/examples/basic_solana/src/state.rs +++ b/examples/basic_solana/src/state.rs @@ -2,6 +2,7 @@ use crate::{ ed25519::{get_ed25519_public_key, Ed25519ExtendedPublicKey}, Ed25519KeyName, InitArg, SolanaNetwork, }; +use candid::Principal; use std::{ cell::RefCell, ops::{Deref, DerefMut}, @@ -28,6 +29,7 @@ where #[derive(Debug, Default, PartialEq, Eq)] pub struct State { + sol_rpc_canister_id: Option, solana_network: SolanaNetwork, ed25519_public_key: Option, ed25519_key_name: Ed25519KeyName, @@ -35,17 +37,22 @@ pub struct State { impl State { pub fn ed25519_key_name(&self) -> Ed25519KeyName { - self.ed25519_key_name.clone() + self.ed25519_key_name } pub fn solana_network(&self) -> SolanaNetwork { self.solana_network } + + pub fn sol_rpc_canister_id(&self) -> Option { + self.sol_rpc_canister_id + } } impl From for State { fn from(init_arg: InitArg) -> Self { State { + sol_rpc_canister_id: init_arg.sol_rpc_canister_id, solana_network: init_arg.solana_network.unwrap_or_default(), ed25519_key_name: init_arg.ed25519_key_name.unwrap_or_default(), ..Default::default() diff --git a/libs/client/src/fixtures/mod.rs b/libs/client/src/fixtures/mod.rs index e1b5eb82..9889230e 100644 --- a/libs/client/src/fixtures/mod.rs +++ b/libs/client/src/fixtures/mod.rs @@ -4,8 +4,7 @@ use crate::{ClientBuilder, Runtime}; use async_trait::async_trait; -use candid::utils::ArgumentEncoder; -use candid::{CandidType, Principal}; +use candid::{utils::ArgumentEncoder, CandidType, Principal}; use ic_cdk::api::call::RejectionCode; use serde::de::DeserializeOwned; use sol_rpc_types::{AccountData, AccountEncoding, AccountInfo}; diff --git a/libs/types/src/solana/request/mod.rs b/libs/types/src/solana/request/mod.rs index c1c9618a..2ab8d79e 100644 --- a/libs/types/src/solana/request/mod.rs +++ b/libs/types/src/solana/request/mod.rs @@ -35,12 +35,11 @@ impl GetAccountInfoParams { && data_slice.is_none() && min_context_slot.is_none() } -} -impl From for GetAccountInfoParams { - fn from(pubkey: solana_pubkey::Pubkey) -> Self { + /// Parameters for a `getAccountInfo` request with the given pubkey already base58-encoded. + pub fn from_encoded_pubkey(pubkey: String) -> Self { Self { - pubkey: pubkey.to_string(), + pubkey, commitment: None, encoding: None, data_slice: None, @@ -49,6 +48,12 @@ impl From for GetAccountInfoParams { } } +impl From for GetAccountInfoParams { + fn from(pubkey: solana_pubkey::Pubkey) -> Self { + Self::from_encoded_pubkey(pubkey.to_string()) + } +} + /// Encoding for the return value of the Solana [`getAccountInfo`](https://solana.com/docs/rpc/http/getaccountinfo) RPC method. #[derive(Debug, Clone, Deserialize, Serialize, CandidType)] pub enum GetAccountInfoEncoding { @@ -233,7 +238,7 @@ pub struct SendTransactionParams { } impl SendTransactionParams { - /// Parameters for a `sendTransaction` request with the given transaction already encoded wit + /// Parameters for a `sendTransaction` request with the given transaction already encoded with /// the given encoding. pub fn from_encoded_transaction( transaction: String,