From 1cdf810598a34205b4e566be6e878ad29ebb0a43 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Fri, 2 May 2025 16:41:42 +0200 Subject: [PATCH 01/42] XC-326: base types --- canister/sol_rpc_canister.did | 31 ++++++++++++++++++++++++++++ canister/src/main.rs | 26 ++++++++++++++++++++++- libs/types/src/lib.rs | 15 +++++++------- libs/types/src/rpc_client/mod.rs | 22 ++++++++++++++++++++ libs/types/src/solana/mod.rs | 11 ++++++++++ libs/types/src/solana/request/mod.rs | 20 ++++++++++++++++++ 6 files changed, 117 insertions(+), 8 deletions(-) diff --git a/canister/sol_rpc_canister.did b/canister/sol_rpc_canister.did index e61fb600..a5994540 100644 --- a/canister/sol_rpc_canister.did +++ b/canister/sol_rpc_canister.did @@ -57,6 +57,15 @@ type GetSlotRpcConfig = record { roundingError : opt RoundingError; }; +// Configures how to perform `getRecentPrioritizationFees` RPC HTTP calls. +type GetRecentPrioritizationFeesRpcConfig = record { + responseSizeEstimate : opt nat64; + responseConsensus : opt ConsensusStrategy; + maxSlotRoundingError : opt RoundingError; + // The number of slots to look back to calculate the estimate. Valid numbers are 1-150, default is 100 + numSlots : opt nat8; +}; + // Defines a consensus strategy for combining responses from different providers. type ConsensusStrategy = variant { Equality; @@ -567,6 +576,20 @@ type MultiGetTransactionResult = variant { Inconsistent : vec record { RpcSource; GetTransactionResult }; }; +type PrioritizationFee = record { + slot: Slot; + prioritizationFee: nat64 +}; + +// Represents the result of a call to the `getSlot` Solana RPC method. +type GetRecentPrioritizationFeesResult = variant { Ok : vec PrioritizationFee; Err : RpcError }; + +// Represents an aggregated result from multiple RPC calls to the `getRecentPrioritizationFeesResult` Solana RPC method. +type MultiGetRecentPrioritizationFeesResult = variant { + Consistent : GetRecentPrioritizationFeesResult; + Inconsistent : vec record { RpcSource; GetRecentPrioritizationFeesResult }; +}; + // Represents a Solana slot type Slot = nat64; @@ -595,6 +618,10 @@ type CommitmentLevel = variant { finalized; }; +// The parameters for a call to the `getRecentPrioritizationFees` Solana RPC method. +// An array of Account addresses (up to a maximum of 128 addresses), as base-58 encoded strings. +type GetRecentPrioritizationFeesParams = vec Pubkey; + // The parameters for a call to the `getSlot` Solana RPC method. type GetSlotParams = record { commitment: opt CommitmentLevel; @@ -674,6 +701,10 @@ service : (InstallArgs,) -> { // Call the Solana `getBlock` RPC method and return the resulting block. getBlock : (RpcSources, opt RpcConfig, GetBlockParams) -> (MultiGetBlockResult); getBlockCyclesCost : (RpcSources, opt RpcConfig, GetBlockParams) -> (RequestCostResult) query; + + // Call the Solana `getRecentPrioritizationFees` RPC method and return the resulting slot. + getRecentPrioritizationFees : (RpcSources, opt GetRecentPrioritizationFeesRpcConfig, opt GetRecentPrioritizationFeesParams) -> (MultiGetRecentPrioritizationFeesResult); + getRecentPrioritizationFeesCyclesCost : (RpcSources, opt GetRecentPrioritizationFeesRpcConfig, opt GetRecentPrioritizationFeesParams) -> (RequestCostResult) query; // Call the Solana `getSlot` RPC method and return the resulting slot. getSlot : (RpcSources, opt GetSlotRpcConfig, opt GetSlotParams) -> (MultiGetSlotResult); diff --git a/canister/src/main.rs b/canister/src/main.rs index 54ea8ca6..d2b3ea10 100644 --- a/canister/src/main.rs +++ b/canister/src/main.rs @@ -14,7 +14,8 @@ use sol_rpc_canister::{ }; use sol_rpc_types::{ AccountInfo, ConfirmedBlock, GetAccountInfoParams, GetBalanceParams, GetBlockParams, - GetSlotParams, GetSlotRpcConfig, GetTransactionParams, Lamport, MultiRpcResult, RpcAccess, + GetRecentPrioritizationFeesParams, GetRecentPrioritizationFeesRpcConfig, GetSlotParams, + GetSlotRpcConfig, GetTransactionParams, Lamport, MultiRpcResult, PrioritizationFee, RpcAccess, RpcConfig, RpcResult, RpcSources, SendTransactionParams, Signature, Slot, SupportedRpcProvider, SupportedRpcProviderId, TransactionInfo, }; @@ -154,6 +155,29 @@ async fn get_block_cycles_cost( .await } +#[update(name = "getRecentPrioritizationFees")] +#[candid_method(rename = "getRecentPrioritizationFees")] +async fn get_recent_prioritization_fees( + source: RpcSources, + config: Option, + params: Option, +) -> MultiRpcResult> { + todo!() +} + +#[query(name = "getRecentPrioritizationFeesCyclesCost")] +#[candid_method(query, rename = "getRecentPrioritizationFeesCyclesCost")] +async fn get_recent_prioritization_fees_cycles_cost( + source: RpcSources, + config: Option, + params: Option, +) -> RpcResult { + if read_state(State::is_demo_mode_active) { + return Ok(0); + } + todo!() +} + #[update(name = "getSlot")] #[candid_method(rename = "getSlot")] async fn get_slot( diff --git a/libs/types/src/lib.rs b/libs/types/src/lib.rs index 593c7b90..3d0a74f9 100644 --- a/libs/types/src/lib.rs +++ b/libs/types/src/lib.rs @@ -11,17 +11,18 @@ mod solana; pub use lifecycle::{InstallArgs, Mode, NumSubnetNodes}; pub use response::MultiRpcResult; pub use rpc_client::{ - ConsensusStrategy, GetSlotRpcConfig, HttpHeader, HttpOutcallError, JsonRpcError, - OverrideProvider, ProviderError, RegexString, RegexSubstitution, RpcAccess, RpcAuth, RpcConfig, - RpcEndpoint, RpcError, RpcResult, RpcSource, RpcSources, SolanaCluster, SupportedRpcProvider, - SupportedRpcProviderId, + ConsensusStrategy, GetRecentPrioritizationFeesRpcConfig, GetSlotRpcConfig, HttpHeader, + HttpOutcallError, JsonRpcError, OverrideProvider, ProviderError, RegexString, + RegexSubstitution, RpcAccess, RpcAuth, RpcConfig, RpcEndpoint, RpcError, RpcResult, RpcSource, + RpcSources, SolanaCluster, SupportedRpcProvider, SupportedRpcProviderId, }; pub use solana::{ account::{AccountData, AccountEncoding, AccountInfo, ParsedAccount}, request::{ CommitmentLevel, DataSlice, GetAccountInfoEncoding, GetAccountInfoParams, GetBalanceParams, - GetBlockCommitmentLevel, GetBlockParams, GetSlotParams, GetTransactionEncoding, - GetTransactionParams, SendTransactionEncoding, SendTransactionParams, TransactionDetails, + GetBlockCommitmentLevel, GetBlockParams, GetRecentPrioritizationFeesParams, GetSlotParams, + GetTransactionEncoding, GetTransactionParams, SendTransactionEncoding, + SendTransactionParams, TransactionDetails, }, transaction::{ error::{InstructionError, TransactionError}, @@ -30,5 +31,5 @@ pub use solana::{ EncodedTransaction, LoadedAddresses, TransactionBinaryEncoding, TransactionInfo, TransactionReturnData, TransactionStatusMeta, TransactionTokenBalance, TransactionVersion, }, - Blockhash, ConfirmedBlock, Lamport, Pubkey, Signature, Slot, Timestamp, + Blockhash, ConfirmedBlock, Lamport, PrioritizationFee, Pubkey, Signature, Slot, Timestamp, }; diff --git a/libs/types/src/rpc_client/mod.rs b/libs/types/src/rpc_client/mod.rs index f236b4e9..6e3a72cc 100644 --- a/libs/types/src/rpc_client/mod.rs +++ b/libs/types/src/rpc_client/mod.rs @@ -139,6 +139,28 @@ impl From for GetSlotRpcConfig { } } +/// Configures how to perform HTTP calls for the Solana `getRecentPrioritizationFees` RPC method. +#[derive(Clone, Debug, PartialEq, Eq, Default, CandidType, Deserialize)] +pub struct GetRecentPrioritizationFeesRpcConfig { + /// Describes the expected (90th percentile) number of bytes in the HTTP response body. + /// This number should be less than `MAX_PAYLOAD_SIZE`. + #[serde(rename = "responseSizeEstimate")] + pub response_size_estimate: Option, + + /// Specifies how the responses of the different RPC providers should be aggregated into + /// a single response. + #[serde(rename = "responseConsensus")] + pub response_consensus: Option, + + /// TODO + #[serde(rename = "maxSlotRoundingError")] + pub max_slot_rounding_error: Option, + + /// TODO + #[serde(rename = "numSlots")] + pub num_slots: Option +} + /// Defines a consensus strategy for combining responses from different providers. #[derive(Clone, Debug, PartialEq, Eq, Default, CandidType, Deserialize)] pub enum ConsensusStrategy { diff --git a/libs/types/src/solana/mod.rs b/libs/types/src/solana/mod.rs index 95aa9a78..e4953ddc 100644 --- a/libs/types/src/solana/mod.rs +++ b/libs/types/src/solana/mod.rs @@ -80,3 +80,14 @@ impl From for solana_transaction_status_client_types::UiConfirme } } } + +/// An entry in the result of a Solana `getRecentPrioritizationFees` RPC method call. +#[derive(Debug, Clone, Deserialize, Serialize, CandidType, PartialEq)] +pub struct PrioritizationFee { + /// Slot in which the fee was observed. + pub slot: u64, + /// The per-compute-unit fee paid by at least one successfully landed transaction, + /// specified in increments of micro-lamports (0.000001 lamports) + #[serde(rename = "prioritizationFee")] + pub prioritization_fee: u64, +} diff --git a/libs/types/src/solana/request/mod.rs b/libs/types/src/solana/request/mod.rs index eb4dd112..2a6d2b0c 100644 --- a/libs/types/src/solana/request/mod.rs +++ b/libs/types/src/solana/request/mod.rs @@ -1,6 +1,7 @@ use crate::{solana::Pubkey, RpcError, Signature, Slot}; use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine}; use candid::{CandidType, Deserialize}; +use candid::types::{Serializer, Type}; use serde::Serialize; /// The parameters for a Solana [`getAccountInfo`](https://solana.com/docs/rpc/http/getaccountinfo) RPC method call. @@ -173,6 +174,25 @@ pub enum TransactionDetails { None, } +/// The parameters for a Solana [`getRecentPrioritizationFees`](https://solana.com/de/docs/rpc/http/getrecentprioritizationfees) RPC method call. +#[derive(Debug, Clone, Default, Deserialize, Serialize, CandidType)] +#[serde(try_from = "Vec", into = "Vec")] +pub struct GetRecentPrioritizationFeesParams(pub Vec); + +impl TryFrom> for GetRecentPrioritizationFeesParams { + type Error = String; + + fn try_from(value: Vec) -> Result { + todo!("validate at most 128 addresses") + } +} + +impl From for Vec { + fn from(value: GetRecentPrioritizationFeesParams) -> Self { + value.0 + } +} + /// The parameters for a Solana [`getSlot`](https://solana.com/docs/rpc/http/getslot) RPC method call. #[derive(Debug, Clone, Default, Deserialize, Serialize, CandidType)] pub struct GetSlotParams { From 0790f6dc5fc30db82ea9ccb91dc55daf0de564a9 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Fri, 2 May 2025 18:04:36 +0200 Subject: [PATCH 02/42] XC-326: logic for GetRecentPrioritizationFees transform --- canister/src/rpc_client/sol_rpc/mod.rs | 73 ++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/canister/src/rpc_client/sol_rpc/mod.rs b/canister/src/rpc_client/sol_rpc/mod.rs index d2c1e4fc..10952b74 100644 --- a/canister/src/rpc_client/sol_rpc/mod.rs +++ b/canister/src/rpc_client/sol_rpc/mod.rs @@ -3,7 +3,7 @@ mod tests; use crate::types::RoundingError; use candid::candid_method; -use canhttp::http::json::JsonRpcResponse; +use canhttp::http::json::{JsonRpcResponse, JsonRpcResult}; use ic_cdk::{ api::management_canister::http_request::{HttpResponse, TransformArgs}, query, @@ -11,6 +11,7 @@ use ic_cdk::{ use minicbor::{Decode, Encode}; use serde::{de::DeserializeOwned, Serialize}; use serde_json::{from_slice, to_vec, Value}; +use sol_rpc_types::PrioritizationFee; use solana_clock::Slot; use std::fmt::Debug; @@ -26,12 +27,19 @@ pub enum ResponseTransform { #[n(2)] GetBlock, #[n(3)] - GetSlot(#[n(0)] RoundingError), + GetRecentPrioritizationFees { + #[n(0)] + max_slot_rounding_error: RoundingError, + #[n(1)] + num_slots: u8, + }, #[n(4)] - GetTransaction, + GetSlot(#[n(0)] RoundingError), #[n(5)] - SendTransaction, + GetTransaction, #[n(6)] + SendTransaction, + #[n(7)] Raw, } @@ -67,6 +75,63 @@ impl ResponseTransform { value => Some(value), }); } + Self::GetRecentPrioritizationFees { + max_slot_rounding_error, + num_slots, + } => { + assert!( + &1_u8 <= num_slots && num_slots <= &150_u8, + "BUG: expected number of slots to be between 1 and 150, but got {num_slots}" + ); + if let Ok(response) = + from_slice::>>(body_bytes) + { + let (id, result) = response.into_parts(); + match result { + Ok(mut fees) => { + // The order of the prioritization fees in the response is not specified in the + // [API](https://solana.com/de/docs/rpc/http/getrecentprioritizationfees), + // although examples and manual testing show that the response is sorted by increasing number of slot. + // To avoid any problem, we enforce the sorting. + fees.sort_unstable_by_key(|fee| fee.slot); + // Currently, a node's prioritization-fee cache stores data from up to 150 blocks. + if fees.len() <= 150 { + *body_bytes = serde_json::to_vec(&JsonRpcResponse::from_ok( id, fees )) + .expect( + "BUG: failed to serialize previously deserialized JsonRpcResponse", + ); + return; + } + let max_slot = max_slot_rounding_error.round( + fees.last() + .expect("BUG: recent prioritization fees should contain at least 150 elements") + .slot, + ); + let min_slot = max_slot + .checked_sub((num_slots - 1) as u64) + .expect("ERROR: "); + fees.retain(|fee| min_slot <= fee.slot && fee.slot <= max_slot); + assert_eq!(fees.len(), *num_slots as usize); + + *body_bytes = serde_json::to_vec(&JsonRpcResponse::from_ok(id, fees)) + .expect( + "BUG: failed to serialize previously deserialized JsonRpcResponse", + ); + } + Err(json_rpc_error) => { + // canonicalize json representation + *body_bytes = serde_json::to_vec(&JsonRpcResponse::< + Vec, + >::from_error( + id, json_rpc_error + )) + .expect( + "BUG: failed to serialize previously deserialized JsonRpcResponse", + ) + } + } + } + } Self::GetSlot(rounding_error) => { canonicalize_response::(body_bytes, |slot| rounding_error.round(slot)); } From 0237fcc6e9c89ca19d8def46659bbf641fe1aa34 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Mon, 5 May 2025 10:19:22 +0200 Subject: [PATCH 03/42] XC-326: rename maxNumSlots --- canister/sol_rpc_canister.did | 2 +- canister/src/rpc_client/sol_rpc/mod.rs | 4 ++-- libs/types/src/rpc_client/mod.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/canister/sol_rpc_canister.did b/canister/sol_rpc_canister.did index a5994540..63be81de 100644 --- a/canister/sol_rpc_canister.did +++ b/canister/sol_rpc_canister.did @@ -63,7 +63,7 @@ type GetRecentPrioritizationFeesRpcConfig = record { responseConsensus : opt ConsensusStrategy; maxSlotRoundingError : opt RoundingError; // The number of slots to look back to calculate the estimate. Valid numbers are 1-150, default is 100 - numSlots : opt nat8; + maxNumSlots : opt nat8; }; // Defines a consensus strategy for combining responses from different providers. diff --git a/canister/src/rpc_client/sol_rpc/mod.rs b/canister/src/rpc_client/sol_rpc/mod.rs index 10952b74..2486ffe4 100644 --- a/canister/src/rpc_client/sol_rpc/mod.rs +++ b/canister/src/rpc_client/sol_rpc/mod.rs @@ -31,7 +31,7 @@ pub enum ResponseTransform { #[n(0)] max_slot_rounding_error: RoundingError, #[n(1)] - num_slots: u8, + max_num_slots: u8, }, #[n(4)] GetSlot(#[n(0)] RoundingError), @@ -77,7 +77,7 @@ impl ResponseTransform { } Self::GetRecentPrioritizationFees { max_slot_rounding_error, - num_slots, + max_num_slots: num_slots, } => { assert!( &1_u8 <= num_slots && num_slots <= &150_u8, diff --git a/libs/types/src/rpc_client/mod.rs b/libs/types/src/rpc_client/mod.rs index 6e3a72cc..c12a264d 100644 --- a/libs/types/src/rpc_client/mod.rs +++ b/libs/types/src/rpc_client/mod.rs @@ -157,8 +157,8 @@ pub struct GetRecentPrioritizationFeesRpcConfig { pub max_slot_rounding_error: Option, /// TODO - #[serde(rename = "numSlots")] - pub num_slots: Option + #[serde(rename = "maxNumSlots")] + pub max_num_slots: Option } /// Defines a consensus strategy for combining responses from different providers. From 9600f079e21857bf381b79f77ba1af3c379cca57 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Mon, 5 May 2025 13:04:01 +0200 Subject: [PATCH 04/42] XC-326: test normalization --- Cargo.lock | 2 + Cargo.toml | 2 + canister/Cargo.toml | 4 +- canister/src/lib.rs | 1 + canister/src/rpc_client/sol_rpc/mod.rs | 56 +++--- canister/src/rpc_client/sol_rpc/tests.rs | 211 +++++++++++++++++++++++ libs/types/src/solana/request/mod.rs | 1 - 7 files changed, 248 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 13b3b6c9..57bc53c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4117,6 +4117,8 @@ dependencies = [ "minicbor", "num-traits", "proptest", + "rand 0.9.1", + "rand_chacha 0.9.0", "regex", "serde", "serde_bytes", diff --git a/Cargo.toml b/Cargo.toml index 8ded8f8c..414a1889 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,8 @@ num = "0.4.3" num-traits = "0.2.19" pocket-ic = "7.0.0" proptest = "1.6.0" +rand = "0.9.1" +rand_chacha = "0.9.0" regex = "1.11.1" serde = { version = "1.0.219", features = ["derive"] } serde_bytes = "0.11.17" diff --git a/canister/Cargo.toml b/canister/Cargo.toml index 104df330..ff1c3ad7 100644 --- a/canister/Cargo.toml +++ b/canister/Cargo.toml @@ -16,7 +16,7 @@ path = "src/main.rs" assert_matches = { workspace = true } candid = { workspace = true } canhttp = { workspace = true, features = ["json", "multi"] } -canlog = {workspace = true} +canlog = { workspace = true } ciborium = { workspace = true } const_format = { workspace = true } derive_more = { workspace = true } @@ -48,6 +48,8 @@ serde_with = { workspace = true } [dev-dependencies] candid_parser = { workspace = true } proptest = { workspace = true } +rand = { workspace = true } +rand_chacha = { workspace = true } solana-pubkey = { workspace = true } solana-signature = { workspace = true } diff --git a/canister/src/lib.rs b/canister/src/lib.rs index d3b070ad..f923cf20 100644 --- a/canister/src/lib.rs +++ b/canister/src/lib.rs @@ -1,3 +1,4 @@ +#![recursion_limit = "512"] pub mod candid_rpc; pub mod constants; pub mod http; diff --git a/canister/src/rpc_client/sol_rpc/mod.rs b/canister/src/rpc_client/sol_rpc/mod.rs index 2486ffe4..d1c0aa64 100644 --- a/canister/src/rpc_client/sol_rpc/mod.rs +++ b/canister/src/rpc_client/sol_rpc/mod.rs @@ -3,7 +3,7 @@ mod tests; use crate::types::RoundingError; use candid::candid_method; -use canhttp::http::json::{JsonRpcResponse, JsonRpcResult}; +use canhttp::http::json::JsonRpcResponse; use ic_cdk::{ api::management_canister::http_request::{HttpResponse, TransformArgs}, query, @@ -77,41 +77,43 @@ impl ResponseTransform { } Self::GetRecentPrioritizationFees { max_slot_rounding_error, - max_num_slots: num_slots, + max_num_slots, } => { - assert!( - &1_u8 <= num_slots && num_slots <= &150_u8, - "BUG: expected number of slots to be between 1 and 150, but got {num_slots}" - ); if let Ok(response) = from_slice::>>(body_bytes) { let (id, result) = response.into_parts(); match result { Ok(mut fees) => { - // The order of the prioritization fees in the response is not specified in the + // The exact number of elements for the returned priority fees is not really specified in the // [API](https://solana.com/de/docs/rpc/http/getrecentprioritizationfees), - // although examples and manual testing show that the response is sorted by increasing number of slot. - // To avoid any problem, we enforce the sorting. - fees.sort_unstable_by_key(|fee| fee.slot); - // Currently, a node's prioritization-fee cache stores data from up to 150 blocks. - if fees.len() <= 150 { - *body_bytes = serde_json::to_vec(&JsonRpcResponse::from_ok( id, fees )) - .expect( - "BUG: failed to serialize previously deserialized JsonRpcResponse", - ); - return; + // which simply mentions + // "Currently, a node's prioritization-fee cache stores data from up to 150 blocks." + // Manual testing shows that the result seems to always contain 150 elements, + // also for not used addresses. + if fees.is_empty() || max_num_slots == &0 { + fees.clear(); + } else { + // The order of the prioritization fees in the response is not specified in the + // [API](https://solana.com/de/docs/rpc/http/getrecentprioritizationfees), + // although examples and manual testing show that the response is sorted by increasing number of slot. + // To avoid any problem, we enforce the sorting. + fees.sort_unstable_by_key(|fee| fee.slot); + let max_rounded_slot = max_slot_rounding_error.round( + fees.last() + .expect( + "BUG: recent prioritization fees should be non-empty", + ) + .slot, + ); + let min_slot = + max_rounded_slot.saturating_sub((max_num_slots - 1) as u64); + fees.retain(|fee| { + min_slot <= fee.slot && fee.slot <= max_rounded_slot + }); + assert!(fees.len() <= *max_num_slots as usize, + "BUG: expected prioritization fees to have at most {max_num_slots} elements, but got {}", fees.len()); } - let max_slot = max_slot_rounding_error.round( - fees.last() - .expect("BUG: recent prioritization fees should contain at least 150 elements") - .slot, - ); - let min_slot = max_slot - .checked_sub((num_slots - 1) as u64) - .expect("ERROR: "); - fees.retain(|fee| min_slot <= fee.slot && fee.slot <= max_slot); - assert_eq!(fees.len(), *num_slots as usize); *body_bytes = serde_json::to_vec(&JsonRpcResponse::from_ok(id, fees)) .expect( diff --git a/canister/src/rpc_client/sol_rpc/tests.rs b/canister/src/rpc_client/sol_rpc/tests.rs index 07dae7cc..3bf15438 100644 --- a/canister/src/rpc_client/sol_rpc/tests.rs +++ b/canister/src/rpc_client/sol_rpc/tests.rs @@ -310,3 +310,214 @@ mod normalization_tests { ); } } + +mod get_recent_prioritization_fees { + use crate::rpc_client::sol_rpc::ResponseTransform; + use crate::types::RoundingError; + use proptest::arbitrary::any; + use proptest::array::uniform32; + use proptest::prelude::{prop, Strategy}; + use proptest::{prop_assert_eq, proptest}; + use rand::prelude::SliceRandom; + use rand_chacha::rand_core::SeedableRng; + use rand_chacha::ChaCha20Rng; + use serde_json::json; + use sol_rpc_types::{PrioritizationFee, Slot}; + use std::ops::RangeInclusive; + + #[test] + fn should_normalize_response_with_less_than_150_entries() { + fn prioritization_fees(slots: Vec) -> Vec { + slots + .into_iter() + .map(|slot| { + json!({ + "prioritizationFee": slot, + "slot": slot + }) + }) + .collect() + } + let raw_response = json!({ + "jsonrpc": "2.0", + "result": prioritization_fees(vec![1, 2, 3, 4, 5]), + "id": 1 + }); + + for (transform, expected_fees) in [ + ( + ResponseTransform::GetRecentPrioritizationFees { + max_slot_rounding_error: RoundingError::new(2), + max_num_slots: 2, + }, + prioritization_fees(vec![3, 4]), + ), + ( + ResponseTransform::GetRecentPrioritizationFees { + max_slot_rounding_error: RoundingError::new(2), + max_num_slots: 0, + }, + prioritization_fees(vec![]), + ), + ( + ResponseTransform::GetRecentPrioritizationFees { + max_slot_rounding_error: RoundingError::new(2), + max_num_slots: u8::MAX, + }, + prioritization_fees(vec![1, 2, 3, 4]), + ), + ( + ResponseTransform::GetRecentPrioritizationFees { + max_slot_rounding_error: RoundingError::new(10), + max_num_slots: 2, + }, + prioritization_fees(vec![]), + ), + ] { + let mut raw_bytes = serde_json::to_vec(&raw_response).unwrap(); + transform.apply(&mut raw_bytes); + let transformed_response: serde_json::Value = + serde_json::from_slice(&raw_bytes).unwrap(); + + assert_eq!( + transformed_response, + json!({ + "jsonrpc": "2.0", + "result": expected_fees, + "id": 1 + }) + ); + } + } + + #[test] + fn should_normalize_response_with_no_fees() { + let raw_response = json!({ + "jsonrpc": "2.0", + "result": [], + "id": 1 + }); + let transform = ResponseTransform::GetRecentPrioritizationFees { + max_slot_rounding_error: RoundingError::new(2), + max_num_slots: 2, + }; + let original_bytes = serde_json::to_vec(&raw_response).unwrap(); + let mut transformed_bytes = original_bytes.clone(); + transform.apply(&mut transformed_bytes); + let transformed_response: serde_json::Value = + serde_json::from_slice(&transformed_bytes).unwrap(); + + assert_eq!(raw_response, transformed_response); + } + + proptest! { + #[test] + fn should_be_nop_when_failed_to_deserialize(original_bytes in prop::collection::vec(any::(), 0..1000)) { + let transform = ResponseTransform::GetRecentPrioritizationFees { + max_slot_rounding_error: RoundingError::new(2), + max_num_slots: 2, + }; + let mut transformed_bytes = original_bytes.clone(); + transform.apply(&mut transformed_bytes); + + assert_eq!(original_bytes, transformed_bytes); + } + + #[test] + fn should_normalize_get_recent_prioritization_fees_response(fees in arb_prioritization_fees(337346483..=337346632)) { + let raw_response = json!({ + "jsonrpc": "2.0", + "result": fees.clone(), + "id": 1 + }); + + let transform = ResponseTransform::GetRecentPrioritizationFees { + max_slot_rounding_error: RoundingError::new(20), + max_num_slots: 100, + }; + let mut raw_bytes = serde_json::to_vec(&raw_response).unwrap(); + transform.apply(&mut raw_bytes); + let transformed_response: serde_json::Value = serde_json::from_slice(&raw_bytes).unwrap(); + + let mut expected_fees = fees; + // last slot is 337346632 and has index 150. + // Last slot rounded by 20 is 337346620, which has index 138. + expected_fees.drain(138..); + expected_fees.drain(..38); + prop_assert_eq!(expected_fees.len(), 100); + + prop_assert_eq!( + transformed_response, + json!({ + "jsonrpc": "2.0", + "result": expected_fees, + "id": 1 + }) + ) + } + + #[test] + fn should_normalize_unsorted_prioritization_fees( + seed in uniform32(any::()), + fees in arb_prioritization_fees(337346483..=337346632) + ) { + let mut rng = ChaCha20Rng::from_seed(seed); + let shuffled_fees = { + let mut f = fees.clone(); + f.shuffle(&mut rng); + f + }; + let transform = ResponseTransform::GetRecentPrioritizationFees { + max_slot_rounding_error: RoundingError::new(20), + max_num_slots: 100, + }; + + let fees_bytes = { + let raw_response = json!({ + "jsonrpc": "2.0", + "result": fees.clone(), + "id": 1 + }); + let mut raw_bytes = serde_json::to_vec(&raw_response).unwrap(); + transform.apply(&mut raw_bytes); + raw_bytes + }; + + let shuffled_fees_bytes = { + let raw_response = json!({ + "jsonrpc": "2.0", + "result": shuffled_fees, + "id": 1 + }); + let mut raw_bytes = serde_json::to_vec(&raw_response).unwrap(); + transform.apply(&mut raw_bytes); + raw_bytes + }; + + assert_eq!(fees_bytes, shuffled_fees_bytes); + } + } + + fn arb_prioritization_fees( + slots: RangeInclusive, + ) -> impl Strategy> { + let len = if slots.is_empty() { + 0 + } else { + slots.end() - slots.start() + 1 + }; + prop::collection::vec(any::(), len as usize).prop_map(move |fees| { + fees.into_iter() + .enumerate() + .map(|(index, prioritization_fee)| { + let slot = slots.start() + index as u64; + assert!(slots.contains(&slot)); + PrioritizationFee { + slot, + prioritization_fee, + } + }) + .collect::>() + }) + } +} diff --git a/libs/types/src/solana/request/mod.rs b/libs/types/src/solana/request/mod.rs index 2a6d2b0c..8a34bb03 100644 --- a/libs/types/src/solana/request/mod.rs +++ b/libs/types/src/solana/request/mod.rs @@ -1,7 +1,6 @@ use crate::{solana::Pubkey, RpcError, Signature, Slot}; use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine}; use candid::{CandidType, Deserialize}; -use candid::types::{Serializer, Type}; use serde::Serialize; /// The parameters for a Solana [`getAccountInfo`](https://solana.com/docs/rpc/http/getaccountinfo) RPC method call. From 2a119c66cbf4770045479480adf8d371a1e58786 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Tue, 6 May 2025 11:34:45 +0200 Subject: [PATCH 05/42] XC-326: main logic --- Cargo.lock | 1 + canister/Cargo.toml | 1 + canister/src/main.rs | 15 ++++++++++-- canister/src/rpc_client/mod.rs | 34 +++++++++++++++++++++++++++- libs/types/src/rpc_client/mod.rs | 2 +- libs/types/src/solana/request/mod.rs | 11 +++++++-- 6 files changed, 58 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 57bc53c4..1d5a203c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4129,6 +4129,7 @@ dependencies = [ "solana-account-decoder-client-types", "solana-clock", "solana-pubkey", + "solana-rpc-client-api", "solana-signature", "solana-transaction-status-client-types", "thiserror 2.0.12", diff --git a/canister/Cargo.toml b/canister/Cargo.toml index ff1c3ad7..70db5bab 100644 --- a/canister/Cargo.toml +++ b/canister/Cargo.toml @@ -37,6 +37,7 @@ serde_json = { workspace = true } serde_bytes = { workspace = true } solana-account = { workspace = true, features = ["serde"] } solana-account-decoder-client-types = { workspace = true } +solana-rpc-client-api = { workspace = true } solana-transaction-status-client-types = { workspace = true } tower = { workspace = true } tower-http = { workspace = true, features = ["set-header", "util"] } diff --git a/canister/src/main.rs b/canister/src/main.rs index d2b3ea10..c2375097 100644 --- a/canister/src/main.rs +++ b/canister/src/main.rs @@ -162,7 +162,12 @@ async fn get_recent_prioritization_fees( config: Option, params: Option, ) -> MultiRpcResult> { - todo!() + let request = MultiRpcRequest::get_recent_prioritization_fees( + source, + config.unwrap_or_default(), + params.unwrap_or_default(), + ); + send_multi(request).await } #[query(name = "getRecentPrioritizationFeesCyclesCost")] @@ -175,7 +180,13 @@ async fn get_recent_prioritization_fees_cycles_cost( if read_state(State::is_demo_mode_active) { return Ok(0); } - todo!() + MultiRpcRequest::get_recent_prioritization_fees( + source, + config.unwrap_or_default(), + params.unwrap_or_default(), + )? + .cycles_cost() + .await } #[update(name = "getSlot")] diff --git a/canister/src/rpc_client/mod.rs b/canister/src/rpc_client/mod.rs index 8ef9d43e..740f5a54 100644 --- a/canister/src/rpc_client/mod.rs +++ b/canister/src/rpc_client/mod.rs @@ -25,7 +25,8 @@ use ic_cdk::api::management_canister::http_request::{ }; use serde::{de::DeserializeOwned, Serialize}; use sol_rpc_types::{ - ConsensusStrategy, GetSlotRpcConfig, Lamport, ProviderError, RpcConfig, RpcError, RpcResult, + ConsensusStrategy, GetRecentPrioritizationFeesParams, GetRecentPrioritizationFeesRpcConfig, + GetSlotRpcConfig, Lamport, PrioritizationFee, ProviderError, RpcConfig, RpcError, RpcResult, RpcSource, RpcSources, Signature, TransactionDetails, }; use solana_clock::Slot; @@ -193,6 +194,37 @@ impl GetSlotRequest { } } +pub type GetRecentPrioritizationFeesRequest = + MultiRpcRequest>; + +impl GetRecentPrioritizationFeesRequest { + pub fn get_recent_prioritization_fees>( + rpc_sources: RpcSources, + config: GetRecentPrioritizationFeesRpcConfig, + 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(2 * 1024 + HEADER_SIZE_LIMIT); + + Ok(MultiRpcRequest::new( + providers, + JsonRpcRequest::new("getRecentPrioritizationFees", params.into()), + max_response_bytes, + ResponseTransform::GetRecentPrioritizationFees { + max_slot_rounding_error: config + .max_slot_rounding_error + .map(RoundingError::new) + .unwrap_or_default(), + max_num_slots: config.max_num_slots.unwrap_or(100), + }, + ReductionStrategy::from(consensus_strategy), + )) + } +} + pub type GetTransactionRequest = MultiRpcRequest< json::GetTransactionParams, Option, diff --git a/libs/types/src/rpc_client/mod.rs b/libs/types/src/rpc_client/mod.rs index c12a264d..e68fd0a2 100644 --- a/libs/types/src/rpc_client/mod.rs +++ b/libs/types/src/rpc_client/mod.rs @@ -158,7 +158,7 @@ pub struct GetRecentPrioritizationFeesRpcConfig { /// TODO #[serde(rename = "maxNumSlots")] - pub max_num_slots: Option + pub max_num_slots: Option, } /// Defines a consensus strategy for combining responses from different providers. diff --git a/libs/types/src/solana/request/mod.rs b/libs/types/src/solana/request/mod.rs index 8a34bb03..88a7756f 100644 --- a/libs/types/src/solana/request/mod.rs +++ b/libs/types/src/solana/request/mod.rs @@ -181,8 +181,15 @@ pub struct GetRecentPrioritizationFeesParams(pub Vec); impl TryFrom> for GetRecentPrioritizationFeesParams { type Error = String; - fn try_from(value: Vec) -> Result { - todo!("validate at most 128 addresses") + fn try_from(accounts: Vec) -> Result { + const MAX_NUM_ACCOUNTS: usize = 128; + if accounts.len() > MAX_NUM_ACCOUNTS { + return Err(format!( + "Expected at most {MAX_NUM_ACCOUNTS} account addresses, but got {}", + accounts.len() + )); + } + Ok(Self(accounts)) } } From 48be23aa884e308940426f60a44c69eae8da4515 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Tue, 6 May 2025 13:07:17 +0200 Subject: [PATCH 06/42] XC-326: client --- Cargo.lock | 2 + Cargo.toml | 4 +- integration_tests/tests/tests.rs | 10 +++++ libs/client/Cargo.toml | 1 + libs/client/src/lib.rs | 18 ++++++++- libs/client/src/request/mod.rs | 68 +++++++++++++++++++++++++++----- libs/client/src/request/tests.rs | 3 ++ libs/types/Cargo.toml | 1 + libs/types/src/response/mod.rs | 15 ++++++- libs/types/src/rpc_client/mod.rs | 10 +++++ libs/types/src/solana/mod.rs | 9 +++++ 11 files changed, 126 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1d5a203c..c27e83a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4153,6 +4153,7 @@ dependencies = [ "solana-account-decoder-client-types", "solana-clock", "solana-pubkey", + "solana-rpc-client-api", "solana-signature", "solana-transaction-status-client-types", "strum 0.27.1", @@ -4220,6 +4221,7 @@ dependencies = [ "solana-message", "solana-pubkey", "solana-reward-info", + "solana-rpc-client-api", "solana-signature", "solana-transaction", "solana-transaction-context", diff --git a/Cargo.toml b/Cargo.toml index 414a1889..2a85f588 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,8 +56,8 @@ num = "0.4.3" num-traits = "0.2.19" pocket-ic = "7.0.0" proptest = "1.6.0" -rand = "0.9.1" -rand_chacha = "0.9.0" +rand = { version = "0.9.1", default-features = false } +rand_chacha = { version = "0.9.0", default-features = false } regex = "1.11.1" serde = { version = "1.0.219", features = ["derive"] } serde_bytes = "0.11.17" diff --git a/integration_tests/tests/tests.rs b/integration_tests/tests/tests.rs index a0284f58..e516ad96 100644 --- a/integration_tests/tests/tests.rs +++ b/integration_tests/tests/tests.rs @@ -813,6 +813,9 @@ mod cycles_cost_tests { SolRpcEndpoint::GetBlock => { check(client.get_block(577996)).await; } + SolRpcEndpoint::GetRecentPrioritizationFees => { + check(client.get_recent_prioritization_fees()).await + } SolRpcEndpoint::GetTransaction => { check(client.get_transaction(some_signature())).await; } @@ -860,6 +863,9 @@ mod cycles_cost_tests { SolRpcEndpoint::GetBlock => { check(client.get_block(577996)).await; } + SolRpcEndpoint::GetRecentPrioritizationFees => { + check(client.get_recent_prioritization_fees()).await; + } SolRpcEndpoint::GetTransaction => { check(client.get_transaction(some_signature())).await; } @@ -965,6 +971,10 @@ mod cycles_cost_tests { SolRpcEndpoint::GetBlock => { check(&setup, client.get_block(577996), 1_791_868_000).await; } + + SolRpcEndpoint::GetRecentPrioritizationFees => { + check(&setup, client.get_recent_prioritization_fees(), 1).await; + } SolRpcEndpoint::GetSlot => { check( &setup, diff --git a/libs/client/Cargo.toml b/libs/client/Cargo.toml index e0e63f1d..a2c0c09f 100644 --- a/libs/client/Cargo.toml +++ b/libs/client/Cargo.toml @@ -20,6 +20,7 @@ solana-account = { workspace = true } solana-account-decoder-client-types = { workspace = true } solana-clock = { workspace = true } solana-pubkey = { workspace = true } +solana-rpc-client-api = {workspace = true} solana-signature = { workspace = true } solana-transaction-status-client-types = { workspace = true } sol_rpc_types = { version = "0.1.0", path = "../types" } diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index 76fcb51d..4f367755 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -126,8 +126,9 @@ pub use request::{Request, RequestBuilder, SolRpcEndpoint, SolRpcRequest}; use std::fmt::Debug; use crate::request::{ - GetAccountInfoRequest, GetBalanceRequest, GetBlockRequest, GetSlotRequest, - GetTransactionRequest, JsonRequest, SendTransactionRequest, + GetAccountInfoRequest, GetBalanceRequest, GetBlockRequest, GetRecentPrioritizationFeesRequest, + GetRecentPrioritizationFeesRequestBuilder, GetSlotRequest, GetTransactionRequest, JsonRequest, + SendTransactionRequest, }; use async_trait::async_trait; use candid::{utils::ArgumentEncoder, CandidType, Principal}; @@ -396,6 +397,19 @@ impl SolRpcClient { RequestBuilder::new(self.clone(), GetBlockRequest::new(params), cycles) } + /// Call `getRecentPrioritizationFees` on the SOL RPC canister. + /// + /// # Examples + /// + /// TODO XC-326: rust example + pub fn get_recent_prioritization_fees(&self) -> GetRecentPrioritizationFeesRequestBuilder { + RequestBuilder::new( + self.clone(), + GetRecentPrioritizationFeesRequest::default(), + 1_000, + ) + } + /// Call `getSlot` on the SOL RPC canister. /// /// # Examples diff --git a/libs/client/src/request/mod.rs b/libs/client/src/request/mod.rs index 1871a375..b2f31c38 100644 --- a/libs/client/src/request/mod.rs +++ b/libs/client/src/request/mod.rs @@ -6,8 +6,10 @@ use candid::CandidType; use serde::de::DeserializeOwned; use sol_rpc_types::{ AccountInfo, CommitmentLevel, ConfirmedBlock, GetAccountInfoParams, GetBalanceParams, - GetBlockCommitmentLevel, GetBlockParams, GetSlotParams, GetSlotRpcConfig, GetTransactionParams, - Lamport, RpcConfig, RpcResult, RpcSources, SendTransactionParams, Signature, TransactionInfo, + GetBlockCommitmentLevel, GetBlockParams, GetRecentPrioritizationFeesParams, + GetRecentPrioritizationFeesRpcConfig, GetSlotParams, GetSlotRpcConfig, GetTransactionParams, + Lamport, PrioritizationFee, RpcConfig, RpcResult, RpcSources, SendTransactionParams, Signature, + TransactionInfo, }; use solana_clock::Slot; use solana_transaction_status_client_types::EncodedConfirmedTransactionWithStatusMeta; @@ -41,6 +43,8 @@ pub enum SolRpcEndpoint { GetBalance, /// `getBlock` endpoint. GetBlock, + /// `getRecentPrioritizationFees` endpoint. + GetRecentPrioritizationFees, /// `getSlot` endpoint. GetSlot, /// `getTransaction` endpoint. @@ -58,6 +62,7 @@ impl SolRpcEndpoint { SolRpcEndpoint::GetAccountInfo => "getAccountInfo", SolRpcEndpoint::GetBalance => "getBalance", SolRpcEndpoint::GetBlock => "getBlock", + SolRpcEndpoint::GetRecentPrioritizationFees => "getRecentPrioritizationFees", SolRpcEndpoint::GetSlot => "getSlot", SolRpcEndpoint::GetTransaction => "getTransaction", SolRpcEndpoint::JsonRequest => "jsonRequest", @@ -71,6 +76,7 @@ impl SolRpcEndpoint { SolRpcEndpoint::GetAccountInfo => "getAccountInfoCyclesCost", SolRpcEndpoint::GetBalance => "getBalanceCyclesCost", SolRpcEndpoint::GetBlock => "getBlockCyclesCost", + SolRpcEndpoint::GetRecentPrioritizationFees => "getRecentPrioritizationFeesCyclesCost", SolRpcEndpoint::GetSlot => "getSlotCyclesCost", SolRpcEndpoint::GetTransaction => "getTransactionCyclesCost", SolRpcEndpoint::JsonRequest => "jsonRequestCyclesCost", @@ -173,6 +179,27 @@ impl SolRpcRequest for GetBlockRequest { } } +#[derive(Debug, Clone, Default)] +pub struct GetRecentPrioritizationFeesRequest(Option); + +impl SolRpcRequest for GetRecentPrioritizationFeesRequest { + type Config = GetRecentPrioritizationFeesRpcConfig; + type Params = Option; + type CandidOutput = sol_rpc_types::MultiRpcResult>; + type Output = + sol_rpc_types::MultiRpcResult>; + + fn endpoint(&self) -> SolRpcEndpoint { + SolRpcEndpoint::GetRecentPrioritizationFees + } + + fn params(self, _default_commitment_level: Option) -> Self::Params { + // [getRecentPrioritizationFees](https://solana.com/de/docs/rpc/http/getrecentprioritizationfees) + // does not use commitment levels + self.0 + } +} + #[derive(Debug, Clone, Default)] pub struct GetSlotRequest(Option); @@ -290,6 +317,14 @@ pub struct RequestBuilder { request: Request, } +pub type GetRecentPrioritizationFeesRequestBuilder = RequestBuilder< + R, + GetRecentPrioritizationFeesRpcConfig, + Option, + sol_rpc_types::MultiRpcResult>, + sol_rpc_types::MultiRpcResult>, +>; + impl Clone for RequestBuilder { @@ -392,19 +427,32 @@ impl } } +impl + RequestBuilder +{ + /// Change the rounding error for the maximum slot value for a `getRecentPrioritizationFees` request. + pub fn with_max_slot_rounding_error(mut self, rounding_error: u64) -> Self { + let config = self.request.rpc_config_mut().get_or_insert_default(); + config.max_slot_rounding_error = Some(rounding_error); + self + } + + /// Change the maximum number of slots for a `getRecentPrioritizationFees` request. + pub fn with_max_num_slots(mut self, num_slots: u8) -> Self { + let config = self.request.rpc_config_mut().get_or_insert_default(); + config.max_num_slots = Some(num_slots); + self + } +} + impl RequestBuilder { /// Change the rounding error for `getSlot` request. pub fn with_rounding_error(mut self, rounding_error: u64) -> Self { - if let Some(config) = self.request.rpc_config_mut() { - config.rounding_error = Some(rounding_error); - return self; - } - self.with_rpc_config(GetSlotRpcConfig { - rounding_error: Some(rounding_error), - ..Default::default() - }) + let config = self.request.rpc_config_mut().get_or_insert_default(); + config.rounding_error = Some(rounding_error); + self } } diff --git a/libs/client/src/request/tests.rs b/libs/client/src/request/tests.rs index 78858e5e..e121154f 100644 --- a/libs/client/src/request/tests.rs +++ b/libs/client/src/request/tests.rs @@ -38,6 +38,9 @@ fn should_set_correct_commitment_level() { Some(GetBlockCommitmentLevel::Confirmed) ); } + SolRpcEndpoint::GetRecentPrioritizationFees => { + //no op, GetRecentPrioritizationFees does not use commitment level + } SolRpcEndpoint::GetSlot => { let builder = client_with_commitment_level.get_slot(); assert_eq!( diff --git a/libs/types/Cargo.toml b/libs/types/Cargo.toml index 55817e1b..60299f3e 100644 --- a/libs/types/Cargo.toml +++ b/libs/types/Cargo.toml @@ -26,6 +26,7 @@ solana-commitment-config = { workspace = true } solana-instruction = { workspace = true } solana-message = { workspace = true } solana-pubkey = { workspace = true } +solana-rpc-client-api = { workspace = true } solana-reward-info = { workspace = true } solana-signature = { workspace = true } solana-transaction = { workspace = true, features = ["bincode"] } diff --git a/libs/types/src/response/mod.rs b/libs/types/src/response/mod.rs index c0182bdb..0b6aadb6 100644 --- a/libs/types/src/response/mod.rs +++ b/libs/types/src/response/mod.rs @@ -1,5 +1,6 @@ use crate::{ - solana::account::AccountInfo, ConfirmedBlock, RpcResult, RpcSource, Signature, TransactionInfo, + solana::account::AccountInfo, ConfirmedBlock, PrioritizationFee, RpcResult, RpcSource, + Signature, TransactionInfo, }; use candid::CandidType; use serde::Deserialize; @@ -137,3 +138,15 @@ impl From>> result.map(|maybe_transaction| maybe_transaction.map(|transaction| transaction.into())) } } + +impl From>> + for MultiRpcResult> +{ + fn from(result: MultiRpcResult>) -> Self { + result.map(|fees| { + fees.into_iter() + .map(solana_rpc_client_api::response::RpcPrioritizationFee::from) + .collect() + }) + } +} diff --git a/libs/types/src/rpc_client/mod.rs b/libs/types/src/rpc_client/mod.rs index e68fd0a2..2eaf471c 100644 --- a/libs/types/src/rpc_client/mod.rs +++ b/libs/types/src/rpc_client/mod.rs @@ -161,6 +161,16 @@ pub struct GetRecentPrioritizationFeesRpcConfig { pub max_num_slots: Option, } +impl From for GetRecentPrioritizationFeesRpcConfig { + fn from(value: RpcConfig) -> Self { + GetRecentPrioritizationFeesRpcConfig { + response_size_estimate: value.response_size_estimate, + response_consensus: value.response_consensus, + ..Default::default() + } + } +} + /// Defines a consensus strategy for combining responses from different providers. #[derive(Clone, Debug, PartialEq, Eq, Default, CandidType, Deserialize)] pub enum ConsensusStrategy { diff --git a/libs/types/src/solana/mod.rs b/libs/types/src/solana/mod.rs index e4953ddc..88d5c3f9 100644 --- a/libs/types/src/solana/mod.rs +++ b/libs/types/src/solana/mod.rs @@ -91,3 +91,12 @@ pub struct PrioritizationFee { #[serde(rename = "prioritizationFee")] pub prioritization_fee: u64, } + +impl From for solana_rpc_client_api::response::RpcPrioritizationFee { + fn from(value: PrioritizationFee) -> Self { + Self { + slot: value.slot, + prioritization_fee: value.prioritization_fee, + } + } +} From 830f102a9129a1211ed564fd31d29eac43656647 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Tue, 6 May 2025 15:39:18 +0200 Subject: [PATCH 07/42] XC-326: remove chacha20rng dep --- Cargo.lock | 2 - Cargo.toml | 4 +- canister/Cargo.toml | 4 +- canister/src/rpc_client/sol_rpc/tests.rs | 88 ++++++++++++------------ 4 files changed, 48 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c27e83a4..b6029340 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4117,8 +4117,6 @@ dependencies = [ "minicbor", "num-traits", "proptest", - "rand 0.9.1", - "rand_chacha 0.9.0", "regex", "serde", "serde_bytes", diff --git a/Cargo.toml b/Cargo.toml index 2a85f588..d434fc70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,8 +56,8 @@ num = "0.4.3" num-traits = "0.2.19" pocket-ic = "7.0.0" proptest = "1.6.0" -rand = { version = "0.9.1", default-features = false } -rand_chacha = { version = "0.9.0", default-features = false } +#rand = { version = "0.9.1", default-features = false } +#rand_chacha = { version = "0.9.0", default-features = false } regex = "1.11.1" serde = { version = "1.0.219", features = ["derive"] } serde_bytes = "0.11.17" diff --git a/canister/Cargo.toml b/canister/Cargo.toml index 70db5bab..9afbdb34 100644 --- a/canister/Cargo.toml +++ b/canister/Cargo.toml @@ -49,8 +49,8 @@ serde_with = { workspace = true } [dev-dependencies] candid_parser = { workspace = true } proptest = { workspace = true } -rand = { workspace = true } -rand_chacha = { workspace = true } +#rand = { workspace = true } +#rand_chacha = { workspace = true } solana-pubkey = { workspace = true } solana-signature = { workspace = true } diff --git a/canister/src/rpc_client/sol_rpc/tests.rs b/canister/src/rpc_client/sol_rpc/tests.rs index 3bf15438..74aa2f7c 100644 --- a/canister/src/rpc_client/sol_rpc/tests.rs +++ b/canister/src/rpc_client/sol_rpc/tests.rs @@ -315,12 +315,12 @@ mod get_recent_prioritization_fees { use crate::rpc_client::sol_rpc::ResponseTransform; use crate::types::RoundingError; use proptest::arbitrary::any; - use proptest::array::uniform32; + // use proptest::array::uniform32; use proptest::prelude::{prop, Strategy}; use proptest::{prop_assert_eq, proptest}; - use rand::prelude::SliceRandom; - use rand_chacha::rand_core::SeedableRng; - use rand_chacha::ChaCha20Rng; + // use rand::prelude::SliceRandom; + // use rand_chacha::rand_core::SeedableRng; + // use rand_chacha::ChaCha20Rng; use serde_json::json; use sol_rpc_types::{PrioritizationFee, Slot}; use std::ops::RangeInclusive; @@ -456,46 +456,46 @@ mod get_recent_prioritization_fees { ) } - #[test] - fn should_normalize_unsorted_prioritization_fees( - seed in uniform32(any::()), - fees in arb_prioritization_fees(337346483..=337346632) - ) { - let mut rng = ChaCha20Rng::from_seed(seed); - let shuffled_fees = { - let mut f = fees.clone(); - f.shuffle(&mut rng); - f - }; - let transform = ResponseTransform::GetRecentPrioritizationFees { - max_slot_rounding_error: RoundingError::new(20), - max_num_slots: 100, - }; - - let fees_bytes = { - let raw_response = json!({ - "jsonrpc": "2.0", - "result": fees.clone(), - "id": 1 - }); - let mut raw_bytes = serde_json::to_vec(&raw_response).unwrap(); - transform.apply(&mut raw_bytes); - raw_bytes - }; - - let shuffled_fees_bytes = { - let raw_response = json!({ - "jsonrpc": "2.0", - "result": shuffled_fees, - "id": 1 - }); - let mut raw_bytes = serde_json::to_vec(&raw_response).unwrap(); - transform.apply(&mut raw_bytes); - raw_bytes - }; - - assert_eq!(fees_bytes, shuffled_fees_bytes); - } + // #[test] + // fn should_normalize_unsorted_prioritization_fees( + // seed in uniform32(any::()), + // fees in arb_prioritization_fees(337346483..=337346632) + // ) { + // let mut rng = ChaCha20Rng::from_seed(seed); + // let shuffled_fees = { + // let mut f = fees.clone(); + // f.shuffle(&mut rng); + // f + // }; + // let transform = ResponseTransform::GetRecentPrioritizationFees { + // max_slot_rounding_error: RoundingError::new(20), + // max_num_slots: 100, + // }; + // + // let fees_bytes = { + // let raw_response = json!({ + // "jsonrpc": "2.0", + // "result": fees.clone(), + // "id": 1 + // }); + // let mut raw_bytes = serde_json::to_vec(&raw_response).unwrap(); + // transform.apply(&mut raw_bytes); + // raw_bytes + // }; + // + // let shuffled_fees_bytes = { + // let raw_response = json!({ + // "jsonrpc": "2.0", + // "result": shuffled_fees, + // "id": 1 + // }); + // let mut raw_bytes = serde_json::to_vec(&raw_response).unwrap(); + // transform.apply(&mut raw_bytes); + // raw_bytes + // }; + // + // assert_eq!(fees_bytes, shuffled_fees_bytes); + // } } fn arb_prioritization_fees( From 3ba62949b0c218d9ca2bffad8011881dd700fdd3 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Tue, 6 May 2025 15:45:30 +0200 Subject: [PATCH 08/42] Revert "XC-326: remove chacha20rng dep" This reverts commit 830f102a9129a1211ed564fd31d29eac43656647. --- Cargo.lock | 2 + Cargo.toml | 4 +- canister/Cargo.toml | 4 +- canister/src/rpc_client/sol_rpc/tests.rs | 88 ++++++++++++------------ 4 files changed, 50 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6029340..c27e83a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4117,6 +4117,8 @@ dependencies = [ "minicbor", "num-traits", "proptest", + "rand 0.9.1", + "rand_chacha 0.9.0", "regex", "serde", "serde_bytes", diff --git a/Cargo.toml b/Cargo.toml index d434fc70..2a85f588 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,8 +56,8 @@ num = "0.4.3" num-traits = "0.2.19" pocket-ic = "7.0.0" proptest = "1.6.0" -#rand = { version = "0.9.1", default-features = false } -#rand_chacha = { version = "0.9.0", default-features = false } +rand = { version = "0.9.1", default-features = false } +rand_chacha = { version = "0.9.0", default-features = false } regex = "1.11.1" serde = { version = "1.0.219", features = ["derive"] } serde_bytes = "0.11.17" diff --git a/canister/Cargo.toml b/canister/Cargo.toml index 9afbdb34..70db5bab 100644 --- a/canister/Cargo.toml +++ b/canister/Cargo.toml @@ -49,8 +49,8 @@ serde_with = { workspace = true } [dev-dependencies] candid_parser = { workspace = true } proptest = { workspace = true } -#rand = { workspace = true } -#rand_chacha = { workspace = true } +rand = { workspace = true } +rand_chacha = { workspace = true } solana-pubkey = { workspace = true } solana-signature = { workspace = true } diff --git a/canister/src/rpc_client/sol_rpc/tests.rs b/canister/src/rpc_client/sol_rpc/tests.rs index 74aa2f7c..3bf15438 100644 --- a/canister/src/rpc_client/sol_rpc/tests.rs +++ b/canister/src/rpc_client/sol_rpc/tests.rs @@ -315,12 +315,12 @@ mod get_recent_prioritization_fees { use crate::rpc_client::sol_rpc::ResponseTransform; use crate::types::RoundingError; use proptest::arbitrary::any; - // use proptest::array::uniform32; + use proptest::array::uniform32; use proptest::prelude::{prop, Strategy}; use proptest::{prop_assert_eq, proptest}; - // use rand::prelude::SliceRandom; - // use rand_chacha::rand_core::SeedableRng; - // use rand_chacha::ChaCha20Rng; + use rand::prelude::SliceRandom; + use rand_chacha::rand_core::SeedableRng; + use rand_chacha::ChaCha20Rng; use serde_json::json; use sol_rpc_types::{PrioritizationFee, Slot}; use std::ops::RangeInclusive; @@ -456,46 +456,46 @@ mod get_recent_prioritization_fees { ) } - // #[test] - // fn should_normalize_unsorted_prioritization_fees( - // seed in uniform32(any::()), - // fees in arb_prioritization_fees(337346483..=337346632) - // ) { - // let mut rng = ChaCha20Rng::from_seed(seed); - // let shuffled_fees = { - // let mut f = fees.clone(); - // f.shuffle(&mut rng); - // f - // }; - // let transform = ResponseTransform::GetRecentPrioritizationFees { - // max_slot_rounding_error: RoundingError::new(20), - // max_num_slots: 100, - // }; - // - // let fees_bytes = { - // let raw_response = json!({ - // "jsonrpc": "2.0", - // "result": fees.clone(), - // "id": 1 - // }); - // let mut raw_bytes = serde_json::to_vec(&raw_response).unwrap(); - // transform.apply(&mut raw_bytes); - // raw_bytes - // }; - // - // let shuffled_fees_bytes = { - // let raw_response = json!({ - // "jsonrpc": "2.0", - // "result": shuffled_fees, - // "id": 1 - // }); - // let mut raw_bytes = serde_json::to_vec(&raw_response).unwrap(); - // transform.apply(&mut raw_bytes); - // raw_bytes - // }; - // - // assert_eq!(fees_bytes, shuffled_fees_bytes); - // } + #[test] + fn should_normalize_unsorted_prioritization_fees( + seed in uniform32(any::()), + fees in arb_prioritization_fees(337346483..=337346632) + ) { + let mut rng = ChaCha20Rng::from_seed(seed); + let shuffled_fees = { + let mut f = fees.clone(); + f.shuffle(&mut rng); + f + }; + let transform = ResponseTransform::GetRecentPrioritizationFees { + max_slot_rounding_error: RoundingError::new(20), + max_num_slots: 100, + }; + + let fees_bytes = { + let raw_response = json!({ + "jsonrpc": "2.0", + "result": fees.clone(), + "id": 1 + }); + let mut raw_bytes = serde_json::to_vec(&raw_response).unwrap(); + transform.apply(&mut raw_bytes); + raw_bytes + }; + + let shuffled_fees_bytes = { + let raw_response = json!({ + "jsonrpc": "2.0", + "result": shuffled_fees, + "id": 1 + }); + let mut raw_bytes = serde_json::to_vec(&raw_response).unwrap(); + transform.apply(&mut raw_bytes); + raw_bytes + }; + + assert_eq!(fees_bytes, shuffled_fees_bytes); + } } fn arb_prioritization_fees( From 3f3eb8b81f036bae7301ce559ec8ba4dd1899808 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Tue, 6 May 2025 15:46:45 +0200 Subject: [PATCH 09/42] XC-326: remove solana-rpc-client-api dependency because it depends transitively on getrandom with default features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cargo tree -i getrandom@0.2.16 -p "sol_rpc_canister" 101 ↵ getrandom v0.2.16 ├── ahash v0.8.11 │ └── solana-feature-set v2.2.0 (https://github.com/dfinity/agave?tag=323039e-js-feature-flag#552a0763) │ └── solana-version v2.2.0 (https://github.com/dfinity/agave?tag=323039e-js-feature-flag#552a0763) │ └── solana-rpc-client-api v2.2.0 (https://github.com/dfinity/agave?tag=323039e-js-feature-flag#552a0763) │ └── sol_rpc_canister v0.1.0 (/Users/greg/git/sol-rpc-canister/canister) --- Cargo.lock | 3 --- canister/Cargo.toml | 1 - libs/client/Cargo.toml | 1 - libs/client/src/request/mod.rs | 5 ++--- libs/types/Cargo.toml | 1 - libs/types/src/response/mod.rs | 15 +-------------- libs/types/src/solana/mod.rs | 9 --------- 7 files changed, 3 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c27e83a4..57bc53c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4129,7 +4129,6 @@ dependencies = [ "solana-account-decoder-client-types", "solana-clock", "solana-pubkey", - "solana-rpc-client-api", "solana-signature", "solana-transaction-status-client-types", "thiserror 2.0.12", @@ -4153,7 +4152,6 @@ dependencies = [ "solana-account-decoder-client-types", "solana-clock", "solana-pubkey", - "solana-rpc-client-api", "solana-signature", "solana-transaction-status-client-types", "strum 0.27.1", @@ -4221,7 +4219,6 @@ dependencies = [ "solana-message", "solana-pubkey", "solana-reward-info", - "solana-rpc-client-api", "solana-signature", "solana-transaction", "solana-transaction-context", diff --git a/canister/Cargo.toml b/canister/Cargo.toml index 70db5bab..ff1c3ad7 100644 --- a/canister/Cargo.toml +++ b/canister/Cargo.toml @@ -37,7 +37,6 @@ serde_json = { workspace = true } serde_bytes = { workspace = true } solana-account = { workspace = true, features = ["serde"] } solana-account-decoder-client-types = { workspace = true } -solana-rpc-client-api = { workspace = true } solana-transaction-status-client-types = { workspace = true } tower = { workspace = true } tower-http = { workspace = true, features = ["set-header", "util"] } diff --git a/libs/client/Cargo.toml b/libs/client/Cargo.toml index a2c0c09f..e0e63f1d 100644 --- a/libs/client/Cargo.toml +++ b/libs/client/Cargo.toml @@ -20,7 +20,6 @@ solana-account = { workspace = true } solana-account-decoder-client-types = { workspace = true } solana-clock = { workspace = true } solana-pubkey = { workspace = true } -solana-rpc-client-api = {workspace = true} solana-signature = { workspace = true } solana-transaction-status-client-types = { workspace = true } sol_rpc_types = { version = "0.1.0", path = "../types" } diff --git a/libs/client/src/request/mod.rs b/libs/client/src/request/mod.rs index b2f31c38..280cf3d4 100644 --- a/libs/client/src/request/mod.rs +++ b/libs/client/src/request/mod.rs @@ -186,8 +186,7 @@ impl SolRpcRequest for GetRecentPrioritizationFeesRequest { type Config = GetRecentPrioritizationFeesRpcConfig; type Params = Option; type CandidOutput = sol_rpc_types::MultiRpcResult>; - type Output = - sol_rpc_types::MultiRpcResult>; + type Output = Self::CandidOutput; fn endpoint(&self) -> SolRpcEndpoint { SolRpcEndpoint::GetRecentPrioritizationFees @@ -322,7 +321,7 @@ pub type GetRecentPrioritizationFeesRequestBuilder = RequestBuilder< GetRecentPrioritizationFeesRpcConfig, Option, sol_rpc_types::MultiRpcResult>, - sol_rpc_types::MultiRpcResult>, + sol_rpc_types::MultiRpcResult>, >; impl Clone diff --git a/libs/types/Cargo.toml b/libs/types/Cargo.toml index 60299f3e..55817e1b 100644 --- a/libs/types/Cargo.toml +++ b/libs/types/Cargo.toml @@ -26,7 +26,6 @@ solana-commitment-config = { workspace = true } solana-instruction = { workspace = true } solana-message = { workspace = true } solana-pubkey = { workspace = true } -solana-rpc-client-api = { workspace = true } solana-reward-info = { workspace = true } solana-signature = { workspace = true } solana-transaction = { workspace = true, features = ["bincode"] } diff --git a/libs/types/src/response/mod.rs b/libs/types/src/response/mod.rs index 0b6aadb6..c0182bdb 100644 --- a/libs/types/src/response/mod.rs +++ b/libs/types/src/response/mod.rs @@ -1,6 +1,5 @@ use crate::{ - solana::account::AccountInfo, ConfirmedBlock, PrioritizationFee, RpcResult, RpcSource, - Signature, TransactionInfo, + solana::account::AccountInfo, ConfirmedBlock, RpcResult, RpcSource, Signature, TransactionInfo, }; use candid::CandidType; use serde::Deserialize; @@ -138,15 +137,3 @@ impl From>> result.map(|maybe_transaction| maybe_transaction.map(|transaction| transaction.into())) } } - -impl From>> - for MultiRpcResult> -{ - fn from(result: MultiRpcResult>) -> Self { - result.map(|fees| { - fees.into_iter() - .map(solana_rpc_client_api::response::RpcPrioritizationFee::from) - .collect() - }) - } -} diff --git a/libs/types/src/solana/mod.rs b/libs/types/src/solana/mod.rs index 88d5c3f9..e4953ddc 100644 --- a/libs/types/src/solana/mod.rs +++ b/libs/types/src/solana/mod.rs @@ -91,12 +91,3 @@ pub struct PrioritizationFee { #[serde(rename = "prioritizationFee")] pub prioritization_fee: u64, } - -impl From for solana_rpc_client_api::response::RpcPrioritizationFee { - fn from(value: PrioritizationFee) -> Self { - Self { - slot: value.slot, - prioritization_fee: value.prioritization_fee, - } - } -} From 58d15d6bf16c889fbd80f4e45d0341a919724a68 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Tue, 6 May 2025 15:58:28 +0200 Subject: [PATCH 10/42] XC-326: fix cycles cost --- integration_tests/tests/tests.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/integration_tests/tests/tests.rs b/integration_tests/tests/tests.rs index e516ad96..768077a9 100644 --- a/integration_tests/tests/tests.rs +++ b/integration_tests/tests/tests.rs @@ -973,7 +973,12 @@ mod cycles_cost_tests { } SolRpcEndpoint::GetRecentPrioritizationFees => { - check(&setup, client.get_recent_prioritization_fees(), 1).await; + check( + &setup, + client.get_recent_prioritization_fees(), + 1_876_772_800, + ) + .await; } SolRpcEndpoint::GetSlot => { check( From c2ea7114dcfa6e171d0cb83fe554a21d7fdd0a4c Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Tue, 6 May 2025 17:15:22 +0200 Subject: [PATCH 11/42] XC-326: failing int test --- integration_tests/tests/tests.rs | 655 +++++++++++++++++++++++++++ libs/client/src/lib.rs | 2 +- libs/client/src/request/mod.rs | 14 + libs/types/src/solana/request/mod.rs | 6 + 4 files changed, 676 insertions(+), 1 deletion(-) diff --git a/integration_tests/tests/tests.rs b/integration_tests/tests/tests.rs index 768077a9..5e12b99e 100644 --- a/integration_tests/tests/tests.rs +++ b/integration_tests/tests/tests.rs @@ -1,3 +1,4 @@ +#![recursion_limit = "512"] use assert_matches::*; use candid::CandidType; use canhttp::http::json::{ConstantSizeId, Id}; @@ -465,6 +466,660 @@ mod get_slot_tests { setup.drop().await; } } +mod get_recent_prioritization_fees_tests { + use crate::USDC_PUBLIC_KEY; + use canhttp::http::json::ConstantSizeId; + use serde_json::json; + use sol_rpc_int_tests::mock::MockOutcallBuilder; + use sol_rpc_int_tests::{Setup, SolRpcTestClient}; + use sol_rpc_types::PrioritizationFee; + + #[tokio::test] + async fn should_get_fees_with_rounding() { + fn request_body(id: u8) -> serde_json::Value { + let id = ConstantSizeId::from(id).to_string(); + json!( { "jsonrpc": "2.0", "id": id, "method": "getRecentPrioritizationFees", "params": [ [ USDC_PUBLIC_KEY.to_string() ] ] } ) + } + + fn response_body(id: u8) -> serde_json::Value { + let id = ConstantSizeId::from(id).to_string(); + json!({ + "jsonrpc": "2.0", + "result": [ + { + "prioritizationFee": 0, + "slot": 338225766 + }, + { + "prioritizationFee": 203228, + "slot": 338225767 + }, + { + "prioritizationFee": 110788, + "slot": 338225768 + }, + { + "prioritizationFee": 395962, + "slot": 338225769 + }, + { + "prioritizationFee": 0, + "slot": 338225770 + }, + { + "prioritizationFee": 395477, + "slot": 338225771 + }, + { + "prioritizationFee": 202136, + "slot": 338225772 + }, + { + "prioritizationFee": 0, + "slot": 338225773 + }, + { + "prioritizationFee": 0, + "slot": 338225774 + }, + { + "prioritizationFee": 0, + "slot": 338225775 + }, + { + "prioritizationFee": 2894338, + "slot": 338225776 + }, + { + "prioritizationFee": 0, + "slot": 338225777 + }, + { + "prioritizationFee": 162918, + "slot": 338225778 + }, + { + "prioritizationFee": 238785, + "slot": 338225779 + }, + { + "prioritizationFee": 10714, + "slot": 338225780 + }, + { + "prioritizationFee": 81000, + "slot": 338225781 + }, + { + "prioritizationFee": 0, + "slot": 338225782 + }, + { + "prioritizationFee": 0, + "slot": 338225783 + }, + { + "prioritizationFee": 202136, + "slot": 338225784 + }, + { + "prioritizationFee": 166667, + "slot": 338225785 + }, + { + "prioritizationFee": 166667, + "slot": 338225786 + }, + { + "prioritizationFee": 0, + "slot": 338225787 + }, + { + "prioritizationFee": 0, + "slot": 338225788 + }, + { + "prioritizationFee": 0, + "slot": 338225789 + }, + { + "prioritizationFee": 0, + "slot": 338225790 + }, + { + "prioritizationFee": 0, + "slot": 338225791 + }, + { + "prioritizationFee": 0, + "slot": 338225792 + }, + { + "prioritizationFee": 0, + "slot": 338225793 + }, + { + "prioritizationFee": 494120, + "slot": 338225794 + }, + { + "prioritizationFee": 0, + "slot": 338225795 + }, + { + "prioritizationFee": 0, + "slot": 338225796 + }, + { + "prioritizationFee": 202136, + "slot": 338225797 + }, + { + "prioritizationFee": 0, + "slot": 338225798 + }, + { + "prioritizationFee": 0, + "slot": 338225799 + }, + { + "prioritizationFee": 202136, + "slot": 338225800 + }, + { + "prioritizationFee": 0, + "slot": 338225801 + }, + { + "prioritizationFee": 0, + "slot": 338225802 + }, + { + "prioritizationFee": 10001, + "slot": 338225803 + }, + { + "prioritizationFee": 0, + "slot": 338225804 + }, + { + "prioritizationFee": 0, + "slot": 338225805 + }, + { + "prioritizationFee": 0, + "slot": 338225806 + }, + { + "prioritizationFee": 0, + "slot": 338225807 + }, + { + "prioritizationFee": 202136, + "slot": 338225808 + }, + { + "prioritizationFee": 0, + "slot": 338225809 + }, + { + "prioritizationFee": 202136, + "slot": 338225810 + }, + { + "prioritizationFee": 0, + "slot": 338225811 + }, + { + "prioritizationFee": 0, + "slot": 338225812 + }, + { + "prioritizationFee": 0, + "slot": 338225813 + }, + { + "prioritizationFee": 0, + "slot": 338225814 + }, + { + "prioritizationFee": 6064097, + "slot": 338225815 + }, + { + "prioritizationFee": 0, + "slot": 338225816 + }, + { + "prioritizationFee": 0, + "slot": 338225817 + }, + { + "prioritizationFee": 0, + "slot": 338225818 + }, + { + "prioritizationFee": 517927, + "slot": 338225819 + }, + { + "prioritizationFee": 0, + "slot": 338225820 + }, + { + "prioritizationFee": 0, + "slot": 338225821 + }, + { + "prioritizationFee": 0, + "slot": 338225822 + }, + { + "prioritizationFee": 602011, + "slot": 338225823 + }, + { + "prioritizationFee": 187015, + "slot": 338225824 + }, + { + "prioritizationFee": 50000, + "slot": 338225825 + }, + { + "prioritizationFee": 0, + "slot": 338225826 + }, + { + "prioritizationFee": 0, + "slot": 338225827 + }, + { + "prioritizationFee": 0, + "slot": 338225828 + }, + { + "prioritizationFee": 0, + "slot": 338225829 + }, + { + "prioritizationFee": 0, + "slot": 338225830 + }, + { + "prioritizationFee": 0, + "slot": 338225831 + }, + { + "prioritizationFee": 0, + "slot": 338225832 + }, + { + "prioritizationFee": 0, + "slot": 338225833 + }, + { + "prioritizationFee": 0, + "slot": 338225834 + }, + { + "prioritizationFee": 0, + "slot": 338225835 + }, + { + "prioritizationFee": 0, + "slot": 338225836 + }, + { + "prioritizationFee": 0, + "slot": 338225837 + }, + { + "prioritizationFee": 0, + "slot": 338225838 + }, + { + "prioritizationFee": 487330, + "slot": 338225839 + }, + { + "prioritizationFee": 149432, + "slot": 338225840 + }, + { + "prioritizationFee": 0, + "slot": 338225841 + }, + { + "prioritizationFee": 0, + "slot": 338225842 + }, + { + "prioritizationFee": 68526, + "slot": 338225843 + }, + { + "prioritizationFee": 0, + "slot": 338225844 + }, + { + "prioritizationFee": 310090, + "slot": 338225845 + }, + { + "prioritizationFee": 0, + "slot": 338225846 + }, + { + "prioritizationFee": 2173913, + "slot": 338225847 + }, + { + "prioritizationFee": 99725, + "slot": 338225848 + }, + { + "prioritizationFee": 0, + "slot": 338225849 + }, + { + "prioritizationFee": 88441, + "slot": 338225850 + }, + { + "prioritizationFee": 0, + "slot": 338225851 + }, + { + "prioritizationFee": 400000, + "slot": 338225852 + }, + { + "prioritizationFee": 0, + "slot": 338225853 + }, + { + "prioritizationFee": 0, + "slot": 338225854 + }, + { + "prioritizationFee": 164507, + "slot": 338225855 + }, + { + "prioritizationFee": 0, + "slot": 338225856 + }, + { + "prioritizationFee": 4898, + "slot": 338225857 + }, + { + "prioritizationFee": 0, + "slot": 338225858 + }, + { + "prioritizationFee": 0, + "slot": 338225859 + }, + { + "prioritizationFee": 142369, + "slot": 338225860 + }, + { + "prioritizationFee": 84566, + "slot": 338225861 + }, + { + "prioritizationFee": 0, + "slot": 338225862 + }, + { + "prioritizationFee": 10001, + "slot": 338225863 + }, + { + "prioritizationFee": 187015, + "slot": 338225864 + }, + { + "prioritizationFee": 8902, + "slot": 338225865 + }, + { + "prioritizationFee": 0, + "slot": 338225866 + }, + { + "prioritizationFee": 75000, + "slot": 338225867 + }, + { + "prioritizationFee": 0, + "slot": 338225868 + }, + { + "prioritizationFee": 0, + "slot": 338225869 + }, + { + "prioritizationFee": 1771477, + "slot": 338225870 + }, + { + "prioritizationFee": 1110536, + "slot": 338225871 + }, + { + "prioritizationFee": 215920, + "slot": 338225872 + }, + { + "prioritizationFee": 68408, + "slot": 338225873 + }, + { + "prioritizationFee": 0, + "slot": 338225874 + }, + { + "prioritizationFee": 260520, + "slot": 338225875 + }, + { + "prioritizationFee": 2143332, + "slot": 338225876 + }, + { + "prioritizationFee": 0, + "slot": 338225877 + }, + { + "prioritizationFee": 84168, + "slot": 338225878 + }, + { + "prioritizationFee": 0, + "slot": 338225879 + }, + { + "prioritizationFee": 0, + "slot": 338225880 + }, + { + "prioritizationFee": 501111, + "slot": 338225881 + }, + { + "prioritizationFee": 88060, + "slot": 338225882 + }, + { + "prioritizationFee": 10001, + "slot": 338225883 + }, + { + "prioritizationFee": 171521, + "slot": 338225884 + }, + { + "prioritizationFee": 0, + "slot": 338225885 + }, + { + "prioritizationFee": 6064097, + "slot": 338225886 + }, + { + "prioritizationFee": 6064097, + "slot": 338225887 + }, + { + "prioritizationFee": 0, + "slot": 338225888 + }, + { + "prioritizationFee": 7578, + "slot": 338225889 + }, + { + "prioritizationFee": 0, + "slot": 338225890 + }, + { + "prioritizationFee": 0, + "slot": 338225891 + }, + { + "prioritizationFee": 202136, + "slot": 338225892 + }, + { + "prioritizationFee": 106090, + "slot": 338225893 + }, + { + "prioritizationFee": 80776, + "slot": 338225894 + }, + { + "prioritizationFee": 111939, + "slot": 338225895 + }, + { + "prioritizationFee": 75000, + "slot": 338225896 + }, + { + "prioritizationFee": 0, + "slot": 338225897 + }, + { + "prioritizationFee": 0, + "slot": 338225898 + }, + { + "prioritizationFee": 0, + "slot": 338225899 + }, + { + "prioritizationFee": 0, + "slot": 338225900 + }, + { + "prioritizationFee": 0, + "slot": 338225901 + }, + { + "prioritizationFee": 183582, + "slot": 338225902 + }, + { + "prioritizationFee": 0, + "slot": 338225903 + }, + { + "prioritizationFee": 0, + "slot": 338225904 + }, + { + "prioritizationFee": 0, + "slot": 338225905 + }, + { + "prioritizationFee": 535775, + "slot": 338225906 + }, + { + "prioritizationFee": 65038, + "slot": 338225907 + }, + { + "prioritizationFee": 0, + "slot": 338225908 + }, + { + "prioritizationFee": 0, + "slot": 338225909 + }, + { + "prioritizationFee": 0, + "slot": 338225910 + }, + { + "prioritizationFee": 0, + "slot": 338225911 + }, + { + "prioritizationFee": 0, + "slot": 338225912 + }, + { + "prioritizationFee": 0, + "slot": 338225913 + }, + { + "prioritizationFee": 0, + "slot": 338225914 + }, + { + "prioritizationFee": 0, + "slot": 338225915 + } + ], + "id": id + } + ) + } + + let setup = Setup::new().await.with_mock_api_keys().await; + let client = setup.client(); + let fees = client + .mock_http_sequence(vec![ + MockOutcallBuilder::new(200, response_body(0)).with_request_body(request_body(0)), + MockOutcallBuilder::new(200, response_body(1)).with_request_body(request_body(1)), + MockOutcallBuilder::new(200, response_body(2)).with_request_body(request_body(2)), + ]) + .build() + .get_recent_prioritization_fees() + .for_writable_account(USDC_PUBLIC_KEY) + .with_max_slot_rounding_error(10) + .with_max_num_slots(5) + .send() + .await + .expect_consistent(); + + assert_eq!( + fees, + Ok(vec![PrioritizationFee { + slot: 0, + prioritization_fee: 0, + }]) + ); + + setup.drop().await; + } +} mod send_transaction_tests { use super::*; diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index 4f367755..9ae0f088 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -406,7 +406,7 @@ impl SolRpcClient { RequestBuilder::new( self.clone(), GetRecentPrioritizationFeesRequest::default(), - 1_000, + 10_000_000_000, ) } diff --git a/libs/client/src/request/mod.rs b/libs/client/src/request/mod.rs index 280cf3d4..6405797e 100644 --- a/libs/client/src/request/mod.rs +++ b/libs/client/src/request/mod.rs @@ -426,6 +426,20 @@ impl } } +impl + RequestBuilder, CandidOutput, Output> +{ + /// Add an account to look up for a `getRecentPrioritizationFees` request. + /// + /// The response to a `getRecentPrioritizationFees` request reflects a fee to land + /// a transaction locking all of the provided accounts as writable. + pub fn for_writable_account(mut self, account: solana_pubkey::Pubkey) -> Self { + let params = self.request.params_mut().get_or_insert_default(); + params.0.push(account.to_string()); + self + } +} + impl RequestBuilder { diff --git a/libs/types/src/solana/request/mod.rs b/libs/types/src/solana/request/mod.rs index 88a7756f..34f421b7 100644 --- a/libs/types/src/solana/request/mod.rs +++ b/libs/types/src/solana/request/mod.rs @@ -193,6 +193,12 @@ impl TryFrom> for GetRecentPrioritizationFeesParams { } } +impl From for GetRecentPrioritizationFeesParams { + fn from(value: solana_pubkey::Pubkey) -> Self { + Self(vec![value.to_string()]) + } +} + impl From for Vec { fn from(value: GetRecentPrioritizationFeesParams) -> Self { value.0 From a7ba8472ce0871e445c1e7911d1bc5fab119620c Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Tue, 6 May 2025 17:39:54 +0200 Subject: [PATCH 12/42] XC-326: fix int test --- canister/src/rpc_client/mod.rs | 2 +- integration_tests/tests/tests.rs | 26 ++++++++++++++++++++++---- libs/types/src/solana/request/mod.rs | 6 +++--- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/canister/src/rpc_client/mod.rs b/canister/src/rpc_client/mod.rs index 740f5a54..8c15a5b8 100644 --- a/canister/src/rpc_client/mod.rs +++ b/canister/src/rpc_client/mod.rs @@ -207,7 +207,7 @@ impl GetRecentPrioritizationFeesRequest { let providers = Providers::new(rpc_sources, consensus_strategy.clone())?; let max_response_bytes = config .response_size_estimate - .unwrap_or(2 * 1024 + HEADER_SIZE_LIMIT); + .unwrap_or(8 * 1024 + HEADER_SIZE_LIMIT); Ok(MultiRpcRequest::new( providers, diff --git a/integration_tests/tests/tests.rs b/integration_tests/tests/tests.rs index 5e12b99e..7de08596 100644 --- a/integration_tests/tests/tests.rs +++ b/integration_tests/tests/tests.rs @@ -1111,10 +1111,28 @@ mod get_recent_prioritization_fees_tests { assert_eq!( fees, - Ok(vec![PrioritizationFee { - slot: 0, - prioritization_fee: 0, - }]) + Ok(vec![ + PrioritizationFee { + prioritization_fee: 535775, + slot: 338225906 + }, + PrioritizationFee { + prioritization_fee: 65038, + slot: 338225907 + }, + PrioritizationFee { + prioritization_fee: 0, + slot: 338225908 + }, + PrioritizationFee { + prioritization_fee: 0, + slot: 338225909 + }, + PrioritizationFee { + prioritization_fee: 0, + slot: 338225910 + }, + ]) ); setup.drop().await; diff --git a/libs/types/src/solana/request/mod.rs b/libs/types/src/solana/request/mod.rs index 34f421b7..e20fbf7e 100644 --- a/libs/types/src/solana/request/mod.rs +++ b/libs/types/src/solana/request/mod.rs @@ -175,7 +175,7 @@ pub enum TransactionDetails { /// The parameters for a Solana [`getRecentPrioritizationFees`](https://solana.com/de/docs/rpc/http/getrecentprioritizationfees) RPC method call. #[derive(Debug, Clone, Default, Deserialize, Serialize, CandidType)] -#[serde(try_from = "Vec", into = "Vec")] +#[serde(try_from = "Vec", into = "[Vec; 1]")] pub struct GetRecentPrioritizationFeesParams(pub Vec); impl TryFrom> for GetRecentPrioritizationFeesParams { @@ -199,9 +199,9 @@ impl From for GetRecentPrioritizationFeesParams { } } -impl From for Vec { +impl From for [Vec; 1] { fn from(value: GetRecentPrioritizationFeesParams) -> Self { - value.0 + [value.0] } } From a56a5d737d54b5eae1212e92e422095854895fa3 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Tue, 6 May 2025 18:04:24 +0200 Subject: [PATCH 13/42] XC-326: trap when too many accounts --- integration_tests/tests/tests.rs | 29 ++++++++++++++++++++++++++++- libs/client/src/request/mod.rs | 11 +++++++---- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/integration_tests/tests/tests.rs b/integration_tests/tests/tests.rs index 7de08596..ba0f4e34 100644 --- a/integration_tests/tests/tests.rs +++ b/integration_tests/tests/tests.rs @@ -473,6 +473,8 @@ mod get_recent_prioritization_fees_tests { use sol_rpc_int_tests::mock::MockOutcallBuilder; use sol_rpc_int_tests::{Setup, SolRpcTestClient}; use sol_rpc_types::PrioritizationFee; + use solana_pubkey::Pubkey; + use std::collections::BTreeSet; #[tokio::test] async fn should_get_fees_with_rounding() { @@ -1102,7 +1104,7 @@ mod get_recent_prioritization_fees_tests { ]) .build() .get_recent_prioritization_fees() - .for_writable_account(USDC_PUBLIC_KEY) + .for_writable_accounts([USDC_PUBLIC_KEY]) .with_max_slot_rounding_error(10) .with_max_num_slots(5) .send() @@ -1137,6 +1139,31 @@ mod get_recent_prioritization_fees_tests { setup.drop().await; } + + #[tokio::test] + #[should_panic( + expected = "Deserialize error: Expected at most 128 account addresses, but got 129" + )] + async fn should_fail_when_requesting_too_many_accounts() { + let setup = Setup::new().await.with_mock_api_keys().await; + + let mut too_many_accounts = BTreeSet::new(); + for i in 0..129_u8 { + let mut key = [0_u8; 32]; + key[0] = i; + too_many_accounts.insert(Pubkey::from(key)); + } + assert_eq!(too_many_accounts.len(), 129); + + let client = setup.client(); + + let _ = client + .build() + .get_recent_prioritization_fees() + .for_writable_accounts(too_many_accounts) + .send() + .await; + } } mod send_transaction_tests { diff --git a/libs/client/src/request/mod.rs b/libs/client/src/request/mod.rs index 6405797e..71ec5bc2 100644 --- a/libs/client/src/request/mod.rs +++ b/libs/client/src/request/mod.rs @@ -430,12 +430,15 @@ impl RequestBuilder, CandidOutput, Output> { /// Add an account to look up for a `getRecentPrioritizationFees` request. - /// - /// The response to a `getRecentPrioritizationFees` request reflects a fee to land + /// + /// The response to a `getRecentPrioritizationFees` request reflects a fee to land /// a transaction locking all of the provided accounts as writable. - pub fn for_writable_account(mut self, account: solana_pubkey::Pubkey) -> Self { + pub fn for_writable_accounts(mut self, accounts: I) -> Self + where + I: IntoIterator, + { let params = self.request.params_mut().get_or_insert_default(); - params.0.push(account.to_string()); + params.0.extend(accounts.into_iter().map(|a| a.to_string())); self } } From fa84e7b42a0e376ad16145941b0e9eb70629bca5 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Wed, 7 May 2025 09:09:14 +0200 Subject: [PATCH 14/42] XC-326: fix exact cycles cost --- integration_tests/tests/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_tests/tests/tests.rs b/integration_tests/tests/tests.rs index ba0f4e34..7c418b25 100644 --- a/integration_tests/tests/tests.rs +++ b/integration_tests/tests/tests.rs @@ -1676,7 +1676,7 @@ mod cycles_cost_tests { check( &setup, client.get_recent_prioritization_fees(), - 1_876_772_800, + 2_378_204_800, ) .await; } From 217393e7bd59beda6c74a9316de160764aab31fd Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Wed, 7 May 2025 09:44:32 +0200 Subject: [PATCH 15/42] XC-326: int test with validator --- .../tests/solana_test_validator.rs | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/integration_tests/tests/solana_test_validator.rs b/integration_tests/tests/solana_test_validator.rs index a741879a..7bf119b0 100644 --- a/integration_tests/tests/solana_test_validator.rs +++ b/integration_tests/tests/solana_test_validator.rs @@ -9,7 +9,8 @@ use sol_rpc_int_tests::PocketIcLiveModeRuntime; use sol_rpc_types::{ CommitmentLevel, GetAccountInfoEncoding, GetAccountInfoParams, GetBlockCommitmentLevel, GetBlockParams, GetSlotParams, GetTransactionEncoding, GetTransactionParams, InstallArgs, - Lamport, OverrideProvider, RegexSubstitution, SendTransactionParams, TransactionDetails, + Lamport, OverrideProvider, PrioritizationFee, RegexSubstitution, SendTransactionParams, + TransactionDetails, }; use solana_account_decoder_client_types::UiAccount; use solana_client::rpc_client::{RpcClient as SolanaRpcClient, RpcClient}; @@ -17,12 +18,14 @@ use solana_commitment_config::CommitmentConfig; use solana_hash::Hash; use solana_keypair::Keypair; use solana_program::system_instruction; -use solana_pubkey::Pubkey; +use solana_pubkey::{pubkey, Pubkey}; use solana_rpc_client_api::config::{RpcBlockConfig, RpcTransactionConfig}; +use solana_rpc_client_api::response::RpcPrioritizationFee; use solana_signature::Signature; use solana_signer::Signer; use solana_transaction::Transaction; use solana_transaction_status_client_types::UiTransactionEncoding; +use std::iter::zip; use std::{ future::Future, str::FromStr, @@ -59,6 +62,48 @@ async fn should_get_slot() { setup.setup.drop().await; } +#[tokio::test(flavor = "multi_thread")] +async fn should_get_recent_prioritization_fees() { + const TOKEN_2022: Pubkey = pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); + let setup = Setup::new().await; + + let (sol_res, ic_res) = setup + .compare_client( + |sol| { + sol.get_recent_prioritization_fees(&[TOKEN_2022]) + .expect("Failed to get recent prioritization fees") + }, + |ic| async move { + ic.get_recent_prioritization_fees() + .for_writable_accounts(vec![TOKEN_2022]) + .with_max_num_slots(150) + .with_max_slot_rounding_error(1) + .send() + .await + .expect_consistent() + .unwrap_or_else(|e| panic!("`getRecentPrioritizationFees` call failed: {e}")) + }, + ) + .await; + + assert_eq!(sol_res.len(), ic_res.len()); + for (fees_sol, fees_ic) in zip(sol_res, ic_res) { + let RpcPrioritizationFee { + slot: slot_sol, + prioritization_fee: prioritization_fee_sol, + } = fees_sol; + let PrioritizationFee { + slot: slot_ic, + prioritization_fee: prioritization_fee_ic, + } = fees_ic; + + assert_eq!(slot_sol, slot_ic); + assert_eq!(prioritization_fee_sol, prioritization_fee_ic) + } + + setup.setup.drop().await; +} + #[tokio::test(flavor = "multi_thread")] async fn should_get_account_info() { let setup = Setup::new().await; From 10ff2672fa23ec0fcde3b6bd3c8523cffbffcae4 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Wed, 7 May 2025 17:08:35 +0200 Subject: [PATCH 16/42] XC-326: generate transactions with priority fees --- Cargo.lock | 10 ++++ Cargo.toml | 4 +- integration_tests/Cargo.toml | 1 + .../tests/solana_test_validator.rs | 57 +++++++++++++++++-- 4 files changed, 67 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 57bc53c4..bb7af7a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4185,6 +4185,7 @@ dependencies = [ "solana-account-decoder-client-types", "solana-client", "solana-commitment-config", + "solana-compute-budget-interface", "solana-hash", "solana-keypair", "solana-message", @@ -4431,6 +4432,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "solana-compute-budget-interface" +version = "2.2.0" +source = "git+https://github.com/dfinity/agave?tag=323039e-js-feature-flag#552a076307b8e68cde986d4b2d7740a88f15fb8b" +dependencies = [ + "solana-instruction", + "solana-sdk-ids", +] + [[package]] name = "solana-connection-cache" version = "2.2.0" diff --git a/Cargo.toml b/Cargo.toml index 2a85f588..d29412be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,7 +56,7 @@ num = "0.4.3" num-traits = "0.2.19" pocket-ic = "7.0.0" proptest = "1.6.0" -rand = { version = "0.9.1", default-features = false } +rand = { version = "0.9.1", default-features = false } rand_chacha = { version = "0.9.0", default-features = false } regex = "1.11.1" serde = { version = "1.0.219", features = ["derive"] } @@ -68,6 +68,7 @@ solana-account-decoder-client-types = "2.2.0" solana-client = "2.2.0" solana-clock = "2.2.0" solana-commitment-config = "2.2.0" +solana-compute-budget-interface = "2.2.0" solana-hash = "2.2.0" solana-instruction = "2.2.0" solana-keypair = "2.2.0" @@ -102,6 +103,7 @@ solana-account-decoder-client-types = { git = "https://github.com/dfinity/agave" solana-client = { git = "https://github.com/dfinity/agave", tag = "323039e-js-feature-flag" } solana-clock = { git = "https://github.com/dfinity/agave", tag = "323039e-js-feature-flag" } solana-commitment-config = { git = "https://github.com/dfinity/agave", tag = "323039e-js-feature-flag" } +solana-compute-budget-interface = { git = "https://github.com/dfinity/agave", tag = "323039e-js-feature-flag" } solana-hash = { git = "https://github.com/dfinity/agave", tag = "323039e-js-feature-flag" } solana-instruction = { git = "https://github.com/dfinity/agave", tag = "323039e-js-feature-flag" } solana-keypair = { git = "https://github.com/dfinity/agave", tag = "323039e-js-feature-flag" } diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index f7624d9b..9fe58bc4 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -28,6 +28,7 @@ sol_rpc_types = { path = "../libs/types" } solana-account = { workspace = true } solana-account-decoder-client-types = { workspace = true } solana-commitment-config = { workspace = true } +solana-compute-budget-interface = {workspace = true} solana-hash = { workspace = true } solana-keypair = { workspace = true } solana-message = { workspace = true } diff --git a/integration_tests/tests/solana_test_validator.rs b/integration_tests/tests/solana_test_validator.rs index 7bf119b0..5de87d3a 100644 --- a/integration_tests/tests/solana_test_validator.rs +++ b/integration_tests/tests/solana_test_validator.rs @@ -15,6 +15,7 @@ use sol_rpc_types::{ use solana_account_decoder_client_types::UiAccount; use solana_client::rpc_client::{RpcClient as SolanaRpcClient, RpcClient}; use solana_commitment_config::CommitmentConfig; +use solana_compute_budget_interface::ComputeBudgetInstruction; use solana_hash::Hash; use solana_keypair::Keypair; use solana_program::system_instruction; @@ -64,18 +65,60 @@ async fn should_get_slot() { #[tokio::test(flavor = "multi_thread")] async fn should_get_recent_prioritization_fees() { - const TOKEN_2022: Pubkey = pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); + const BASE_FEE_PER_SIGNATURE_LAMPORTS: u64 = 5000; + const NUM_TRANSACTIONS: u64 = 10; + let setup = Setup::new().await; + let (sender, sender_balance_before) = setup.generate_keypair_and_fund_account(); + let (recipient, _recipient_balance_before) = setup.generate_keypair_and_fund_account(); + + // Generate some transactions with priority fees to ensure that + // 1) There are some slots + // 2) Priority fee is not always 0 + let mut transactions = Vec::with_capacity(NUM_TRANSACTIONS as usize); + let transaction_amount = 1; + for micro_lamports in 1..=NUM_TRANSACTIONS { + let modify_cu_ix = ComputeBudgetInstruction::set_compute_unit_limit(1_000_000); + let add_priority_fee_ix = + ComputeBudgetInstruction::set_compute_unit_price(micro_lamports as u64); + let transfer_ix = + system_instruction::transfer(&sender.pubkey(), &recipient.pubkey(), transaction_amount); + let blockhash = setup.solana_client.get_latest_blockhash().unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[modify_cu_ix, add_priority_fee_ix, transfer_ix], + Some(&sender.pubkey()), + &[&sender], + blockhash, + ); + let signature = setup + .solana_client + .send_and_confirm_transaction(&transaction) + .unwrap(); + println!("Sent transaction {micro_lamports}: {signature}"); + transactions.push(signature); + } + + let spent_lamports = NUM_TRANSACTIONS * transaction_amount //amount sent + + NUM_TRANSACTIONS * BASE_FEE_PER_SIGNATURE_LAMPORTS //base fee + + NUM_TRANSACTIONS * (NUM_TRANSACTIONS+1) / 2; //prioritization_fee = 1 µL * CUL / 1_000_000 + .. + NUM_TRANSACTIONS * CUL / 1_000_000 and compute unit limit was set to 1 million. + assert_eq!( + sender_balance_before - setup.solana_client.get_balance(&sender.pubkey()).unwrap(), + spent_lamports + ); + + setup.confirm_transaction(transactions.last().unwrap()); + + let account = sender.pubkey(); let (sol_res, ic_res) = setup .compare_client( |sol| { - sol.get_recent_prioritization_fees(&[TOKEN_2022]) + sol.get_recent_prioritization_fees(&[account]) .expect("Failed to get recent prioritization fees") }, |ic| async move { ic.get_recent_prioritization_fees() - .for_writable_accounts(vec![TOKEN_2022]) + .for_writable_accounts(vec![account]) .with_max_num_slots(150) .with_max_slot_rounding_error(1) .send() @@ -86,7 +129,13 @@ async fn should_get_recent_prioritization_fees() { ) .await; - assert_eq!(sol_res.len(), ic_res.len()); + assert_eq!( + sol_res.len(), + ic_res.len(), + "SOL results {:?}, ICP results {:?}", + sol_res, + ic_res + ); for (fees_sol, fees_ic) in zip(sol_res, ic_res) { let RpcPrioritizationFee { slot: slot_sol, From 8896dd12fc8929d1e90c229ace5bb47834083995 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 8 May 2025 09:01:11 +0200 Subject: [PATCH 17/42] XC-326: fix for non-contiguous range of slots --- canister/src/rpc_client/sol_rpc/mod.rs | 22 ++-- canister/src/rpc_client/sol_rpc/tests.rs | 112 ++++++++++++------ .../tests/solana_test_validator.rs | 5 +- 3 files changed, 88 insertions(+), 51 deletions(-) diff --git a/canister/src/rpc_client/sol_rpc/mod.rs b/canister/src/rpc_client/sol_rpc/mod.rs index d1c0aa64..eca5342c 100644 --- a/canister/src/rpc_client/sol_rpc/mod.rs +++ b/canister/src/rpc_client/sol_rpc/mod.rs @@ -31,6 +31,7 @@ pub enum ResponseTransform { #[n(0)] max_slot_rounding_error: RoundingError, #[n(1)] + // TODO XC-326: rename to max_length max_num_slots: u8, }, #[n(4)] @@ -98,21 +99,24 @@ impl ResponseTransform { // [API](https://solana.com/de/docs/rpc/http/getrecentprioritizationfees), // although examples and manual testing show that the response is sorted by increasing number of slot. // To avoid any problem, we enforce the sorting. - fees.sort_unstable_by_key(|fee| fee.slot); + fees.sort_unstable_by(|fee, other_fee| { + other_fee.slot.cmp(&fee.slot) //sort by decreasing order of slot + }); let max_rounded_slot = max_slot_rounding_error.round( - fees.last() + fees.first() .expect( "BUG: recent prioritization fees should be non-empty", ) .slot, ); - let min_slot = - max_rounded_slot.saturating_sub((max_num_slots - 1) as u64); - fees.retain(|fee| { - min_slot <= fee.slot && fee.slot <= max_rounded_slot - }); - assert!(fees.len() <= *max_num_slots as usize, - "BUG: expected prioritization fees to have at most {max_num_slots} elements, but got {}", fees.len()); + + fees = fees + .into_iter() + .skip_while(|fee| fee.slot > max_rounded_slot) + .take(*max_num_slots as usize) + .collect(); + + fees = fees.into_iter().rev().collect(); } *body_bytes = serde_json::to_vec(&JsonRpcResponse::from_ok(id, fees)) diff --git a/canister/src/rpc_client/sol_rpc/tests.rs b/canister/src/rpc_client/sol_rpc/tests.rs index 3bf15438..f2491902 100644 --- a/canister/src/rpc_client/sol_rpc/tests.rs +++ b/canister/src/rpc_client/sol_rpc/tests.rs @@ -321,6 +321,7 @@ mod get_recent_prioritization_fees { use rand::prelude::SliceRandom; use rand_chacha::rand_core::SeedableRng; use rand_chacha::ChaCha20Rng; + use serde::Serialize; use serde_json::json; use sol_rpc_types::{PrioritizationFee, Slot}; use std::ops::RangeInclusive; @@ -338,11 +339,7 @@ mod get_recent_prioritization_fees { }) .collect() } - let raw_response = json!({ - "jsonrpc": "2.0", - "result": prioritization_fees(vec![1, 2, 3, 4, 5]), - "id": 1 - }); + let raw_response = json_response(&prioritization_fees(vec![1, 2, 3, 4, 5])); for (transform, expected_fees) in [ ( @@ -379,24 +376,13 @@ mod get_recent_prioritization_fees { let transformed_response: serde_json::Value = serde_json::from_slice(&raw_bytes).unwrap(); - assert_eq!( - transformed_response, - json!({ - "jsonrpc": "2.0", - "result": expected_fees, - "id": 1 - }) - ); + assert_eq!(transformed_response, json_response(&expected_fees)); } } #[test] fn should_normalize_response_with_no_fees() { - let raw_response = json!({ - "jsonrpc": "2.0", - "result": [], - "id": 1 - }); + let raw_response = json_response::(&[]); let transform = ResponseTransform::GetRecentPrioritizationFees { max_slot_rounding_error: RoundingError::new(2), max_num_slots: 2, @@ -410,6 +396,63 @@ mod get_recent_prioritization_fees { assert_eq!(raw_response, transformed_response); } + // The API of [getRecentPrioritizationFees](https://solana.com/de/docs/rpc/http/getrecentprioritizationfees) + // does not specify whether the array of prioritization fees includes a range of continuous slots. + // The following was observed: + // 1) On mainnet: the range seems always continuous (e.g., for slots 337346483..=337346632), also for not used addresses + // 2) Locally with solana-test-validator, the range is not necessarily continuous, e.g. + // RpcPrioritizationFee { slot: 5183, prioritization_fee: 150 }, RpcPrioritizationFee { slot: 5321, prioritization_fee: 0 } + #[test] + fn should_normalize_response_with_non_contiguous_slots() { + let range_1 = [PrioritizationFee { + slot: 150, + prioritization_fee: 150, + }]; + let range_2 = [PrioritizationFee { + slot: 500, + prioritization_fee: 500, + }]; + let fees = [&range_1[..], &range_2[..]].concat(); + + let transform = ResponseTransform::GetRecentPrioritizationFees { + max_slot_rounding_error: RoundingError::new(10), + max_num_slots: 100, + }; + let mut raw_bytes = serde_json::to_vec(&json_response(&fees)).unwrap(); + transform.apply(&mut raw_bytes); + let transformed_response: serde_json::Value = serde_json::from_slice(&raw_bytes).unwrap(); + + assert_eq!(transformed_response, json_response(&fees)); + } + + #[test] + fn should_normalize_response_when_rounded_slot_not_in_range() { + let fees = [ + PrioritizationFee { + slot: 100, + prioritization_fee: 100, + }, + PrioritizationFee { + slot: 200, + prioritization_fee: 200, + }, + PrioritizationFee { + slot: 301, + prioritization_fee: 300, + }, + ]; + + let transform = ResponseTransform::GetRecentPrioritizationFees { + max_slot_rounding_error: RoundingError::new(10), + max_num_slots: 100, + }; + let mut raw_bytes = serde_json::to_vec(&json_response(&fees)).unwrap(); + transform.apply(&mut raw_bytes); + let transformed_response: serde_json::Value = serde_json::from_slice(&raw_bytes).unwrap(); + + assert_eq!(transformed_response, json_response(&fees[0..2])); + } + proptest! { #[test] fn should_be_nop_when_failed_to_deserialize(original_bytes in prop::collection::vec(any::(), 0..1000)) { @@ -425,12 +468,7 @@ mod get_recent_prioritization_fees { #[test] fn should_normalize_get_recent_prioritization_fees_response(fees in arb_prioritization_fees(337346483..=337346632)) { - let raw_response = json!({ - "jsonrpc": "2.0", - "result": fees.clone(), - "id": 1 - }); - + let raw_response = json_response(&fees); let transform = ResponseTransform::GetRecentPrioritizationFees { max_slot_rounding_error: RoundingError::new(20), max_num_slots: 100, @@ -448,11 +486,7 @@ mod get_recent_prioritization_fees { prop_assert_eq!( transformed_response, - json!({ - "jsonrpc": "2.0", - "result": expected_fees, - "id": 1 - }) + json_response(&expected_fees) ) } @@ -473,22 +507,14 @@ mod get_recent_prioritization_fees { }; let fees_bytes = { - let raw_response = json!({ - "jsonrpc": "2.0", - "result": fees.clone(), - "id": 1 - }); + let raw_response = json_response(&fees); let mut raw_bytes = serde_json::to_vec(&raw_response).unwrap(); transform.apply(&mut raw_bytes); raw_bytes }; let shuffled_fees_bytes = { - let raw_response = json!({ - "jsonrpc": "2.0", - "result": shuffled_fees, - "id": 1 - }); + let raw_response = json_response(&shuffled_fees); let mut raw_bytes = serde_json::to_vec(&raw_response).unwrap(); transform.apply(&mut raw_bytes); raw_bytes @@ -520,4 +546,12 @@ mod get_recent_prioritization_fees { .collect::>() }) } + + fn json_response(fees: &[T]) -> serde_json::Value { + json!({ + "jsonrpc": "2.0", + "result": fees, + "id": 1 + }) + } } diff --git a/integration_tests/tests/solana_test_validator.rs b/integration_tests/tests/solana_test_validator.rs index 5de87d3a..7b06ebfb 100644 --- a/integration_tests/tests/solana_test_validator.rs +++ b/integration_tests/tests/solana_test_validator.rs @@ -19,7 +19,7 @@ use solana_compute_budget_interface::ComputeBudgetInstruction; use solana_hash::Hash; use solana_keypair::Keypair; use solana_program::system_instruction; -use solana_pubkey::{pubkey, Pubkey}; +use solana_pubkey::Pubkey; use solana_rpc_client_api::config::{RpcBlockConfig, RpcTransactionConfig}; use solana_rpc_client_api::response::RpcPrioritizationFee; use solana_signature::Signature; @@ -80,8 +80,7 @@ async fn should_get_recent_prioritization_fees() { let transaction_amount = 1; for micro_lamports in 1..=NUM_TRANSACTIONS { let modify_cu_ix = ComputeBudgetInstruction::set_compute_unit_limit(1_000_000); - let add_priority_fee_ix = - ComputeBudgetInstruction::set_compute_unit_price(micro_lamports as u64); + let add_priority_fee_ix = ComputeBudgetInstruction::set_compute_unit_price(micro_lamports); let transfer_ix = system_instruction::transfer(&sender.pubkey(), &recipient.pubkey(), transaction_amount); let blockhash = setup.solana_client.get_latest_blockhash().unwrap(); From ed330639d87ecec2e8db173173ffebe8c31ebda8 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 8 May 2025 09:29:14 +0200 Subject: [PATCH 18/42] XC-326: rename max_num_slots for max_length --- canister/sol_rpc_canister.did | 4 ++-- canister/src/rpc_client/mod.rs | 2 +- canister/src/rpc_client/sol_rpc/mod.rs | 9 ++++----- canister/src/rpc_client/sol_rpc/tests.rs | 20 +++++++++---------- .../tests/solana_test_validator.rs | 2 +- integration_tests/tests/tests.rs | 2 +- libs/client/src/request/mod.rs | 6 +++--- libs/types/src/rpc_client/mod.rs | 4 ++-- 8 files changed, 24 insertions(+), 25 deletions(-) diff --git a/canister/sol_rpc_canister.did b/canister/sol_rpc_canister.did index d4b3be3c..c6452584 100644 --- a/canister/sol_rpc_canister.did +++ b/canister/sol_rpc_canister.did @@ -62,8 +62,8 @@ type GetRecentPrioritizationFeesRpcConfig = record { responseSizeEstimate : opt nat64; responseConsensus : opt ConsensusStrategy; maxSlotRoundingError : opt RoundingError; - // The number of slots to look back to calculate the estimate. Valid numbers are 1-150, default is 100 - maxNumSlots : opt nat8; + // The number of slots to look back to calculate the estimate. Valid numbers are 1-150, default is 100. + maxLength : opt nat8; }; // Defines a consensus strategy for combining responses from different providers. diff --git a/canister/src/rpc_client/mod.rs b/canister/src/rpc_client/mod.rs index 76a0c830..2a795f54 100644 --- a/canister/src/rpc_client/mod.rs +++ b/canister/src/rpc_client/mod.rs @@ -218,7 +218,7 @@ impl GetRecentPrioritizationFeesRequest { .max_slot_rounding_error .map(RoundingError::new) .unwrap_or_default(), - max_num_slots: config.max_num_slots.unwrap_or(100), + max_length: config.max_length.unwrap_or(100), }, ReductionStrategy::from(consensus_strategy), )) diff --git a/canister/src/rpc_client/sol_rpc/mod.rs b/canister/src/rpc_client/sol_rpc/mod.rs index e4649ed3..f57b8869 100644 --- a/canister/src/rpc_client/sol_rpc/mod.rs +++ b/canister/src/rpc_client/sol_rpc/mod.rs @@ -31,8 +31,7 @@ pub enum ResponseTransform { #[n(0)] max_slot_rounding_error: RoundingError, #[n(1)] - // TODO XC-326: rename to max_length - max_num_slots: u8, + max_length: u8, }, #[n(4)] GetSlot(#[n(0)] RoundingError), @@ -80,7 +79,7 @@ impl ResponseTransform { } Self::GetRecentPrioritizationFees { max_slot_rounding_error, - max_num_slots, + max_length, } => { if let Ok(response) = from_slice::>>(body_bytes) @@ -94,7 +93,7 @@ impl ResponseTransform { // "Currently, a node's prioritization-fee cache stores data from up to 150 blocks." // Manual testing shows that the result seems to always contain 150 elements, // also for not used addresses. - if fees.is_empty() || max_num_slots == &0 { + if fees.is_empty() || max_length == &0 { fees.clear(); } else { // The order of the prioritization fees in the response is not specified in the @@ -115,7 +114,7 @@ impl ResponseTransform { fees = fees .into_iter() .skip_while(|fee| fee.slot > max_rounded_slot) - .take(*max_num_slots as usize) + .take(*max_length as usize) .collect(); fees = fees.into_iter().rev().collect(); diff --git a/canister/src/rpc_client/sol_rpc/tests.rs b/canister/src/rpc_client/sol_rpc/tests.rs index f2491902..04ad8c93 100644 --- a/canister/src/rpc_client/sol_rpc/tests.rs +++ b/canister/src/rpc_client/sol_rpc/tests.rs @@ -345,28 +345,28 @@ mod get_recent_prioritization_fees { ( ResponseTransform::GetRecentPrioritizationFees { max_slot_rounding_error: RoundingError::new(2), - max_num_slots: 2, + max_length: 2, }, prioritization_fees(vec![3, 4]), ), ( ResponseTransform::GetRecentPrioritizationFees { max_slot_rounding_error: RoundingError::new(2), - max_num_slots: 0, + max_length: 0, }, prioritization_fees(vec![]), ), ( ResponseTransform::GetRecentPrioritizationFees { max_slot_rounding_error: RoundingError::new(2), - max_num_slots: u8::MAX, + max_length: u8::MAX, }, prioritization_fees(vec![1, 2, 3, 4]), ), ( ResponseTransform::GetRecentPrioritizationFees { max_slot_rounding_error: RoundingError::new(10), - max_num_slots: 2, + max_length: 2, }, prioritization_fees(vec![]), ), @@ -385,7 +385,7 @@ mod get_recent_prioritization_fees { let raw_response = json_response::(&[]); let transform = ResponseTransform::GetRecentPrioritizationFees { max_slot_rounding_error: RoundingError::new(2), - max_num_slots: 2, + max_length: 2, }; let original_bytes = serde_json::to_vec(&raw_response).unwrap(); let mut transformed_bytes = original_bytes.clone(); @@ -416,7 +416,7 @@ mod get_recent_prioritization_fees { let transform = ResponseTransform::GetRecentPrioritizationFees { max_slot_rounding_error: RoundingError::new(10), - max_num_slots: 100, + max_length: 100, }; let mut raw_bytes = serde_json::to_vec(&json_response(&fees)).unwrap(); transform.apply(&mut raw_bytes); @@ -444,7 +444,7 @@ mod get_recent_prioritization_fees { let transform = ResponseTransform::GetRecentPrioritizationFees { max_slot_rounding_error: RoundingError::new(10), - max_num_slots: 100, + max_length: 100, }; let mut raw_bytes = serde_json::to_vec(&json_response(&fees)).unwrap(); transform.apply(&mut raw_bytes); @@ -458,7 +458,7 @@ mod get_recent_prioritization_fees { fn should_be_nop_when_failed_to_deserialize(original_bytes in prop::collection::vec(any::(), 0..1000)) { let transform = ResponseTransform::GetRecentPrioritizationFees { max_slot_rounding_error: RoundingError::new(2), - max_num_slots: 2, + max_length: 2, }; let mut transformed_bytes = original_bytes.clone(); transform.apply(&mut transformed_bytes); @@ -471,7 +471,7 @@ mod get_recent_prioritization_fees { let raw_response = json_response(&fees); let transform = ResponseTransform::GetRecentPrioritizationFees { max_slot_rounding_error: RoundingError::new(20), - max_num_slots: 100, + max_length: 100, }; let mut raw_bytes = serde_json::to_vec(&raw_response).unwrap(); transform.apply(&mut raw_bytes); @@ -503,7 +503,7 @@ mod get_recent_prioritization_fees { }; let transform = ResponseTransform::GetRecentPrioritizationFees { max_slot_rounding_error: RoundingError::new(20), - max_num_slots: 100, + max_length: 100, }; let fees_bytes = { diff --git a/integration_tests/tests/solana_test_validator.rs b/integration_tests/tests/solana_test_validator.rs index a28b31a2..30fa01f6 100644 --- a/integration_tests/tests/solana_test_validator.rs +++ b/integration_tests/tests/solana_test_validator.rs @@ -118,7 +118,7 @@ async fn should_get_recent_prioritization_fees() { |ic| async move { ic.get_recent_prioritization_fees() .for_writable_accounts(vec![account]) - .with_max_num_slots(150) + .with_max_length(150) .with_max_slot_rounding_error(1) .send() .await diff --git a/integration_tests/tests/tests.rs b/integration_tests/tests/tests.rs index 4b7e8fe6..537d9d0d 100644 --- a/integration_tests/tests/tests.rs +++ b/integration_tests/tests/tests.rs @@ -1108,7 +1108,7 @@ mod get_recent_prioritization_fees_tests { .get_recent_prioritization_fees() .for_writable_accounts([USDC_PUBLIC_KEY]) .with_max_slot_rounding_error(10) - .with_max_num_slots(5) + .with_max_length(5) .send() .await .expect_consistent(); diff --git a/libs/client/src/request/mod.rs b/libs/client/src/request/mod.rs index 30d980f7..9dbf7ea4 100644 --- a/libs/client/src/request/mod.rs +++ b/libs/client/src/request/mod.rs @@ -484,10 +484,10 @@ impl self } - /// Change the maximum number of slots for a `getRecentPrioritizationFees` request. - pub fn with_max_num_slots(mut self, num_slots: u8) -> Self { + /// Change the maximum number of entries for a `getRecentPrioritizationFees` response. + pub fn with_max_length(mut self, len: u8) -> Self { let config = self.request.rpc_config_mut().get_or_insert_default(); - config.max_num_slots = Some(num_slots); + config.max_length = Some(len); self } } diff --git a/libs/types/src/rpc_client/mod.rs b/libs/types/src/rpc_client/mod.rs index 2eaf471c..8e9851e8 100644 --- a/libs/types/src/rpc_client/mod.rs +++ b/libs/types/src/rpc_client/mod.rs @@ -157,8 +157,8 @@ pub struct GetRecentPrioritizationFeesRpcConfig { pub max_slot_rounding_error: Option, /// TODO - #[serde(rename = "maxNumSlots")] - pub max_num_slots: Option, + #[serde(rename = "maxLength")] + pub max_length: Option, } impl From for GetRecentPrioritizationFeesRpcConfig { From 60526810fc7c10cc1823720750dcbc9b38210913 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 8 May 2025 12:48:07 +0200 Subject: [PATCH 19/42] XC-326: add Debug to RequestBuilder --- libs/client/src/lib.rs | 1 + libs/client/src/request/mod.rs | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index fd84eb3c..3cfb91d7 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -186,6 +186,7 @@ pub trait Runtime { } /// Client to interact with the SOL RPC canister. +#[derive(Debug)] pub struct SolRpcClient { config: Arc>, } diff --git a/libs/client/src/request/mod.rs b/libs/client/src/request/mod.rs index 9dbf7ea4..a797c1c1 100644 --- a/libs/client/src/request/mod.rs +++ b/libs/client/src/request/mod.rs @@ -366,6 +366,18 @@ impl Clone } } +impl Debug + for RequestBuilder +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let RequestBuilder { client, request } = &self; + f.debug_struct("RequestBuilder") + .field("client", client) + .field("request", request) + .finish() + } +} + impl RequestBuilder { From 23d2b923ffba6f779439722a10e29c495038d38e Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 8 May 2025 12:49:38 +0200 Subject: [PATCH 20/42] XC-326: refactor to use json type and add params to client get_recent_prioritization_fees --- Cargo.lock | 1 + canister/src/rpc_client/json/mod.rs | 19 ++++++- canister/src/rpc_client/mod.rs | 10 ++-- canister/src/rpc_client/sol_rpc/mod.rs | 4 +- .../tests/solana_test_validator.rs | 4 +- integration_tests/tests/tests.rs | 14 ++--- libs/client/Cargo.toml | 5 +- libs/client/src/lib.rs | 55 ++++++++++++++++--- libs/client/src/request/mod.rs | 29 +++------- libs/types/src/solana/request/mod.rs | 14 ++--- 10 files changed, 101 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 053c0006..6ccf1c8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4141,6 +4141,7 @@ dependencies = [ name = "sol_rpc_client" version = "0.1.0" dependencies = [ + "assert_matches", "async-trait", "candid", "ic-cdk", diff --git a/canister/src/rpc_client/json/mod.rs b/canister/src/rpc_client/json/mod.rs index 88615df7..5f2d99ce 100644 --- a/canister/src/rpc_client/json/mod.rs +++ b/canister/src/rpc_client/json/mod.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use sol_rpc_types::{ CommitmentLevel, DataSlice, GetAccountInfoEncoding, GetBlockCommitmentLevel, - GetTransactionEncoding, SendTransactionEncoding, Slot, TransactionDetails, + GetTransactionEncoding, Pubkey, SendTransactionEncoding, Slot, TransactionDetails, }; use solana_transaction_status_client_types::UiTransactionEncoding; @@ -163,6 +163,23 @@ pub struct GetBlockConfig { pub max_supported_transaction_version: Option, } +#[skip_serializing_none] +#[derive(Serialize, Clone, Debug)] +#[serde(into = "[Vec; 1]")] +pub struct GetRecentPrioritizationFeesParams(Vec); + +impl From for [Vec; 1] { + fn from(value: GetRecentPrioritizationFeesParams) -> Self { + [value.0] + } +} + +impl From for GetRecentPrioritizationFeesParams { + fn from(value: sol_rpc_types::GetRecentPrioritizationFeesParams) -> Self { + Self(value.into()) + } +} + #[skip_serializing_none] #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(into = "(String, Option)")] diff --git a/canister/src/rpc_client/mod.rs b/canister/src/rpc_client/mod.rs index 2a795f54..312ba579 100644 --- a/canister/src/rpc_client/mod.rs +++ b/canister/src/rpc_client/mod.rs @@ -25,9 +25,9 @@ use ic_cdk::api::management_canister::http_request::{ }; use serde::{de::DeserializeOwned, Serialize}; use sol_rpc_types::{ - ConsensusStrategy, GetRecentPrioritizationFeesParams, GetRecentPrioritizationFeesRpcConfig, - GetSlotRpcConfig, Lamport, PrioritizationFee, ProviderError, RpcConfig, RpcError, RpcResult, - RpcSource, RpcSources, Signature, TransactionDetails, + ConsensusStrategy, GetRecentPrioritizationFeesRpcConfig, GetSlotRpcConfig, Lamport, + PrioritizationFee, ProviderError, RpcConfig, RpcError, RpcResult, RpcSource, RpcSources, + Signature, TransactionDetails, }; use solana_clock::Slot; use std::{fmt::Debug, marker::PhantomData}; @@ -195,10 +195,10 @@ impl GetSlotRequest { } pub type GetRecentPrioritizationFeesRequest = - MultiRpcRequest>; + MultiRpcRequest>; impl GetRecentPrioritizationFeesRequest { - pub fn get_recent_prioritization_fees>( + pub fn get_recent_prioritization_fees>( rpc_sources: RpcSources, config: GetRecentPrioritizationFeesRpcConfig, params: Params, diff --git a/canister/src/rpc_client/sol_rpc/mod.rs b/canister/src/rpc_client/sol_rpc/mod.rs index f57b8869..48bfdb15 100644 --- a/canister/src/rpc_client/sol_rpc/mod.rs +++ b/canister/src/rpc_client/sol_rpc/mod.rs @@ -91,8 +91,8 @@ impl ResponseTransform { // [API](https://solana.com/de/docs/rpc/http/getrecentprioritizationfees), // which simply mentions // "Currently, a node's prioritization-fee cache stores data from up to 150 blocks." - // Manual testing shows that the result seems to always contain 150 elements, - // also for not used addresses. + // Manual testing shows that the result seems to always contain 150 elements on mainnet (also for not used addresses) + // but not necessarily when using a local validator. if fees.is_empty() || max_length == &0 { fees.clear(); } else { diff --git a/integration_tests/tests/solana_test_validator.rs b/integration_tests/tests/solana_test_validator.rs index 30fa01f6..2c874c63 100644 --- a/integration_tests/tests/solana_test_validator.rs +++ b/integration_tests/tests/solana_test_validator.rs @@ -116,8 +116,8 @@ async fn should_get_recent_prioritization_fees() { .expect("Failed to get recent prioritization fees") }, |ic| async move { - ic.get_recent_prioritization_fees() - .for_writable_accounts(vec![account]) + ic.get_recent_prioritization_fees(&[account]) + .unwrap() .with_max_length(150) .with_max_slot_rounding_error(1) .send() diff --git a/integration_tests/tests/tests.rs b/integration_tests/tests/tests.rs index 537d9d0d..30777006 100644 --- a/integration_tests/tests/tests.rs +++ b/integration_tests/tests/tests.rs @@ -1105,8 +1105,8 @@ mod get_recent_prioritization_fees_tests { MockOutcallBuilder::new(200, response_body(2)).with_request_body(request_body(2)), ]) .build() - .get_recent_prioritization_fees() - .for_writable_accounts([USDC_PUBLIC_KEY]) + .get_recent_prioritization_fees(&[USDC_PUBLIC_KEY]) + .unwrap() .with_max_slot_rounding_error(10) .with_max_length(5) .send() @@ -1161,8 +1161,8 @@ mod get_recent_prioritization_fees_tests { let _ = client .build() - .get_recent_prioritization_fees() - .for_writable_accounts(too_many_accounts) + .get_recent_prioritization_fees(&too_many_accounts) + .unwrap() .send() .await; } @@ -1516,7 +1516,7 @@ mod cycles_cost_tests { check(client.get_block(577996)).await; } SolRpcEndpoint::GetRecentPrioritizationFees => { - check(client.get_recent_prioritization_fees()).await + check(client.get_recent_prioritization_fees(&[]).unwrap()).await } SolRpcEndpoint::GetTransaction => { check(client.get_transaction(some_signature())).await; @@ -1569,7 +1569,7 @@ mod cycles_cost_tests { check(client.get_block(577996)).await; } SolRpcEndpoint::GetRecentPrioritizationFees => { - check(client.get_recent_prioritization_fees()).await; + check(client.get_recent_prioritization_fees(&[]).unwrap()).await; } SolRpcEndpoint::GetTokenAccountBalance => { check(client.get_token_account_balance(USDC_PUBLIC_KEY)).await; @@ -1683,7 +1683,7 @@ mod cycles_cost_tests { SolRpcEndpoint::GetRecentPrioritizationFees => { check( &setup, - client.get_recent_prioritization_fees(), + client.get_recent_prioritization_fees(&[]).unwrap(), 2_378_204_800, ) .await; diff --git a/libs/client/Cargo.toml b/libs/client/Cargo.toml index e0e63f1d..ceaa06f9 100644 --- a/libs/client/Cargo.toml +++ b/libs/client/Cargo.toml @@ -23,7 +23,8 @@ solana-pubkey = { workspace = true } solana-signature = { workspace = true } solana-transaction-status-client-types = { workspace = true } sol_rpc_types = { version = "0.1.0", path = "../types" } -strum = {workspace = true} +strum = { workspace = true } [dev-dependencies] -tokio = {workspace = true, features = ["full"]} +assert_matches = { workspace = true } +tokio = { workspace = true, features = ["full"] } diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index 3cfb91d7..92f84a47 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -135,10 +135,11 @@ use candid::{utils::ArgumentEncoder, CandidType, Principal}; use ic_cdk::api::call::RejectionCode; use serde::de::DeserializeOwned; use sol_rpc_types::{ - CommitmentLevel, GetAccountInfoParams, GetBalanceParams, GetBlockParams, GetSlotParams, - GetSlotRpcConfig, GetTokenAccountBalanceParams, GetTransactionParams, Lamport, RpcConfig, - RpcSources, SendTransactionParams, Signature, SolanaCluster, SupportedRpcProvider, - SupportedRpcProviderId, TokenAmount, TransactionDetails, TransactionInfo, + CommitmentLevel, GetAccountInfoParams, GetBalanceParams, GetBlockParams, + GetRecentPrioritizationFeesParams, GetSlotParams, GetSlotRpcConfig, + GetTokenAccountBalanceParams, GetTransactionParams, Lamport, RpcConfig, RpcError, RpcSources, + SendTransactionParams, Signature, SolanaCluster, SupportedRpcProvider, SupportedRpcProviderId, + TokenAmount, TransactionDetails, TransactionInfo, }; use solana_account_decoder_client_types::token::UiTokenAmount; use solana_clock::Slot; @@ -458,13 +459,51 @@ impl SolRpcClient { /// /// # Examples /// + /// Too many keys + /// + /// ```rust + /// + /// use std::collections::BTreeSet; + /// use assert_matches::assert_matches; + /// use solana_pubkey::Pubkey; + /// use sol_rpc_client::SolRpcClient; + /// use sol_rpc_types::{RpcSources, SolanaCluster, RpcError}; + /// + /// let client = SolRpcClient::builder_for_ic() + /// .with_rpc_sources(RpcSources::Default(SolanaCluster::Mainnet)) + /// .build(); + /// + /// let mut too_many_accounts = BTreeSet::new(); + /// for i in 0..129_u8 { + /// let mut key = [0_u8; 32]; + /// key[0] = i; + /// too_many_accounts.insert(Pubkey::from(key)); + /// } + /// assert_eq!(too_many_accounts.len(), 129); + /// + /// let err = client.get_recent_prioritization_fees(&too_many_accounts).unwrap_err(); + /// assert_matches!(err, RpcError::ValidationError(_)); + /// ``` + /// /// TODO XC-326: rust example - pub fn get_recent_prioritization_fees(&self) -> GetRecentPrioritizationFeesRequestBuilder { - RequestBuilder::new( + pub fn get_recent_prioritization_fees<'a, I>( + &self, + addresses: I, + ) -> Result, RpcError> + where + I: IntoIterator, + { + let params = GetRecentPrioritizationFeesParams::try_from( + addresses + .into_iter() + .map(|a| a.to_string()) + .collect::>(), + )?; + Ok(RequestBuilder::new( self.clone(), - GetRecentPrioritizationFeesRequest::default(), + GetRecentPrioritizationFeesRequest::from(params), 10_000_000_000, - ) + )) } /// Call `getSlot` on the SOL RPC canister. diff --git a/libs/client/src/request/mod.rs b/libs/client/src/request/mod.rs index a797c1c1..fab56d51 100644 --- a/libs/client/src/request/mod.rs +++ b/libs/client/src/request/mod.rs @@ -185,11 +185,11 @@ impl SolRpcRequest for GetBlockRequest { } #[derive(Debug, Clone, Default)] -pub struct GetRecentPrioritizationFeesRequest(Option); +pub struct GetRecentPrioritizationFeesRequest(GetRecentPrioritizationFeesParams); impl SolRpcRequest for GetRecentPrioritizationFeesRequest { type Config = GetRecentPrioritizationFeesRpcConfig; - type Params = Option; + type Params = GetRecentPrioritizationFeesParams; type CandidOutput = sol_rpc_types::MultiRpcResult>; type Output = Self::CandidOutput; @@ -204,6 +204,12 @@ impl SolRpcRequest for GetRecentPrioritizationFeesRequest { } } +impl From for GetRecentPrioritizationFeesRequest { + fn from(value: GetRecentPrioritizationFeesParams) -> Self { + Self(value) + } +} + #[derive(Debug, Clone, Default)] pub struct GetSlotRequest(Option); @@ -350,7 +356,7 @@ pub struct RequestBuilder { pub type GetRecentPrioritizationFeesRequestBuilder = RequestBuilder< R, GetRecentPrioritizationFeesRpcConfig, - Option, + GetRecentPrioritizationFeesParams, sol_rpc_types::MultiRpcResult>, sol_rpc_types::MultiRpcResult>, >; @@ -469,23 +475,6 @@ impl } } -impl - RequestBuilder, CandidOutput, Output> -{ - /// Add an account to look up for a `getRecentPrioritizationFees` request. - /// - /// The response to a `getRecentPrioritizationFees` request reflects a fee to land - /// a transaction locking all of the provided accounts as writable. - pub fn for_writable_accounts(mut self, accounts: I) -> Self - where - I: IntoIterator, - { - let params = self.request.params_mut().get_or_insert_default(); - params.0.extend(accounts.into_iter().map(|a| a.to_string())); - self - } -} - impl RequestBuilder { diff --git a/libs/types/src/solana/request/mod.rs b/libs/types/src/solana/request/mod.rs index d8b61320..6f030adf 100644 --- a/libs/types/src/solana/request/mod.rs +++ b/libs/types/src/solana/request/mod.rs @@ -175,19 +175,19 @@ pub enum TransactionDetails { /// The parameters for a Solana [`getRecentPrioritizationFees`](https://solana.com/de/docs/rpc/http/getrecentprioritizationfees) RPC method call. #[derive(Debug, Clone, Default, Deserialize, Serialize, CandidType)] -#[serde(try_from = "Vec", into = "[Vec; 1]")] -pub struct GetRecentPrioritizationFeesParams(pub Vec); +#[serde(try_from = "Vec")] +pub struct GetRecentPrioritizationFeesParams(Vec); impl TryFrom> for GetRecentPrioritizationFeesParams { - type Error = String; + type Error = RpcError; fn try_from(accounts: Vec) -> Result { const MAX_NUM_ACCOUNTS: usize = 128; if accounts.len() > MAX_NUM_ACCOUNTS { - return Err(format!( + return Err(RpcError::ValidationError(format!( "Expected at most {MAX_NUM_ACCOUNTS} account addresses, but got {}", accounts.len() - )); + ))); } Ok(Self(accounts)) } @@ -199,9 +199,9 @@ impl From for GetRecentPrioritizationFeesParams { } } -impl From for [Vec; 1] { +impl From for Vec { fn from(value: GetRecentPrioritizationFeesParams) -> Self { - [value.0] + value.0 } } From 777ce53975a0cd9bb785200e62dd6c0ee0233acc Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 8 May 2025 13:28:19 +0200 Subject: [PATCH 21/42] XC-326: Rust doc --- libs/client/src/lib.rs | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index 92f84a47..21abefbb 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -459,7 +459,42 @@ impl SolRpcClient { /// /// # Examples /// - /// Too many keys + /// ```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, PrioritizationFee, TokenAmount}; + /// let client = SolRpcClient::builder_for_ic() + /// # .with_mocked_response(MultiRpcResult::Consistent(Ok(vec![PrioritizationFee{slot: 338637772, prioritization_fee: 166667}]))) + /// .with_rpc_sources(RpcSources::Default(SolanaCluster::Mainnet)) + /// .build(); + /// + /// let fees = client + /// .get_recent_prioritization_fees(&[pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")]) + /// .unwrap() + /// .with_max_length(1) + /// .send() + /// .await + /// .expect_consistent(); + /// + /// assert_eq! + /// (fees, + /// Ok(vec![ PrioritizationFee { + /// slot: 338637772, + /// prioritization_fee: 166667 + /// }])); + /// # Ok(()) + /// # } + /// ``` + /// + /// # Errors + /// + /// The number of account addresses that can be passed to + /// [`getRecentPrioritizationFees`](https://solana.com/de/docs/rpc/http/getrecentprioritizationfees) + /// is limited to 128. More accounts result in an error. /// /// ```rust /// @@ -484,8 +519,6 @@ impl SolRpcClient { /// let err = client.get_recent_prioritization_fees(&too_many_accounts).unwrap_err(); /// assert_matches!(err, RpcError::ValidationError(_)); /// ``` - /// - /// TODO XC-326: rust example pub fn get_recent_prioritization_fees<'a, I>( &self, addresses: I, From 454a9a52f164846d3b88e2eeb03f16b132b2c268 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 8 May 2025 17:28:14 +0200 Subject: [PATCH 22/42] XC-326: docs --- canister/sol_rpc_canister.did | 17 ++++++++++++- libs/types/src/rpc_client/mod.rs | 43 +++++++++++++++++++++++++++++--- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/canister/sol_rpc_canister.did b/canister/sol_rpc_canister.did index c6452584..d2002b96 100644 --- a/canister/sol_rpc_canister.did +++ b/canister/sol_rpc_canister.did @@ -58,11 +58,26 @@ type GetSlotRpcConfig = record { }; // Configures how to perform `getRecentPrioritizationFees` RPC HTTP calls. +// +// The response to `getRecentPrioritizationFees` corresponds to a (non-necessarily continuous) range of slots associated +// with the priority fee for that slot and may include `processed` slots (a new `processed` slot is produced every ca. 400ms). +// Similarly to the necessary rounding used for `getSlot`, +// achieving consensus for `getRecentPrioritizationFees` requires to select a subset of those slots +// that can be seen my a super-majority of the nodes, which is done as follows: +// 1) `maxSlotRoundingError`: round down the slot with the maximum value. +// The selected subset will only contain priority fees for slots that are smaller or equal to the rounded down slot. +// 2) `maxLength`: limit the size of the selected subset by removing priority fees for the older slots (lower values). type GetRecentPrioritizationFeesRpcConfig = record { responseSizeEstimate : opt nat64; responseConsensus : opt ConsensusStrategy; + // Round down the slot with the maximum value. + // Increasing that value will reduce the freshness of the returned prioritization fees + // but increase the likelihood of nodes reaching consensus. maxSlotRoundingError : opt RoundingError; - // The number of slots to look back to calculate the estimate. Valid numbers are 1-150, default is 100. + // Limit the number of returned priority fees. + // Valid numbers are 1-150, default is 100. + // Increasing that value can help in estimating the current priority fee + // but will reduce the likelihood of nodes reaching consensus. maxLength : opt nat8; }; diff --git a/libs/types/src/rpc_client/mod.rs b/libs/types/src/rpc_client/mod.rs index 8e9851e8..c77d0e74 100644 --- a/libs/types/src/rpc_client/mod.rs +++ b/libs/types/src/rpc_client/mod.rs @@ -140,6 +140,35 @@ impl From for GetSlotRpcConfig { } /// Configures how to perform HTTP calls for the Solana `getRecentPrioritizationFees` RPC method. +/// +/// The response to `getRecentPrioritizationFees` corresponds to a (non-necessarily continuous) range of slots associated +/// with the priority fee for that slot and may include `processed` slots (a new `processed` slot is produced every ca. 400ms); e.g. +/// ```json +/// ... +/// { +/// "prioritizationFee": 166667, +/// "slot": 338637772 +///}, +///{ +/// "prioritizationFee": 0, +/// "slot": 338637773 +///}, +///{ +/// "prioritizationFee": 0, +/// "slot": 338637774 +///}, +///{ +/// "prioritizationFee": 50000, +/// "slot": 338637775 +///}, +/// ... +/// ``` +/// Similarly to the necessary rounding used for `getSlot`, +/// achieving consensus for `getRecentPrioritizationFees` requires selecting a subset of those slots +/// that can be seen my a super-majority of the nodes, which is done as follows: +/// 1. `max_slot_rounding_error`: round down the slot with the maximum value. +/// The selected subset will only contain priority fees for slots that are smaller or equal to the rounded-down slot. +/// 2. `max_length`: limit the size of the selected subset by removing priority fees for the older slots. #[derive(Clone, Debug, PartialEq, Eq, Default, CandidType, Deserialize)] pub struct GetRecentPrioritizationFeesRpcConfig { /// Describes the expected (90th percentile) number of bytes in the HTTP response body. @@ -147,16 +176,22 @@ pub struct GetRecentPrioritizationFeesRpcConfig { #[serde(rename = "responseSizeEstimate")] pub response_size_estimate: Option, - /// Specifies how the responses of the different RPC providers should be aggregated into - /// a single response. + /// Round down the slot with the maximum value. + /// Increasing that value will reduce the freshness of the returned prioritization fees + /// but increase the likelihood of nodes reaching consensus. #[serde(rename = "responseConsensus")] pub response_consensus: Option, - /// TODO + /// Round down the slot with the maximum value. + /// Increasing that value will reduce the freshness of the returned prioritization fees + /// but increase the likelihood of nodes reaching consensus. #[serde(rename = "maxSlotRoundingError")] pub max_slot_rounding_error: Option, - /// TODO + /// Limit the number of returned priority fees. + /// Valid numbers are 1-150, default is 100. + /// Increasing that value can help in estimating the current priority fee + /// but will reduce the likelihood of nodes reaching consensus. #[serde(rename = "maxLength")] pub max_length: Option, } From 2a7554f2371effde82d1b4144fd401370bba237a Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 8 May 2025 17:29:48 +0200 Subject: [PATCH 23/42] XC-326: TODO doc --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6356f854..938bd87e 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,8 @@ The SOL RPC canister reaches the Solana JSON-RPC providers using [HTTPS outcalls 1. Use a [durable nonce](https://solana.com/de/developers/guides/advanced/introduction-to-durable-nonces) instead of a blockhash. 2. Retrieve a recent blockhash by first retrieving a recent slot with `getSlot` and then getting the block (which includes the blockhash) with `getBlock`. +[//]: # (TODO: XC-326: mention also `getRecenPrioritizationFees`) + ## Reproducible Build The SOL RPC canister supports [reproducible builds](https://internetcomputer.org/docs/current/developer-docs/smart-contracts/test/reproducible-builds): From fc5534f06c6d5dae9d883fb86c26548a83c401e6 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 8 May 2025 17:43:35 +0200 Subject: [PATCH 24/42] XC-326: end-to-end test --- canister/scripts/examples.sh | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/canister/scripts/examples.sh b/canister/scripts/examples.sh index 767860de..55ae37d5 100755 --- a/canister/scripts/examples.sh +++ b/canister/scripts/examples.sh @@ -25,6 +25,24 @@ CYCLES=$(dfx canister call sol_rpc getSlotCyclesCost "$GET_SLOT_PARAMS" $FLAGS - GET_SLOT_OUTPUT=$(dfx canister call sol_rpc getSlot "$GET_SLOT_PARAMS" $FLAGS --output json --with-cycles "$CYCLES" || exit 1) SLOT=$(jq --raw-output '.Consistent.Ok' <<< "$GET_SLOT_OUTPUT") + +# Get the recent prioritization fees on Mainnet with a 2-out-of-3 strategy for USDC +GET_RECENT_PRIORITIZATION_FEES_PARAMS="( + variant { Default = variant { Mainnet } }, + opt record { + responseConsensus = opt variant { + Threshold = record { min = 2 : nat8; total = opt (3 : nat8) } + }; + responseSizeEstimate = null; + maxSlotRoundingError = opt (20 : nat64); + maxLength = opt (100 : nat8); + }, + opt vec { \"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v\" }, +)" +CYCLES=$(dfx canister call sol_rpc getRecentPrioritizationFeesCyclesCost "$GET_RECENT_PRIORITIZATION_FEES_PARAMS" $FLAGS --output json | jq '.Ok' --raw-output || exit 1) +GET_RECENT_PRIORITIZATION_FEES_OUTPUT=$(dfx canister call sol_rpc getRecentPrioritizationFees "$GET_RECENT_PRIORITIZATION_FEES_PARAMS" $FLAGS --output json --with-cycles "$CYCLES" || exit 1) +GET_RECENT_PRIORITIZATION_FEES=$(jq --raw-output '.Consistent.Ok' <<< "$GET_RECENT_PRIORITIZATION_FEES_OUTPUT") + # Fetch the latest finalized block GET_BLOCK_PARAMS="( variant { Default = variant { Mainnet } }, From b1805c8719511c7c6732e10a32ef8c7c3326258c Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 8 May 2025 18:20:18 +0200 Subject: [PATCH 25/42] XC-326: micro-lamport --- canister/sol_rpc_canister.did | 6 +++++- libs/types/src/solana/mod.rs | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/canister/sol_rpc_canister.did b/canister/sol_rpc_canister.did index d2002b96..afe4fb71 100644 --- a/canister/sol_rpc_canister.did +++ b/canister/sol_rpc_canister.did @@ -339,6 +339,10 @@ type GetBalanceParams = record { // 1_000_000_000 Lamports is 1 SOL type Lamport = nat64; +// Micro-lamports are used for the calculation of prioritization fees. +// 1_000_000 MicroLamport == 1 Lamport +type MicroLamport = nat64; + // Represents an aggregated result from multiple RPC calls to the `getBalance` Solana RPC method. type MultiGetBalanceResult = variant { Consistent : GetBalanceResult; @@ -593,7 +597,7 @@ type MultiGetTransactionResult = variant { type PrioritizationFee = record { slot: Slot; - prioritizationFee: nat64 + prioritizationFee: MicroLamport }; // Represents the result of a call to the `getSlot` Solana RPC method. diff --git a/libs/types/src/solana/mod.rs b/libs/types/src/solana/mod.rs index e4953ddc..c5ccd326 100644 --- a/libs/types/src/solana/mod.rs +++ b/libs/types/src/solana/mod.rs @@ -12,6 +12,10 @@ pub type Slot = u64; /// A Solana [Lamport](https://solana.com/de/docs/references/terminology#lamport). pub type Lamport = u64; +/// Within the compute budget, a quantity of micro-lamports is used in the calculation of prioritization fees. +/// `1_000_000 MicroLamport == 1 Lamport` +pub type MicroLamport = u64; + /// A Solana base58-encoded [blockhash](https://solana.com/de/docs/references/terminology#blockhash). pub type Blockhash = String; @@ -89,5 +93,5 @@ pub struct PrioritizationFee { /// The per-compute-unit fee paid by at least one successfully landed transaction, /// specified in increments of micro-lamports (0.000001 lamports) #[serde(rename = "prioritizationFee")] - pub prioritization_fee: u64, + pub prioritization_fee: MicroLamport, } From e91f8fb47019f6d5f241686111a320e7dd3bbb7c Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 8 May 2025 18:23:06 +0200 Subject: [PATCH 26/42] XC-326: remove redundant test since already in Rust docs --- integration_tests/tests/tests.rs | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/integration_tests/tests/tests.rs b/integration_tests/tests/tests.rs index 30777006..799a7a9e 100644 --- a/integration_tests/tests/tests.rs +++ b/integration_tests/tests/tests.rs @@ -475,8 +475,6 @@ mod get_recent_prioritization_fees_tests { use sol_rpc_int_tests::mock::MockOutcallBuilder; use sol_rpc_int_tests::{Setup, SolRpcTestClient}; use sol_rpc_types::PrioritizationFee; - use solana_pubkey::Pubkey; - use std::collections::BTreeSet; #[tokio::test] async fn should_get_fees_with_rounding() { @@ -1141,31 +1139,6 @@ mod get_recent_prioritization_fees_tests { setup.drop().await; } - - #[tokio::test] - #[should_panic( - expected = "Deserialize error: Expected at most 128 account addresses, but got 129" - )] - async fn should_fail_when_requesting_too_many_accounts() { - let setup = Setup::new().await.with_mock_api_keys().await; - - let mut too_many_accounts = BTreeSet::new(); - for i in 0..129_u8 { - let mut key = [0_u8; 32]; - key[0] = i; - too_many_accounts.insert(Pubkey::from(key)); - } - assert_eq!(too_many_accounts.len(), 129); - - let client = setup.client(); - - let _ = client - .build() - .get_recent_prioritization_fees(&too_many_accounts) - .unwrap() - .send() - .await; - } } mod send_transaction_tests { From 2609a63ffa521ca0b157549e0732df2e7f2989d7 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Thu, 8 May 2025 18:23:14 +0200 Subject: [PATCH 27/42] XC-326: clean-up --- canister/src/lib.rs | 1 - libs/types/src/rpc_client/mod.rs | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/canister/src/lib.rs b/canister/src/lib.rs index f923cf20..d3b070ad 100644 --- a/canister/src/lib.rs +++ b/canister/src/lib.rs @@ -1,4 +1,3 @@ -#![recursion_limit = "512"] pub mod candid_rpc; pub mod constants; pub mod http; diff --git a/libs/types/src/rpc_client/mod.rs b/libs/types/src/rpc_client/mod.rs index c77d0e74..fb7ee49a 100644 --- a/libs/types/src/rpc_client/mod.rs +++ b/libs/types/src/rpc_client/mod.rs @@ -189,10 +189,14 @@ pub struct GetRecentPrioritizationFeesRpcConfig { pub max_slot_rounding_error: Option, /// Limit the number of returned priority fees. - /// Valid numbers are 1-150, default is 100. + /// + /// A Solana validator returns at most 150 entries, so that bigger values are possible but not useful. + /// MUST be non-zero to avoid useless call. + /// Default value is 100. /// Increasing that value can help in estimating the current priority fee /// but will reduce the likelihood of nodes reaching consensus. #[serde(rename = "maxLength")] + // TODO XC-326: Use a wrapper type to implement Candid on `NonZeroU8` to prohibit the value 0. pub max_length: Option, } From feb169d390ff3f2534b5d3f30b8d23da7f924610 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Mon, 12 May 2025 09:50:42 +0200 Subject: [PATCH 28/42] XC-326: add doc Candid PrioritizationFee --- canister/sol_rpc_canister.did | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/canister/sol_rpc_canister.did b/canister/sol_rpc_canister.did index afe4fb71..03c65e7d 100644 --- a/canister/sol_rpc_canister.did +++ b/canister/sol_rpc_canister.did @@ -595,8 +595,12 @@ type MultiGetTransactionResult = variant { Inconsistent : vec record { RpcSource; GetTransactionResult }; }; +// Prioritization fee returned by `getRecentPrioritizationFees`. type PrioritizationFee = record { + // Slot in which the fee was observed. slot: Slot; + // The per-compute-unit fee paid by at least one successfully landed transaction, + // specified in increments of micro-lamports. prioritizationFee: MicroLamport }; From 3efcf34ed4d02113da57e719a3fb9bad996961c7 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Mon, 12 May 2025 11:14:56 +0200 Subject: [PATCH 29/42] XC-326: test should_normalize_json_rpc_error --- Cargo.lock | 1 + canister/Cargo.toml | 1 + canister/src/rpc_client/sol_rpc/mod.rs | 3 ++- canister/src/rpc_client/sol_rpc/tests.rs | 25 ++++++++++++++++++++++++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 6ccf1c8e..004226c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4130,6 +4130,7 @@ dependencies = [ "solana-pubkey", "solana-signature", "solana-transaction-status-client-types", + "strum 0.27.1", "thiserror 2.0.12", "tower", "tower-http", diff --git a/canister/Cargo.toml b/canister/Cargo.toml index ff1c3ad7..7e060af2 100644 --- a/canister/Cargo.toml +++ b/canister/Cargo.toml @@ -38,6 +38,7 @@ serde_bytes = { workspace = true } solana-account = { workspace = true, features = ["serde"] } solana-account-decoder-client-types = { workspace = true } solana-transaction-status-client-types = { workspace = true } +strum = {workspace = true} tower = { workspace = true } tower-http = { workspace = true, features = ["set-header", "util"] } url = { workspace = true } diff --git a/canister/src/rpc_client/sol_rpc/mod.rs b/canister/src/rpc_client/sol_rpc/mod.rs index 48bfdb15..9380a2f8 100644 --- a/canister/src/rpc_client/sol_rpc/mod.rs +++ b/canister/src/rpc_client/sol_rpc/mod.rs @@ -14,11 +14,12 @@ use serde_json::{from_slice, to_vec, Value}; use sol_rpc_types::PrioritizationFee; use solana_clock::Slot; use std::fmt::Debug; +use strum::EnumIter; /// Describes a payload transformation to execute before passing the HTTP response to consensus. /// The purpose of these transformations is to ensure that the response encoding is deterministic /// (the field order is the same). -#[derive(Clone, Debug, Decode, Encode)] +#[derive(Clone, Debug, Decode, Encode, EnumIter)] pub enum ResponseTransform { #[n(0)] GetAccountInfo, diff --git a/canister/src/rpc_client/sol_rpc/tests.rs b/canister/src/rpc_client/sol_rpc/tests.rs index 04ad8c93..e928d81b 100644 --- a/canister/src/rpc_client/sol_rpc/tests.rs +++ b/canister/src/rpc_client/sol_rpc/tests.rs @@ -5,6 +5,7 @@ use serde_json::{from_slice, json, to_vec, Value}; mod normalization_tests { use super::*; + use strum::IntoEnumIterator; #[test] fn should_normalize_raw_response() { @@ -271,6 +272,30 @@ mod normalization_tests { ); } + #[test] + fn should_normalize_json_rpc_error() { + fn normalize_json(transform: &ResponseTransform, response: &str) -> Vec { + let mut bytes = response.bytes().collect(); + transform.apply(&mut bytes); + bytes + } + + for transform in ResponseTransform::iter() { + let left = r#"{ "jsonrpc": "2.0", "error": { "code": -32602, "message": "Invalid param: could not find account" }, "id": 1 }"#; + let right = r#"{ "error": { "message": "Invalid param: could not find account", "code": -32602 }, "id": 1, "jsonrpc": "2.0" }"#; + let normalized_left = normalize_json(&transform, left); + let normalized_right = normalize_json(&transform, right); + + assert_eq!(normalized_left, normalized_right); + assert_eq!( + serde_json::from_slice::(&normalized_left).unwrap(), + json!( + { "jsonrpc": "2.0", "error": { "code": -32602, "message": "Invalid param: could not find account" }, "id": 1 } + ) + ); + } + } + 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); From 1c6a71fbc5914634dd29d0fdf3aa51195df03a21 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Mon, 12 May 2025 11:20:15 +0200 Subject: [PATCH 30/42] XC-326: variable rename --- canister/src/rpc_client/sol_rpc/tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/canister/src/rpc_client/sol_rpc/tests.rs b/canister/src/rpc_client/sol_rpc/tests.rs index e928d81b..3e1854e7 100644 --- a/canister/src/rpc_client/sol_rpc/tests.rs +++ b/canister/src/rpc_client/sol_rpc/tests.rs @@ -531,7 +531,7 @@ mod get_recent_prioritization_fees { max_length: 100, }; - let fees_bytes = { + let sorted_fees_bytes = { let raw_response = json_response(&fees); let mut raw_bytes = serde_json::to_vec(&raw_response).unwrap(); transform.apply(&mut raw_bytes); @@ -545,7 +545,7 @@ mod get_recent_prioritization_fees { raw_bytes }; - assert_eq!(fees_bytes, shuffled_fees_bytes); + assert_eq!(sorted_fees_bytes, shuffled_fees_bytes); } } From fc041b0622f9eb0dfe8f6fac54c4b5931b73998e Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Mon, 12 May 2025 11:21:27 +0200 Subject: [PATCH 31/42] XC-326: fix indenting --- canister/src/rpc_client/sol_rpc/tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/canister/src/rpc_client/sol_rpc/tests.rs b/canister/src/rpc_client/sol_rpc/tests.rs index 3e1854e7..5e51584c 100644 --- a/canister/src/rpc_client/sol_rpc/tests.rs +++ b/canister/src/rpc_client/sol_rpc/tests.rs @@ -532,14 +532,14 @@ mod get_recent_prioritization_fees { }; let sorted_fees_bytes = { - let raw_response = json_response(&fees); + let raw_response = json_response(&fees); let mut raw_bytes = serde_json::to_vec(&raw_response).unwrap(); transform.apply(&mut raw_bytes); raw_bytes }; let shuffled_fees_bytes = { - let raw_response = json_response(&shuffled_fees); + let raw_response = json_response(&shuffled_fees); let mut raw_bytes = serde_json::to_vec(&raw_response).unwrap(); transform.apply(&mut raw_bytes); raw_bytes From ccd4c64659cf8137b030fb60b8b240da8c28722b Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Mon, 12 May 2025 11:23:17 +0200 Subject: [PATCH 32/42] XC-326: formatting --- libs/client/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index 21abefbb..3c1672fe 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -497,7 +497,6 @@ impl SolRpcClient { /// is limited to 128. More accounts result in an error. /// /// ```rust - /// /// use std::collections::BTreeSet; /// use assert_matches::assert_matches; /// use solana_pubkey::Pubkey; From 792bba25093d2450d5d9d076e3a44eb47dc1a940 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Mon, 12 May 2025 11:25:28 +0200 Subject: [PATCH 33/42] XC-326: simplify Rust doc test --- libs/client/src/lib.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index 3c1672fe..11eb97cd 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -507,12 +507,9 @@ impl SolRpcClient { /// .with_rpc_sources(RpcSources::Default(SolanaCluster::Mainnet)) /// .build(); /// - /// let mut too_many_accounts = BTreeSet::new(); - /// for i in 0..129_u8 { - /// let mut key = [0_u8; 32]; - /// key[0] = i; - /// too_many_accounts.insert(Pubkey::from(key)); - /// } + /// let too_many_accounts: BTreeSet = (0..129_u8) + /// .map(|i| Pubkey::from([i; 32])) + /// .collect(); /// assert_eq!(too_many_accounts.len(), 129); /// /// let err = client.get_recent_prioritization_fees(&too_many_accounts).unwrap_err(); From 203e8305a254ce61da7ad8041ebe516840909386 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Mon, 12 May 2025 11:43:44 +0200 Subject: [PATCH 34/42] XC-326: typo. --- libs/types/src/rpc_client/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/types/src/rpc_client/mod.rs b/libs/types/src/rpc_client/mod.rs index fb7ee49a..00f84aa1 100644 --- a/libs/types/src/rpc_client/mod.rs +++ b/libs/types/src/rpc_client/mod.rs @@ -165,7 +165,7 @@ impl From for GetSlotRpcConfig { /// ``` /// Similarly to the necessary rounding used for `getSlot`, /// achieving consensus for `getRecentPrioritizationFees` requires selecting a subset of those slots -/// that can be seen my a super-majority of the nodes, which is done as follows: +/// that can be seen by a super-majority of the nodes, which is done as follows: /// 1. `max_slot_rounding_error`: round down the slot with the maximum value. /// The selected subset will only contain priority fees for slots that are smaller or equal to the rounded-down slot. /// 2. `max_length`: limit the size of the selected subset by removing priority fees for the older slots. From 6348bf8d4e09ed65e0060eecede0dcd10d818c2b Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Mon, 12 May 2025 11:44:27 +0200 Subject: [PATCH 35/42] XC-326: use Slot type in PrioritizationFee. --- libs/types/src/solana/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/types/src/solana/mod.rs b/libs/types/src/solana/mod.rs index c5ccd326..36432b93 100644 --- a/libs/types/src/solana/mod.rs +++ b/libs/types/src/solana/mod.rs @@ -89,7 +89,7 @@ impl From for solana_transaction_status_client_types::UiConfirme #[derive(Debug, Clone, Deserialize, Serialize, CandidType, PartialEq)] pub struct PrioritizationFee { /// Slot in which the fee was observed. - pub slot: u64, + pub slot: Slot, /// The per-compute-unit fee paid by at least one successfully landed transaction, /// specified in increments of micro-lamports (0.000001 lamports) #[serde(rename = "prioritizationFee")] From fa4ecf1ebbd507c0e7feb0f9a2e5ebb52b0d18e3 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Mon, 12 May 2025 12:01:32 +0200 Subject: [PATCH 36/42] XC-326: serialize_if_ok. --- canister/src/rpc_client/sol_rpc/mod.rs | 34 +++++++++++++------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/canister/src/rpc_client/sol_rpc/mod.rs b/canister/src/rpc_client/sol_rpc/mod.rs index 9380a2f8..b0836e9d 100644 --- a/canister/src/rpc_client/sol_rpc/mod.rs +++ b/canister/src/rpc_client/sol_rpc/mod.rs @@ -48,15 +48,21 @@ pub enum ResponseTransform { impl ResponseTransform { fn apply(&self, body_bytes: &mut Vec) { + fn serialize_if_ok(body_bytes: &mut Vec, response: &JsonRpcResponse) + where + T: Serialize, + { + if let Ok(bytes) = serde_json::to_vec(response) { + *body_bytes = bytes + } + } fn canonicalize_response(body_bytes: &mut Vec, f: impl FnOnce(T) -> R) where T: Serialize + DeserializeOwned, R: Serialize + DeserializeOwned, { if let Ok(response) = from_slice::>(body_bytes) { - if let Ok(bytes) = to_vec(&response.map(f)) { - *body_bytes = bytes - } + serialize_if_ok(body_bytes, &response.map(f)) } } @@ -120,22 +126,16 @@ impl ResponseTransform { fees = fees.into_iter().rev().collect(); } - - *body_bytes = serde_json::to_vec(&JsonRpcResponse::from_ok(id, fees)) - .expect( - "BUG: failed to serialize previously deserialized JsonRpcResponse", - ); + serialize_if_ok(body_bytes, &JsonRpcResponse::from_ok(id, fees)); } Err(json_rpc_error) => { - // canonicalize json representation - *body_bytes = serde_json::to_vec(&JsonRpcResponse::< - Vec, - >::from_error( - id, json_rpc_error - )) - .expect( - "BUG: failed to serialize previously deserialized JsonRpcResponse", - ) + serialize_if_ok( + body_bytes, + &JsonRpcResponse::>::from_error( + id, + json_rpc_error, + ), + ); } } } From e751e1b6f17ebe9e45cb4ae532f415fa3828358e Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Mon, 12 May 2025 15:32:50 +0200 Subject: [PATCH 37/42] XC-326: clippy. --- canister/src/rpc_client/sol_rpc/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/canister/src/rpc_client/sol_rpc/mod.rs b/canister/src/rpc_client/sol_rpc/mod.rs index b0836e9d..10d62892 100644 --- a/canister/src/rpc_client/sol_rpc/mod.rs +++ b/canister/src/rpc_client/sol_rpc/mod.rs @@ -10,7 +10,7 @@ use ic_cdk::{ }; use minicbor::{Decode, Encode}; use serde::{de::DeserializeOwned, Serialize}; -use serde_json::{from_slice, to_vec, Value}; +use serde_json::{from_slice, Value}; use sol_rpc_types::PrioritizationFee; use solana_clock::Slot; use std::fmt::Debug; From 5437114c33ffa6b85b6e2cae1fff200b0c8d8835 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Tue, 13 May 2025 09:21:59 +0200 Subject: [PATCH 38/42] XC-326: updated comment for should_normalize_response_with_non_contiguous_slots --- canister/src/rpc_client/sol_rpc/tests.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/canister/src/rpc_client/sol_rpc/tests.rs b/canister/src/rpc_client/sol_rpc/tests.rs index 5e51584c..4f64766e 100644 --- a/canister/src/rpc_client/sol_rpc/tests.rs +++ b/canister/src/rpc_client/sol_rpc/tests.rs @@ -424,9 +424,12 @@ mod get_recent_prioritization_fees { // The API of [getRecentPrioritizationFees](https://solana.com/de/docs/rpc/http/getrecentprioritizationfees) // does not specify whether the array of prioritization fees includes a range of continuous slots. // The following was observed: - // 1) On mainnet: the range seems always continuous (e.g., for slots 337346483..=337346632), also for not used addresses - // 2) Locally with solana-test-validator, the range is not necessarily continuous, e.g. + // 1) On mainnet: the range seems most of the time continuous (e.g., for slots 337346483..=337346632), also for not used addresses + // 2) Locally with solana-test-validator, the range is often not continuous, e.g. // RpcPrioritizationFee { slot: 5183, prioritization_fee: 150 }, RpcPrioritizationFee { slot: 5321, prioritization_fee: 0 } + // + // The non-continuity is probably because + // [not all slots have a block](https://docs.chainstack.com/docs/understanding-the-difference-between-blocks-and-slots-on-solana)/ #[test] fn should_normalize_response_with_non_contiguous_slots() { let range_1 = [PrioritizationFee { From b46fa53151f0fbbd2e5c508bf4e8efaacda139d6 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Tue, 13 May 2025 09:35:39 +0200 Subject: [PATCH 39/42] XC-326: rename internal transform method and added docs --- canister/src/rpc_client/sol_rpc/mod.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/canister/src/rpc_client/sol_rpc/mod.rs b/canister/src/rpc_client/sol_rpc/mod.rs index 10d62892..c300c8da 100644 --- a/canister/src/rpc_client/sol_rpc/mod.rs +++ b/canister/src/rpc_client/sol_rpc/mod.rs @@ -48,13 +48,18 @@ pub enum ResponseTransform { impl ResponseTransform { fn apply(&self, body_bytes: &mut Vec) { - fn serialize_if_ok(body_bytes: &mut Vec, response: &JsonRpcResponse) + fn update_body(body_bytes: &mut Vec, response: &JsonRpcResponse) where T: Serialize, { if let Ok(bytes) = serde_json::to_vec(response) { *body_bytes = bytes } + // If the serialization fails, this would typically be the sign of a bug, + // since deserialization was successfully done before calling that method. + // However, since this code path is called in a query method as part of the HTTPs transform, + // we prefer avoiding panicking since this would be hard to debug and could theoretically affect + // all calls. } fn canonicalize_response(body_bytes: &mut Vec, f: impl FnOnce(T) -> R) where @@ -62,7 +67,7 @@ impl ResponseTransform { R: Serialize + DeserializeOwned, { if let Ok(response) = from_slice::>(body_bytes) { - serialize_if_ok(body_bytes, &response.map(f)) + update_body(body_bytes, &response.map(f)) } } @@ -126,10 +131,10 @@ impl ResponseTransform { fees = fees.into_iter().rev().collect(); } - serialize_if_ok(body_bytes, &JsonRpcResponse::from_ok(id, fees)); + update_body(body_bytes, &JsonRpcResponse::from_ok(id, fees)); } Err(json_rpc_error) => { - serialize_if_ok( + update_body( body_bytes, &JsonRpcResponse::>::from_error( id, From 9a131f5d9e90cf2c148c0bf89cd00813df89795c Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Tue, 13 May 2025 10:53:24 +0200 Subject: [PATCH 40/42] XC-326: clippy --- canister/src/rpc_client/sol_rpc/tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/canister/src/rpc_client/sol_rpc/tests.rs b/canister/src/rpc_client/sol_rpc/tests.rs index 4f64766e..3a7cada7 100644 --- a/canister/src/rpc_client/sol_rpc/tests.rs +++ b/canister/src/rpc_client/sol_rpc/tests.rs @@ -428,8 +428,8 @@ mod get_recent_prioritization_fees { // 2) Locally with solana-test-validator, the range is often not continuous, e.g. // RpcPrioritizationFee { slot: 5183, prioritization_fee: 150 }, RpcPrioritizationFee { slot: 5321, prioritization_fee: 0 } // - // The non-continuity is probably because - // [not all slots have a block](https://docs.chainstack.com/docs/understanding-the-difference-between-blocks-and-slots-on-solana)/ + // The non-continuity is probably because + // [not all slots have a block](https://docs.chainstack.com/docs/understanding-the-difference-between-blocks-and-slots-on-solana). #[test] fn should_normalize_response_with_non_contiguous_slots() { let range_1 = [PrioritizationFee { From 79960705b7cc04d4ee8b298b0fcf3af6dd98846a Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Tue, 13 May 2025 16:17:57 +0200 Subject: [PATCH 41/42] XC-326: serialize to tuple --- canister/src/rpc_client/json/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/canister/src/rpc_client/json/mod.rs b/canister/src/rpc_client/json/mod.rs index 5f2d99ce..f03819a3 100644 --- a/canister/src/rpc_client/json/mod.rs +++ b/canister/src/rpc_client/json/mod.rs @@ -165,12 +165,12 @@ pub struct GetBlockConfig { #[skip_serializing_none] #[derive(Serialize, Clone, Debug)] -#[serde(into = "[Vec; 1]")] +#[serde(into = "(Vec,)")] pub struct GetRecentPrioritizationFeesParams(Vec); -impl From for [Vec; 1] { +impl From for (Vec,) { fn from(value: GetRecentPrioritizationFeesParams) -> Self { - [value.0] + (value.0,) } } From 0ad144ab53c6553b4e84cdc21a6db5ec82fcf786 Mon Sep 17 00:00:00 2001 From: gregorydemay Date: Tue, 13 May 2025 16:30:11 +0200 Subject: [PATCH 42/42] XC-326: use canonicalize_response --- canister/src/rpc_client/sol_rpc/mod.rs | 106 ++++++++++--------------- 1 file changed, 42 insertions(+), 64 deletions(-) diff --git a/canister/src/rpc_client/sol_rpc/mod.rs b/canister/src/rpc_client/sol_rpc/mod.rs index c300c8da..071d2753 100644 --- a/canister/src/rpc_client/sol_rpc/mod.rs +++ b/canister/src/rpc_client/sol_rpc/mod.rs @@ -48,26 +48,20 @@ pub enum ResponseTransform { impl ResponseTransform { fn apply(&self, body_bytes: &mut Vec) { - fn update_body(body_bytes: &mut Vec, response: &JsonRpcResponse) - where - T: Serialize, - { - if let Ok(bytes) = serde_json::to_vec(response) { - *body_bytes = bytes - } - // If the serialization fails, this would typically be the sign of a bug, - // since deserialization was successfully done before calling that method. - // However, since this code path is called in a query method as part of the HTTPs transform, - // we prefer avoiding panicking since this would be hard to debug and could theoretically affect - // all calls. - } fn canonicalize_response(body_bytes: &mut Vec, f: impl FnOnce(T) -> R) where T: Serialize + DeserializeOwned, R: Serialize + DeserializeOwned, { if let Ok(response) = from_slice::>(body_bytes) { - update_body(body_bytes, &response.map(f)) + if let Ok(bytes) = serde_json::to_vec(&response.map(f)) { + *body_bytes = bytes + } + // If the serialization fails, this would typically be the sign of a bug, + // since deserialization was successfully done before calling that method. + // However, since this code path is called in a query method as part of the HTTPs transform, + // we prefer avoiding panicking since this would be hard to debug and could theoretically affect + // all calls. } } @@ -93,57 +87,41 @@ impl ResponseTransform { max_slot_rounding_error, max_length, } => { - if let Ok(response) = - from_slice::>>(body_bytes) - { - let (id, result) = response.into_parts(); - match result { - Ok(mut fees) => { - // The exact number of elements for the returned priority fees is not really specified in the - // [API](https://solana.com/de/docs/rpc/http/getrecentprioritizationfees), - // which simply mentions - // "Currently, a node's prioritization-fee cache stores data from up to 150 blocks." - // Manual testing shows that the result seems to always contain 150 elements on mainnet (also for not used addresses) - // but not necessarily when using a local validator. - if fees.is_empty() || max_length == &0 { - fees.clear(); - } else { - // The order of the prioritization fees in the response is not specified in the - // [API](https://solana.com/de/docs/rpc/http/getrecentprioritizationfees), - // although examples and manual testing show that the response is sorted by increasing number of slot. - // To avoid any problem, we enforce the sorting. - fees.sort_unstable_by(|fee, other_fee| { - other_fee.slot.cmp(&fee.slot) //sort by decreasing order of slot - }); - let max_rounded_slot = max_slot_rounding_error.round( - fees.first() - .expect( - "BUG: recent prioritization fees should be non-empty", - ) - .slot, - ); - - fees = fees - .into_iter() - .skip_while(|fee| fee.slot > max_rounded_slot) - .take(*max_length as usize) - .collect(); - - fees = fees.into_iter().rev().collect(); - } - update_body(body_bytes, &JsonRpcResponse::from_ok(id, fees)); - } - Err(json_rpc_error) => { - update_body( - body_bytes, - &JsonRpcResponse::>::from_error( - id, - json_rpc_error, - ), - ); + canonicalize_response::, Vec>( + body_bytes, + |mut fees| { + // actual processing here + // The exact number of elements for the returned priority fees is not really specified in the + // [API](https://solana.com/de/docs/rpc/http/getrecentprioritizationfees), + // which simply mentions + // "Currently, a node's prioritization-fee cache stores data from up to 150 blocks." + // Manual testing shows that the result seems to always contain 150 elements on mainnet (also for not used addresses) + // but not necessarily when using a local validator. + if fees.is_empty() || max_length == &0 { + return Vec::default(); } - } - } + // The order of the prioritization fees in the response is not specified in the + // [API](https://solana.com/de/docs/rpc/http/getrecentprioritizationfees), + // although examples and manual testing show that the response is sorted by increasing number of slot. + // To avoid any problem, we enforce the sorting. + fees.sort_unstable_by(|fee, other_fee| { + other_fee.slot.cmp(&fee.slot) //sort by decreasing order of slot + }); + let max_rounded_slot = max_slot_rounding_error.round( + fees.first() + .expect("BUG: recent prioritization fees should be non-empty") + .slot, + ); + + fees.into_iter() + .skip_while(|fee| fee.slot > max_rounded_slot) + .take(*max_length as usize) + .collect::>() + .into_iter() + .rev() + .collect() + }, + ); } Self::GetSlot(rounding_error) => { canonicalize_response::(body_bytes, |slot| rounding_error.round(slot));