From c119ad788cd5c7e55df887b84da49a696a0d45e7 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 17 Apr 2025 11:20:31 +0200 Subject: [PATCH 01/14] XC-346: method getBalance --- canister/src/main.rs | 18 ++++++++---- canister/src/rpc_client/json/mod.rs | 39 ++++++++++++++++++++++++++ canister/src/rpc_client/mod.rs | 24 ++++++++++++++++ canister/src/rpc_client/sol_rpc/mod.rs | 9 ++++-- libs/types/src/lib.rs | 2 +- libs/types/src/solana/request/mod.rs | 11 ++++++++ 6 files changed, 94 insertions(+), 9 deletions(-) diff --git a/canister/src/main.rs b/canister/src/main.rs index 3b99391e..0a98cd10 100644 --- a/canister/src/main.rs +++ b/canister/src/main.rs @@ -11,12 +11,7 @@ use sol_rpc_canister::{ providers::{get_provider, PROVIDERS}, rpc_client::MultiRpcRequest, }; -use sol_rpc_types::{ - AccountInfo, ConfirmedBlock, GetAccountInfoParams, GetBlockParams, GetSlotParams, - GetSlotRpcConfig, GetTransactionParams, MultiRpcResult, RpcAccess, RpcConfig, RpcResult, - RpcSources, SendTransactionParams, Signature, Slot, SupportedRpcProvider, - SupportedRpcProviderId, TransactionInfo, -}; +use sol_rpc_types::{AccountInfo, ConfirmedBlock, GetAccountInfoParams, GetBalanceParams, GetBlockParams, GetSlotParams, GetSlotRpcConfig, GetTransactionParams, MultiRpcResult, RpcAccess, RpcConfig, RpcResult, RpcSources, SendTransactionParams, Signature, Slot, SupportedRpcProvider, SupportedRpcProviderId, TransactionInfo}; use std::str::FromStr; pub fn require_api_key_principal_or_controller() -> Result<(), String> { @@ -101,6 +96,17 @@ async fn get_account_info_cycles_cost( .await } +#[update(name = "getBalance")] +#[candid_method(rename = "getBalance")] +async fn get_balance( + source: RpcSources, + config: Option, + params: GetBalanceParams, +) -> MultiRpcResult { + let request = MultiRpcRequest::get_balance(source, config.unwrap_or_default(), params); + send_multi(request).await.into() +} + #[update(name = "getBlock")] #[candid_method(rename = "getBlock")] async fn get_block( diff --git a/canister/src/rpc_client/json/mod.rs b/canister/src/rpc_client/json/mod.rs index 3c7fa387..8d317ed6 100644 --- a/canister/src/rpc_client/json/mod.rs +++ b/canister/src/rpc_client/json/mod.rs @@ -77,6 +77,45 @@ pub struct GetAccountInfoConfig { pub min_context_slot: Option, } +#[skip_serializing_none] +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(into = "(String, Option)")] +pub struct GetBalanceParams(String, Option); + +#[skip_serializing_none] +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct GetBalanceConfig { + pub commitment: Option, + #[serde(rename = "minContextSlot")] + pub min_context_slot: Option, +} + +impl From for GetBalanceParams { + fn from( + sol_rpc_types::GetBalanceParams { + pubkey, + commitment, + min_context_slot, + }: sol_rpc_types::GetBalanceParams, + ) -> Self { + let config = if commitment.is_some() || min_context_slot.is_some() { + Some(GetBalanceConfig { + commitment, + min_context_slot, + }) + } else { + None + }; + GetBalanceParams(pubkey, config) + } +} + +impl From for (String, Option) { + fn from(value: GetBalanceParams) -> Self { + (value.0, value.1) + } +} + #[skip_serializing_none] #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(into = "(Slot, Option)")] diff --git a/canister/src/rpc_client/mod.rs b/canister/src/rpc_client/mod.rs index cb43288e..a525e8ba 100644 --- a/canister/src/rpc_client/mod.rs +++ b/canister/src/rpc_client/mod.rs @@ -110,6 +110,30 @@ impl GetAccountInfoRequest { } } +pub type GetBalanceRequest = MultiRpcRequest; + +impl GetBalanceRequest { + pub fn get_balance>( + rpc_sources: RpcSources, + config: RpcConfig, + params: Params, + ) -> Result { + let consensus_strategy = config.response_consensus.unwrap_or_default(); + let providers = Providers::new(rpc_sources, consensus_strategy.clone())?; + let max_response_bytes = config + .response_size_estimate + .unwrap_or(256 + HEADER_SIZE_LIMIT); + + Ok(MultiRpcRequest::new( + providers, + JsonRpcRequest::new("getBalance", params.into()), + max_response_bytes, + ResponseTransform::GetBalance, + ReductionStrategy::from(consensus_strategy), + )) + } +} + pub type GetBlockRequest = MultiRpcRequest< json::GetBlockParams, Option, diff --git a/canister/src/rpc_client/sol_rpc/mod.rs b/canister/src/rpc_client/sol_rpc/mod.rs index 4fac15fb..bda20e4a 100644 --- a/canister/src/rpc_client/sol_rpc/mod.rs +++ b/canister/src/rpc_client/sol_rpc/mod.rs @@ -22,9 +22,11 @@ pub enum ResponseTransform { #[n(0)] GetAccountInfo, #[n(1)] - GetBlock, + GetBalance, #[n(2)] - GetSlot(#[n(3)] RoundingError), + GetBlock, + #[n(3)] + GetSlot(#[n(0)] RoundingError), #[n(4)] GetTransaction, #[n(5)] @@ -56,6 +58,9 @@ impl ResponseTransform { } }); } + Self::GetBalance => { + todo!() + } Self::GetBlock => { canonicalize_response::>(body_bytes, |result| match result { Value::Null => None, diff --git a/libs/types/src/lib.rs b/libs/types/src/lib.rs index 1fe9500d..29f1fdea 100644 --- a/libs/types/src/lib.rs +++ b/libs/types/src/lib.rs @@ -20,7 +20,7 @@ pub use rpc_client::{ pub use solana::{ account::{AccountData, AccountEncoding, AccountInfo, ParsedAccount}, request::{ - CommitmentLevel, DataSlice, GetAccountInfoEncoding, GetAccountInfoParams, + CommitmentLevel, DataSlice, GetAccountInfoEncoding, GetAccountInfoParams, GetBalanceParams, GetBlockCommitmentLevel, GetBlockParams, GetSlotParams, GetTransactionEncoding, GetTransactionParams, SendTransactionEncoding, SendTransactionParams, TransactionDetails, }, diff --git a/libs/types/src/solana/request/mod.rs b/libs/types/src/solana/request/mod.rs index c1c9618a..d4affccb 100644 --- a/libs/types/src/solana/request/mod.rs +++ b/libs/types/src/solana/request/mod.rs @@ -78,6 +78,17 @@ pub struct DataSlice { pub offset: u32, } +#[derive(Debug, Clone, Default, Deserialize, Serialize, CandidType)] +pub struct GetBalanceParams { + /// The public key of the account to query formatted as a base-58 string. + pub pubkey: Pubkey, + /// The request returns the slot that has reached this or the default commitment level. + pub commitment: Option, + /// The minimum slot that the request can be evaluated at. + #[serde(rename = "minContextSlot")] + pub min_context_slot: Option, +} + /// The parameters for a Solana [`getBlock`](https://solana.com/docs/rpc/http/getblock) RPC method call. // TODO XC-342: Add `rewards`, `encoding` and `transactionDetails` fields. #[derive(Debug, Clone, Default, Deserialize, Serialize, CandidType)] From 5661ed93b507a93ba7a124112bcde36afca1b160 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 17 Apr 2025 11:37:30 +0200 Subject: [PATCH 02/14] XC-346: Candid interface --- canister/sol_rpc_canister.did | 26 ++++++++++++++++++++++++++ canister/src/main.rs | 9 +++++++-- canister/src/rpc_client/mod.rs | 6 +++--- libs/types/src/lib.rs | 2 +- libs/types/src/solana/mod.rs | 3 +++ 5 files changed, 40 insertions(+), 6 deletions(-) diff --git a/canister/sol_rpc_canister.did b/canister/sol_rpc_canister.did index 0f6272f4..c66b2cdb 100644 --- a/canister/sol_rpc_canister.did +++ b/canister/sol_rpc_canister.did @@ -295,6 +295,29 @@ type MultiGetAccountInfoResult = variant { Inconsistent : vec record { RpcSource; GetAccountInfoResult }; }; +// The parameters for a Solana `getBalance` RPC method call. +type GetBalanceParams = record { + // Pubkey of account to query, as base-58 encoded string. + pubkey: Pubkey; + // The commitment describes how finalized a block is at that point in time. + commitment: opt CommitmentLevel; + // The minimum slot that the request can be evaluated at. + minContextSlot: opt nat64; +}; + +// Smallest denomination of SOL, the native token on Solana, i.e. +// 1_000_000_000 Lamports is 1 SOL +type Lamport = nat64; + +// Represents an aggregated result from multiple RPC calls to the `getBalance` Solana RPC method. +type MultiGetBalanceResult = variant { + Consistent : GetBalanceResult; + Inconsistent : vec record { RpcSource; GetBalanceResult }; +}; + +// Represents the result of a call to the `getBalance` Solana RPC method. +type GetBalanceResult = variant { Ok : Lamport; Err : RpcError }; + // The parameters for a Solana `getBlock` RPC method call. // TODO XC-342: Add `rewards`, `encoding` and `transactionDetails` fields. type GetBlockParams = record { @@ -613,6 +636,9 @@ service : (InstallArgs,) -> { getAccountInfo : (RpcSources, opt RpcConfig, GetAccountInfoParams) -> (MultiGetAccountInfoResult); getAccountInfoCyclesCost : (RpcSources, opt RpcConfig, GetAccountInfoParams) -> (RequestCostResult) query; + // Call the Solana `getBalance` RPC method and return the resulting block. + getBalance : (RpcSources, opt RpcConfig, GetBalanceParams) -> (MultiGetBalanceResult); + // Call the Solana `getBlock` RPC method and return the resulting block. getBlock : (RpcSources, opt RpcConfig, GetBlockParams) -> (MultiGetBlockResult); getBlockCyclesCost : (RpcSources, opt RpcConfig, GetBlockParams) -> (RequestCostResult) query; diff --git a/canister/src/main.rs b/canister/src/main.rs index 0a98cd10..dcb037b8 100644 --- a/canister/src/main.rs +++ b/canister/src/main.rs @@ -11,7 +11,12 @@ use sol_rpc_canister::{ providers::{get_provider, PROVIDERS}, rpc_client::MultiRpcRequest, }; -use sol_rpc_types::{AccountInfo, ConfirmedBlock, GetAccountInfoParams, GetBalanceParams, GetBlockParams, GetSlotParams, GetSlotRpcConfig, GetTransactionParams, MultiRpcResult, RpcAccess, RpcConfig, RpcResult, RpcSources, SendTransactionParams, Signature, Slot, SupportedRpcProvider, SupportedRpcProviderId, TransactionInfo}; +use sol_rpc_types::{ + AccountInfo, ConfirmedBlock, GetAccountInfoParams, GetBalanceParams, GetBlockParams, + GetSlotParams, GetSlotRpcConfig, GetTransactionParams, Lamport, MultiRpcResult, RpcAccess, + RpcConfig, RpcResult, RpcSources, SendTransactionParams, Signature, Slot, SupportedRpcProvider, + SupportedRpcProviderId, TransactionInfo, +}; use std::str::FromStr; pub fn require_api_key_principal_or_controller() -> Result<(), String> { @@ -102,7 +107,7 @@ async fn get_balance( source: RpcSources, config: Option, params: GetBalanceParams, -) -> MultiRpcResult { +) -> MultiRpcResult { let request = MultiRpcRequest::get_balance(source, config.unwrap_or_default(), params); send_multi(request).await.into() } diff --git a/canister/src/rpc_client/mod.rs b/canister/src/rpc_client/mod.rs index a525e8ba..8ef9d43e 100644 --- a/canister/src/rpc_client/mod.rs +++ b/canister/src/rpc_client/mod.rs @@ -25,8 +25,8 @@ use ic_cdk::api::management_canister::http_request::{ }; use serde::{de::DeserializeOwned, Serialize}; use sol_rpc_types::{ - ConsensusStrategy, GetSlotRpcConfig, ProviderError, RpcConfig, RpcError, RpcResult, RpcSource, - RpcSources, Signature, TransactionDetails, + ConsensusStrategy, GetSlotRpcConfig, Lamport, ProviderError, RpcConfig, RpcError, RpcResult, + RpcSource, RpcSources, Signature, TransactionDetails, }; use solana_clock::Slot; use std::{fmt::Debug, marker::PhantomData}; @@ -110,7 +110,7 @@ impl GetAccountInfoRequest { } } -pub type GetBalanceRequest = MultiRpcRequest; +pub type GetBalanceRequest = MultiRpcRequest; impl GetBalanceRequest { pub fn get_balance>( diff --git a/libs/types/src/lib.rs b/libs/types/src/lib.rs index 29f1fdea..78fc5814 100644 --- a/libs/types/src/lib.rs +++ b/libs/types/src/lib.rs @@ -31,5 +31,5 @@ pub use solana::{ EncodedTransaction, LoadedAddresses, TransactionBinaryEncoding, TransactionInfo, TransactionReturnData, TransactionStatusMeta, TransactionTokenBalance, TransactionVersion, }, - Blockhash, ConfirmedBlock, Pubkey, Signature, Slot, Timestamp, + Blockhash, ConfirmedBlock, Lamport, Pubkey, Signature, Slot, Timestamp, }; diff --git a/libs/types/src/solana/mod.rs b/libs/types/src/solana/mod.rs index 386f1f1e..95aa9a78 100644 --- a/libs/types/src/solana/mod.rs +++ b/libs/types/src/solana/mod.rs @@ -9,6 +9,9 @@ use std::fmt::Debug; /// A Solana [slot](https://solana.com/docs/references/terminology#slot). pub type Slot = u64; +/// A Solana [Lamport](https://solana.com/de/docs/references/terminology#lamport). +pub type Lamport = u64; + /// A Solana base58-encoded [blockhash](https://solana.com/de/docs/references/terminology#blockhash). pub type Blockhash = String; From d961be634d5aa8bc5a51e654ce7d1db007d32c92 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 17 Apr 2025 11:45:03 +0200 Subject: [PATCH 03/14] XC-346: serialization test --- canister/src/rpc_client/tests.rs | 37 ++++++++++++++++++++++++++++ libs/types/src/solana/request/mod.rs | 10 ++++++++ 2 files changed, 47 insertions(+) diff --git a/canister/src/rpc_client/tests.rs b/canister/src/rpc_client/tests.rs index 14e98a44..c956d117 100644 --- a/canister/src/rpc_client/tests.rs +++ b/canister/src/rpc_client/tests.rs @@ -13,6 +13,7 @@ use sol_rpc_types::{ mod request_serialization_tests { use super::*; + use sol_rpc_types::GetBalanceParams; #[test] fn should_serialize_get_account_info_request() { @@ -117,6 +118,42 @@ mod request_serialization_tests { ); } + #[test] + fn should_serialize_get_balance_request() { + let pubkey = solana_pubkey::Pubkey::default(); + assert_serialized( + MultiRpcRequest::get_balance( + RpcSources::Default(SolanaCluster::Mainnet), + RpcConfig::default(), + GetBalanceParams::from(pubkey), + ) + .unwrap(), + json!([pubkey.to_string(), null]), + ); + + assert_serialized( + MultiRpcRequest::get_balance( + RpcSources::Default(SolanaCluster::Mainnet), + RpcConfig::default(), + GetBalanceParams { + pubkey: pubkey.to_string(), + commitment: Some(CommitmentLevel::Confirmed), + min_context_slot: Some(42), + }, + ) + .unwrap(), + json!( + [ + pubkey.to_string(), + { + "commitment": "confirmed", + "minContextSlot": 42 + } + ] + ), + ); + } + #[test] fn should_serialize_get_block_request() { assert_serialized( diff --git a/libs/types/src/solana/request/mod.rs b/libs/types/src/solana/request/mod.rs index d4affccb..f5369e1c 100644 --- a/libs/types/src/solana/request/mod.rs +++ b/libs/types/src/solana/request/mod.rs @@ -89,6 +89,16 @@ pub struct GetBalanceParams { pub min_context_slot: Option, } +impl From for GetBalanceParams { + fn from(pubkey: solana_pubkey::Pubkey) -> Self { + Self { + pubkey: pubkey.to_string(), + commitment: None, + min_context_slot: None, + } + } +} + /// The parameters for a Solana [`getBlock`](https://solana.com/docs/rpc/http/getblock) RPC method call. // TODO XC-342: Add `rewards`, `encoding` and `transactionDetails` fields. #[derive(Debug, Clone, Default, Deserialize, Serialize, CandidType)] From 30e054bdfa9d1cdf159da0d6d7e22414ed107db2 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 17 Apr 2025 13:00:49 +0200 Subject: [PATCH 04/14] XC-346: normalize response --- canister/src/rpc_client/sol_rpc/mod.rs | 2 +- canister/src/rpc_client/sol_rpc/tests.rs | 35 +++++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/canister/src/rpc_client/sol_rpc/mod.rs b/canister/src/rpc_client/sol_rpc/mod.rs index bda20e4a..d2c1e4fc 100644 --- a/canister/src/rpc_client/sol_rpc/mod.rs +++ b/canister/src/rpc_client/sol_rpc/mod.rs @@ -59,7 +59,7 @@ impl ResponseTransform { }); } Self::GetBalance => { - todo!() + canonicalize_response::(body_bytes, |result| result["value"].clone()); } Self::GetBlock => { canonicalize_response::>(body_bytes, |result| match result { diff --git a/canister/src/rpc_client/sol_rpc/tests.rs b/canister/src/rpc_client/sol_rpc/tests.rs index 9ba01f40..07dae7cc 100644 --- a/canister/src/rpc_client/sol_rpc/tests.rs +++ b/canister/src/rpc_client/sol_rpc/tests.rs @@ -238,6 +238,39 @@ mod normalization_tests { assert_normalized(&ResponseTransform::GetTransaction, "null", Value::Null); } + #[test] + fn should_normalize_get_balance_response() { + assert_normalized( + &ResponseTransform::GetAccountInfo, + r#"{ "context": { "slot": 334035824, "apiVersion": "2.1.9" }, "value": 0 }"#, + json!(0), + ); + + assert_normalized( + &ResponseTransform::GetAccountInfo, + r#"{ "context": { "slot": 334035824, "apiVersion": "2.1.9" }, "value": 1000000 }"#, + json!(1000000), + ); + + assert_normalized_equal( + &ResponseTransform::GetBalance, + r#"{ + "context": { + "slot": 334036571, + "apiVersion": "2.1.9" + }, + "value": 1000000 + }"#, + r#"{ + "context": { + "slot": 334036572, + "apiVersion": "2.1.9" + }, + "value": 1000000 + }"#, + ); + } + fn assert_normalized(transform: &ResponseTransform, result: &str, expected: Value) { let expected_response = to_vec(&JsonRpcResponse::from_ok(Id::Number(1), expected)).unwrap(); let normalized_response = normalize_result(transform, result); @@ -246,7 +279,7 @@ mod normalization_tests { normalized_response, "expected {:?}, actual: {:?}", from_slice::(&expected_response), - expected_response, + from_slice::(&normalized_response), ); } From 25f6ddf2208350c9fee10b3fd26ad1ceab3c29c5 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 17 Apr 2025 13:01:33 +0200 Subject: [PATCH 05/14] XC-346: clippy --- canister/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/canister/src/main.rs b/canister/src/main.rs index dcb037b8..3888fc83 100644 --- a/canister/src/main.rs +++ b/canister/src/main.rs @@ -109,7 +109,7 @@ async fn get_balance( params: GetBalanceParams, ) -> MultiRpcResult { let request = MultiRpcRequest::get_balance(source, config.unwrap_or_default(), params); - send_multi(request).await.into() + send_multi(request).await } #[update(name = "getBlock")] From 0148b2f60af127dfda43022ff6e9965f2fb467d9 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 17 Apr 2025 13:11:29 +0200 Subject: [PATCH 06/14] XC-346: request cost --- canister/sol_rpc_canister.did | 1 + canister/src/main.rs | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/canister/sol_rpc_canister.did b/canister/sol_rpc_canister.did index c66b2cdb..7f8c8295 100644 --- a/canister/sol_rpc_canister.did +++ b/canister/sol_rpc_canister.did @@ -638,6 +638,7 @@ service : (InstallArgs,) -> { // Call the Solana `getBalance` RPC method and return the resulting block. getBalance : (RpcSources, opt RpcConfig, GetBalanceParams) -> (MultiGetBalanceResult); + getBalanceCyclesCost : (RpcSources, opt RpcConfig, GetBalanceParams) -> (RequestCostResult) query; // Call the Solana `getBlock` RPC method and return the resulting block. getBlock : (RpcSources, opt RpcConfig, GetBlockParams) -> (MultiGetBlockResult); diff --git a/canister/src/main.rs b/canister/src/main.rs index 3888fc83..90fd2500 100644 --- a/canister/src/main.rs +++ b/canister/src/main.rs @@ -112,6 +112,21 @@ async fn get_balance( send_multi(request).await } +#[query(name = "getBalanceCyclesCost")] +#[candid_method(query, rename = "getBalanceCyclesCost")] +async fn get_balance_cycles_cost( + source: RpcSources, + config: Option, + params: GetBalanceParams, +) -> RpcResult { + if read_state(State::is_demo_mode_active) { + return Ok(0); + } + MultiRpcRequest::get_balance(source, config.unwrap_or_default(), params)? + .cycles_cost() + .await +} + #[update(name = "getBlock")] #[candid_method(rename = "getBlock")] async fn get_block( From 87c4c33d439ac293f054353bfa53998056e689d2 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 17 Apr 2025 13:25:43 +0200 Subject: [PATCH 07/14] XC-346: client --- libs/client/src/lib.rs | 55 ++++++++++++++++++++++++++++++---- libs/client/src/request/mod.rs | 34 +++++++++++++++++++-- 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index bf987fb5..d67b7951 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -126,17 +126,18 @@ pub use request::{Request, RequestBuilder, SolRpcEndpoint, SolRpcRequest}; use std::fmt::Debug; use crate::request::{ - GetAccountInfoRequest, GetBlockRequest, GetSlotRequest, GetTransactionRequest, JsonRequest, - SendTransactionRequest, + GetAccountInfoRequest, GetBalanceRequest, GetBlockRequest, GetSlotRequest, + GetTransactionRequest, JsonRequest, SendTransactionRequest, }; use async_trait::async_trait; use candid::{utils::ArgumentEncoder, CandidType, Principal}; use ic_cdk::api::call::RejectionCode; use serde::de::DeserializeOwned; use sol_rpc_types::{ - GetAccountInfoParams, GetBlockParams, GetSlotParams, GetSlotRpcConfig, GetTransactionParams, - RpcConfig, RpcSources, SendTransactionParams, Signature, SolanaCluster, SupportedRpcProvider, - SupportedRpcProviderId, TransactionDetails, TransactionInfo, + GetAccountInfoParams, GetBalanceParams, GetBlockParams, GetSlotParams, GetSlotRpcConfig, + GetTransactionParams, Lamport, RpcConfig, RpcSources, SendTransactionParams, Signature, + SolanaCluster, SupportedRpcProvider, SupportedRpcProviderId, TransactionDetails, + TransactionInfo, }; use solana_clock::Slot; use solana_transaction_status_client_types::EncodedConfirmedTransactionWithStatusMeta; @@ -322,6 +323,50 @@ impl SolRpcClient { ) } + /// Call `getBalance` on the SOL RPC canister. + /// + /// # Examples + /// + /// ```rust + /// use sol_rpc_client::SolRpcClient; + /// use sol_rpc_types::{RpcSources, SolanaCluster}; + /// use solana_pubkey::pubkey; + /// + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// # use sol_rpc_types::MultiRpcResult; + /// let client = SolRpcClient::builder_for_ic() + /// # .with_mocked_response(MultiRpcResult::Consistent(Ok(389_086_612_571_u64))) + /// .with_rpc_sources(RpcSources::Default(SolanaCluster::Mainnet)) + /// .build(); + /// + /// let balance = client + /// .get_balance(pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")) + /// .send() + /// .await + /// .expect_consistent(); + /// + /// assert_eq!(balance, Ok(389_086_612_571_u64)); + /// # Ok(()) + /// # } + /// ``` + pub fn get_balance( + &self, + params: impl Into, + ) -> RequestBuilder< + R, + RpcConfig, + GetBalanceParams, + sol_rpc_types::MultiRpcResult, + sol_rpc_types::MultiRpcResult, + > { + RequestBuilder::new( + self.clone(), + GetBalanceRequest::new(params.into()), + 1_000_000_000, + ) + } + /// Call `getBlock` on the SOL RPC canister. pub fn get_block( &self, diff --git a/libs/client/src/request/mod.rs b/libs/client/src/request/mod.rs index 9c50493f..b3d53c98 100644 --- a/libs/client/src/request/mod.rs +++ b/libs/client/src/request/mod.rs @@ -2,9 +2,9 @@ use crate::{Runtime, SolRpcClient}; use candid::CandidType; use serde::de::DeserializeOwned; use sol_rpc_types::{ - AccountInfo, ConfirmedBlock, GetAccountInfoParams, GetBlockParams, GetSlotParams, - GetSlotRpcConfig, GetTransactionParams, RpcConfig, RpcResult, RpcSources, - SendTransactionParams, Signature, TransactionInfo, + AccountInfo, ConfirmedBlock, GetAccountInfoParams, GetBalanceParams, GetBlockParams, + GetSlotParams, GetSlotRpcConfig, GetTransactionParams, Lamport, RpcConfig, RpcResult, + RpcSources, SendTransactionParams, Signature, TransactionInfo, }; use solana_clock::Slot; use solana_transaction_status_client_types::EncodedConfirmedTransactionWithStatusMeta; @@ -33,6 +33,8 @@ pub trait SolRpcRequest { pub enum SolRpcEndpoint { /// `getAccountInfo` endpoint. GetAccountInfo, + /// `getBalance` endpoint. + GetBalance, /// `getBlock` endpoint. GetBlock, /// `getSlot` endpoint. @@ -50,6 +52,7 @@ impl SolRpcEndpoint { pub fn rpc_method(&self) -> &'static str { match &self { SolRpcEndpoint::GetAccountInfo => "getAccountInfo", + SolRpcEndpoint::GetBalance => "getBalance", SolRpcEndpoint::GetBlock => "getBlock", SolRpcEndpoint::GetSlot => "getSlot", SolRpcEndpoint::GetTransaction => "getTransaction", @@ -62,6 +65,7 @@ impl SolRpcEndpoint { pub fn cycles_cost_method(&self) -> &'static str { match &self { SolRpcEndpoint::GetAccountInfo => "getAccountInfoCyclesCost", + SolRpcEndpoint::GetBalance => "getBalanceCyclesCost", SolRpcEndpoint::GetBlock => "getBlockCyclesCost", SolRpcEndpoint::GetSlot => "getSlotCyclesCost", SolRpcEndpoint::GetTransaction => "getTransactionCyclesCost", @@ -96,6 +100,30 @@ impl SolRpcRequest for GetAccountInfoRequest { } } +#[derive(Debug, Clone)] +pub struct GetBalanceRequest(GetBalanceParams); + +impl GetBalanceRequest { + pub fn new(params: GetBalanceParams) -> Self { + Self(params) + } +} + +impl SolRpcRequest for GetBalanceRequest { + type Config = RpcConfig; + type Params = GetBalanceParams; + type CandidOutput = sol_rpc_types::MultiRpcResult; + type Output = sol_rpc_types::MultiRpcResult; + + fn endpoint(&self) -> SolRpcEndpoint { + SolRpcEndpoint::GetBalance + } + + fn params(self) -> Self::Params { + self.0 + } +} + #[derive(Debug, Clone)] pub struct GetBlockRequest(GetBlockParams); From 6841ba23df1404bfabc8a52b3ceac3e7d54170c6 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 17 Apr 2025 13:31:46 +0200 Subject: [PATCH 08/14] XC-346: test cycles cost --- integration_tests/tests/tests.rs | 9 +++++++++ libs/client/src/lib.rs | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/integration_tests/tests/tests.rs b/integration_tests/tests/tests.rs index d9619874..f7bc37cb 100644 --- a/integration_tests/tests/tests.rs +++ b/integration_tests/tests/tests.rs @@ -772,6 +772,9 @@ mod cycles_cost_tests { SolRpcEndpoint::GetAccountInfo => { check(client.get_account_info(some_pubkey())).await; } + SolRpcEndpoint::GetBalance => { + check(client.get_balance(some_pubkey())).await; + } SolRpcEndpoint::GetBlock => { check(client.get_block(577996)).await; } @@ -816,6 +819,9 @@ mod cycles_cost_tests { SolRpcEndpoint::GetAccountInfo => { check(client.get_account_info(some_pubkey())).await; } + SolRpcEndpoint::GetBalance => { + check(client.get_balance(some_pubkey())).await; + } SolRpcEndpoint::GetBlock => { check(client.get_block(577996)).await; } @@ -918,6 +924,9 @@ mod cycles_cost_tests { ) .await; } + SolRpcEndpoint::GetBalance => { + check(&setup, client.get_balance(some_pubkey()), 1_731_769_600).await; + } SolRpcEndpoint::GetBlock => { check(&setup, client.get_block(577996), 1_791_868_000).await; } diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index d67b7951..7b1edd4f 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -363,7 +363,7 @@ impl SolRpcClient { RequestBuilder::new( self.clone(), GetBalanceRequest::new(params.into()), - 1_000_000_000, + 10_000_000_000, ) } From 78ec49641c47771e00f67b1c44c76c900ba4b1bd Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 17 Apr 2025 15:02:40 +0200 Subject: [PATCH 09/14] XC-346: integration test --- .../tests/solana_test_validator.rs | 52 ++++++++++++++++--- libs/client/src/request/mod.rs | 9 ++++ 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/integration_tests/tests/solana_test_validator.rs b/integration_tests/tests/solana_test_validator.rs index 2a48dfc5..a741879a 100644 --- a/integration_tests/tests/solana_test_validator.rs +++ b/integration_tests/tests/solana_test_validator.rs @@ -9,7 +9,7 @@ use sol_rpc_int_tests::PocketIcLiveModeRuntime; use sol_rpc_types::{ CommitmentLevel, GetAccountInfoEncoding, GetAccountInfoParams, GetBlockCommitmentLevel, GetBlockParams, GetSlotParams, GetTransactionEncoding, GetTransactionParams, InstallArgs, - OverrideProvider, RegexSubstitution, SendTransactionParams, TransactionDetails, + Lamport, OverrideProvider, RegexSubstitution, SendTransactionParams, TransactionDetails, }; use solana_account_decoder_client_types::UiAccount; use solana_client::rpc_client::{RpcClient as SolanaRpcClient, RpcClient}; @@ -141,7 +141,7 @@ async fn should_get_block() { max_supported_transaction_version: None, }, ) - .expect("Failed to get block") + .expect("Failed to get block") }, |ic| async move { ic.get_block(GetBlockParams { @@ -150,11 +150,11 @@ async fn should_get_block() { max_supported_transaction_version: None, transaction_details: Some(TransactionDetails::Signatures), }) - .send() - .await - .expect_consistent() - .unwrap_or_else(|e| panic!("`getBlock` call failed: {e}")) - .unwrap_or_else(|| panic!("No block for slot {slot}")) + .send() + .await + .expect_consistent() + .unwrap_or_else(|e| panic!("`getBlock` call failed: {e}")) + .unwrap_or_else(|| panic!("No block for slot {slot}")) }, ) .await; @@ -271,6 +271,44 @@ async fn should_send_transaction() { setup.setup.drop().await; } +#[tokio::test(flavor = "multi_thread")] +async fn should_get_balance() { + async fn compare_balances(setup: &Setup, account: Pubkey) -> Lamport { + let pubkey = account; + let (sol_res, ic_res) = setup + .compare_client( + |sol| sol.get_balance(&account).expect("Failed to get balance"), + |ic| async move { + ic.get_balance(pubkey) + .modify_params(|params| { + params.commitment = Some(CommitmentLevel::Confirmed) + }) + .send() + .await + .expect_consistent() + .expect("Failed to get balance from SOL RPC") + }, + ) + .await; + assert_eq!(sol_res, ic_res); + sol_res + } + + let setup = Setup::new().await; + let user = Keypair::new(); + let publickey = user.pubkey(); + + assert_eq!(compare_balances(&setup, publickey).await, 0); + + let tx = setup + .solana_client + .request_airdrop(&user.pubkey(), 10_000_000_000) + .expect("Error while requesting airdrop"); + setup.confirm_transaction(&tx); + + assert_eq!(compare_balances(&setup, publickey).await, 10_000_000_000); +} + fn solana_rpc_client_get_account( pubkey: &Pubkey, sol: &RpcClient, diff --git a/libs/client/src/request/mod.rs b/libs/client/src/request/mod.rs index b3d53c98..5f7dc5ad 100644 --- a/libs/client/src/request/mod.rs +++ b/libs/client/src/request/mod.rs @@ -320,6 +320,15 @@ impl self } + /// Modify current parameters to send for that request. + pub fn modify_params(mut self, mutator: F) -> Self + where + F: FnOnce(&mut Params), + { + mutator(self.request.params_mut()); + self + } + /// Change the RPC configuration to use for that request. pub fn with_rpc_config(mut self, rpc_config: impl Into>) -> Self { *self.request.rpc_config_mut() = rpc_config.into(); From 93ed97ef80e8ba5f52a5d1404618a62e63f90a00 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 17 Apr 2025 15:08:49 +0200 Subject: [PATCH 10/14] XC-346: use getBalance in basic_solana --- examples/basic_solana/src/lib.rs | 23 +++++------------------ libs/types/src/solana/request/mod.rs | 1 + 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/examples/basic_solana/src/lib.rs b/examples/basic_solana/src/lib.rs index 836a175e..4fba7e84 100644 --- a/examples/basic_solana/src/lib.rs +++ b/examples/basic_solana/src/lib.rs @@ -11,7 +11,7 @@ use crate::{ use base64::{prelude::BASE64_STANDARD, Engine}; use candid::{CandidType, Deserialize, Nat, Principal}; use ic_cdk::{init, post_upgrade, update}; -use num::{BigUint, ToPrimitive}; +use num::ToPrimitive; use serde_json::json; use sol_rpc_client::{IcRuntime, SolRpcClient}; use sol_rpc_types::{ @@ -62,28 +62,15 @@ pub async fn associated_token_account(owner: Option, mint_account: St #[update] pub async fn get_balance(account: Option) -> Nat { let account = account.unwrap_or(solana_account(None).await); - - // TODO XC-346: use `getBalance` method from client - let response = client() - .json_request(json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "getBalance", - "params": [ account ] - })) + let public_key = Pubkey::from_str(&account).unwrap(); + let balance = client() + .get_balance(public_key) .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 = serde_json::to_value(response) - .expect("`getBalance` response is not a valid JSON")["result"]["value"] - .as_u64() - .unwrap(); - - Nat(BigUint::from(balance)) + Nat::from(balance) } #[update] diff --git a/libs/types/src/solana/request/mod.rs b/libs/types/src/solana/request/mod.rs index f7996cdf..80ce9a53 100644 --- a/libs/types/src/solana/request/mod.rs +++ b/libs/types/src/solana/request/mod.rs @@ -83,6 +83,7 @@ pub struct DataSlice { pub offset: u32, } +/// The parameters for a Solana [`getBalance`](https://solana.com/docs/rpc/http/getbalance) RPC method call. #[derive(Debug, Clone, Default, Deserialize, Serialize, CandidType)] pub struct GetBalanceParams { /// The public key of the account to query formatted as a base-58 string. From 1a5f8811bf2027329d9ab2a06da9f3419cdcda4d Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 17 Apr 2025 15:16:38 +0200 Subject: [PATCH 11/14] XC-346: E2E test --- canister/scripts/examples.sh | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/canister/scripts/examples.sh b/canister/scripts/examples.sh index a7560bc8..403fc342 100755 --- a/canister/scripts/examples.sh +++ b/canister/scripts/examples.sh @@ -84,4 +84,21 @@ GET_ACCOUNT_INFO_PARAMS="( }, )" CYCLES=$(dfx canister call sol_rpc getAccountInfoCyclesCost "$GET_ACCOUNT_INFO_PARAMS" $FLAGS --output json | jq '.Ok' --raw-output || exit 1) -dfx canister call sol_rpc getAccountInfo "$GET_ACCOUNT_INFO_PARAMS" $FLAGS --with-cycles "$CYCLES" || exit 1 \ No newline at end of file +dfx canister call sol_rpc getAccountInfo "$GET_ACCOUNT_INFO_PARAMS" $FLAGS --with-cycles "$CYCLES" || exit 1 + +GET_BALANCE_PARAMS="( + variant { Default = variant { Mainnet } }, + opt record { + responseConsensus = opt variant { + Threshold = record { min = 2 : nat8; total = opt (3 : nat8) } + }; + responseSizeEstimate = null; + }, + record { + pubkey = \"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v\"; + commitment = null; + minContextSlot = null; + }, +)" +CYCLES=$(dfx canister call sol_rpc getBalanceCyclesCost "$GET_BALANCE_PARAMS" $FLAGS --output json | jq '.Ok' --raw-output || exit 1) +dfx canister call sol_rpc getBalance "$GET_BALANCE_PARAMS" $FLAGS --with-cycles "$CYCLES" || exit 1 From efe54364ae61ed44d48ebd57305ea81a6424cf54 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 17 Apr 2025 15:44:37 +0200 Subject: [PATCH 12/14] XC-346: integration test --- integration_tests/tests/tests.rs | 70 ++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/integration_tests/tests/tests.rs b/integration_tests/tests/tests.rs index f7bc37cb..9d2f322f 100644 --- a/integration_tests/tests/tests.rs +++ b/integration_tests/tests/tests.rs @@ -17,6 +17,7 @@ use sol_rpc_types::{ SupportedRpcProvider, SupportedRpcProviderId, }; use solana_account_decoder_client_types::{UiAccount, UiAccountData, UiAccountEncoding}; +use solana_pubkey::pubkey; use solana_signer::Signer; use std::{fmt::Debug, iter::zip, str::FromStr}; use strum::IntoEnumIterator; @@ -28,6 +29,8 @@ const MOCK_RESPONSE: &str = formatcp!( MOCK_RESPONSE_RESULT ); const MOCK_REQUEST_MAX_RESPONSE_BYTES: u64 = 1000; +const USDC_PUBLIC_KEY: solana_pubkey::Pubkey = + pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); mod mock_request_tests { use super::*; @@ -969,6 +972,73 @@ mod cycles_cost_tests { } } +mod get_balance_tests { + use crate::{rpc_sources, USDC_PUBLIC_KEY}; + use canhttp::http::json::{ConstantSizeId, Id}; + use serde_json::json; + use sol_rpc_int_tests::mock::MockOutcallBuilder; + use sol_rpc_int_tests::{Setup, SolRpcTestClient}; + use sol_rpc_types::CommitmentLevel; + use std::iter::zip; + + #[tokio::test] + async fn should_get_balance() { + fn request_body(id: u8) -> serde_json::Value { + json!({ + "jsonrpc": "2.0", + "id": Id::from(ConstantSizeId::from(id)), + "method": "getBalance", + "params": [ + USDC_PUBLIC_KEY.to_string(), + { + "commitment": "confirmed", + "minContextSlot": 100 + } + ] + }) + } + + fn response_body(id: u8) -> serde_json::Value { + json!({ + "id": Id::from(ConstantSizeId::from(id)), + "jsonrpc": "2.0", + "result": { + "context": { "slot": 334048531, "apiVersion": "2.1.9" }, + "value": 389086612571_u64 + }, + }) + } + let setup = Setup::new().await.with_mock_api_keys().await; + + for (sources, first_id) in zip(rpc_sources(), vec![0_u8, 3, 6]) { + let client = setup.client().with_rpc_sources(sources); + + let results = client + .mock_http_sequence(vec![ + MockOutcallBuilder::new(200, response_body(first_id)) + .with_request_body(request_body(first_id)), + MockOutcallBuilder::new(200, response_body(first_id + 1)) + .with_request_body(request_body(first_id + 1)), + MockOutcallBuilder::new(200, response_body(first_id + 2)) + .with_request_body(request_body(first_id + 2)), + ]) + .build() + .get_balance(USDC_PUBLIC_KEY) + .modify_params(|params| { + params.commitment = Some(CommitmentLevel::Confirmed); + params.min_context_slot = Some(100); + }) + .send() + .await + .expect_consistent(); + + assert_eq!(results, Ok(389_086_612_571_u64)); + } + + setup.drop().await; + } +} + fn assert_within(actual: u128, expected: u128, percentage_error: u8) { assert!(percentage_error <= 100); let error_margin = expected.saturating_mul(percentage_error as u128) / 100; From 41b0636b9321a4e5ff765aafaec9856dd5f62cad Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 17 Apr 2025 15:48:43 +0200 Subject: [PATCH 13/14] XC-346: use const public key for tests --- integration_tests/tests/tests.rs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/integration_tests/tests/tests.rs b/integration_tests/tests/tests.rs index 9d2f322f..77424699 100644 --- a/integration_tests/tests/tests.rs +++ b/integration_tests/tests/tests.rs @@ -773,10 +773,10 @@ mod cycles_cost_tests { check(client.json_request(get_version_request())).await; } SolRpcEndpoint::GetAccountInfo => { - check(client.get_account_info(some_pubkey())).await; + check(client.get_account_info(USDC_PUBLIC_KEY)).await; } SolRpcEndpoint::GetBalance => { - check(client.get_balance(some_pubkey())).await; + check(client.get_balance(USDC_PUBLIC_KEY)).await; } SolRpcEndpoint::GetBlock => { check(client.get_block(577996)).await; @@ -820,10 +820,10 @@ mod cycles_cost_tests { check(client.get_slot().with_params(GetSlotParams::default())).await; } SolRpcEndpoint::GetAccountInfo => { - check(client.get_account_info(some_pubkey())).await; + check(client.get_account_info(USDC_PUBLIC_KEY)).await; } SolRpcEndpoint::GetBalance => { - check(client.get_balance(some_pubkey())).await; + check(client.get_balance(USDC_PUBLIC_KEY)).await; } SolRpcEndpoint::GetBlock => { check(client.get_block(577996)).await; @@ -922,13 +922,13 @@ mod cycles_cost_tests { SolRpcEndpoint::GetAccountInfo => { check( &setup, - client.get_account_info(some_pubkey()), + client.get_account_info(USDC_PUBLIC_KEY), 1_793_744_800, ) .await; } SolRpcEndpoint::GetBalance => { - check(&setup, client.get_balance(some_pubkey()), 1_731_769_600).await; + check(&setup, client.get_balance(USDC_PUBLIC_KEY), 1_731_769_600).await; } SolRpcEndpoint::GetBlock => { check(&setup, client.get_block(577996), 1_791_868_000).await; @@ -1053,12 +1053,6 @@ fn assert_within(actual: u128, expected: u128, percentage_error: u8) { ); } -fn some_pubkey() -> solana_pubkey::Pubkey { - "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" - .parse::() - .unwrap() -} - fn some_transaction() -> solana_transaction::Transaction { let keypair = solana_keypair::Keypair::new(); solana_transaction::Transaction::new_signed_with_payer( From be4dc7b4ea17a5263dbc9c9710a4711a010f76e7 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 17 Apr 2025 15:53:37 +0200 Subject: [PATCH 14/14] XC-346: modify context --- integration_tests/tests/tests.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/integration_tests/tests/tests.rs b/integration_tests/tests/tests.rs index 77424699..89e05254 100644 --- a/integration_tests/tests/tests.rs +++ b/integration_tests/tests/tests.rs @@ -1003,7 +1003,8 @@ mod get_balance_tests { "id": Id::from(ConstantSizeId::from(id)), "jsonrpc": "2.0", "result": { - "context": { "slot": 334048531, "apiVersion": "2.1.9" }, + // context should be filtered out by transform + "context": { "slot": 334048531 + id as u64, "apiVersion": "2.1.9" }, "value": 389086612571_u64 }, })