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 diff --git a/canister/sol_rpc_canister.did b/canister/sol_rpc_canister.did index ec45f0cc..24171b0a 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,10 @@ 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); + getBalanceCyclesCost : (RpcSources, opt RpcConfig, GetBalanceParams) -> (RequestCostResult) query; + // 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 3b99391e..90fd2500 100644 --- a/canister/src/main.rs +++ b/canister/src/main.rs @@ -12,9 +12,9 @@ use sol_rpc_canister::{ rpc_client::MultiRpcRequest, }; use sol_rpc_types::{ - AccountInfo, ConfirmedBlock, GetAccountInfoParams, GetBlockParams, GetSlotParams, - GetSlotRpcConfig, GetTransactionParams, MultiRpcResult, RpcAccess, RpcConfig, RpcResult, - RpcSources, SendTransactionParams, Signature, Slot, SupportedRpcProvider, + AccountInfo, ConfirmedBlock, GetAccountInfoParams, GetBalanceParams, GetBlockParams, + GetSlotParams, GetSlotRpcConfig, GetTransactionParams, Lamport, MultiRpcResult, RpcAccess, + RpcConfig, RpcResult, RpcSources, SendTransactionParams, Signature, Slot, SupportedRpcProvider, SupportedRpcProviderId, TransactionInfo, }; use std::str::FromStr; @@ -101,6 +101,32 @@ 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 +} + +#[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( 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..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,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..d2c1e4fc 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 => { + canonicalize_response::(body_bytes, |result| result["value"].clone()); + } Self::GetBlock => { canonicalize_response::>(body_bytes, |result| match result { Value::Null => None, 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), ); } 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/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/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/integration_tests/tests/tests.rs b/integration_tests/tests/tests.rs index d9619874..89e05254 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::*; @@ -770,7 +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(USDC_PUBLIC_KEY)).await; } SolRpcEndpoint::GetBlock => { check(client.get_block(577996)).await; @@ -814,7 +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(USDC_PUBLIC_KEY)).await; } SolRpcEndpoint::GetBlock => { check(client.get_block(577996)).await; @@ -913,11 +922,14 @@ 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(USDC_PUBLIC_KEY), 1_731_769_600).await; + } SolRpcEndpoint::GetBlock => { check(&setup, client.get_block(577996), 1_791_868_000).await; } @@ -960,6 +972,74 @@ 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 should be filtered out by transform + "context": { "slot": 334048531 + id as u64, "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; @@ -974,12 +1054,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( diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index bf987fb5..7b1edd4f 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()), + 10_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..5f7dc5ad 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); @@ -292,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(); diff --git a/libs/types/src/lib.rs b/libs/types/src/lib.rs index 2b6c557b..593c7b90 100644 --- a/libs/types/src/lib.rs +++ b/libs/types/src/lib.rs @@ -19,7 +19,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, }, @@ -30,5 +30,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; diff --git a/libs/types/src/solana/request/mod.rs b/libs/types/src/solana/request/mod.rs index 96549f78..80ce9a53 100644 --- a/libs/types/src/solana/request/mod.rs +++ b/libs/types/src/solana/request/mod.rs @@ -83,6 +83,28 @@ 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. + 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, +} + +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)]