Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
1cdf810
XC-326: base types
gregorydemay May 2, 2025
0790f6d
XC-326: logic for GetRecentPrioritizationFees transform
gregorydemay May 2, 2025
0237fcc
XC-326: rename maxNumSlots
gregorydemay May 5, 2025
9600f07
XC-326: test normalization
gregorydemay May 5, 2025
2a119c6
XC-326: main logic
gregorydemay May 6, 2025
48be23a
XC-326: client
gregorydemay May 6, 2025
830f102
XC-326: remove chacha20rng dep
gregorydemay May 6, 2025
3ba6294
Revert "XC-326: remove chacha20rng dep"
gregorydemay May 6, 2025
3f3eb8b
XC-326: remove solana-rpc-client-api dependency because it depends tr…
gregorydemay May 6, 2025
58d15d6
XC-326: fix cycles cost
gregorydemay May 6, 2025
c2ea711
XC-326: failing int test
gregorydemay May 6, 2025
a7ba847
XC-326: fix int test
gregorydemay May 6, 2025
a56a5d7
XC-326: trap when too many accounts
gregorydemay May 6, 2025
fa84e7b
XC-326: fix exact cycles cost
gregorydemay May 7, 2025
217393e
XC-326: int test with validator
gregorydemay May 7, 2025
10ff267
XC-326: generate transactions with priority fees
gregorydemay May 7, 2025
8896dd1
XC-326: fix for non-contiguous range of slots
gregorydemay May 8, 2025
5981fe1
Merge branch 'main' into gdemay/XC-326-get-recent-prioritization-fees
gregorydemay May 8, 2025
ed33063
XC-326: rename max_num_slots for max_length
gregorydemay May 8, 2025
6052681
XC-326: add Debug to RequestBuilder
gregorydemay May 8, 2025
23d2b92
XC-326: refactor to use json type and add params to client get_recent…
gregorydemay May 8, 2025
777ce53
XC-326: Rust doc
gregorydemay May 8, 2025
454a9a5
XC-326: docs
gregorydemay May 8, 2025
2a7554f
XC-326: TODO doc
gregorydemay May 8, 2025
fc5534f
XC-326: end-to-end test
gregorydemay May 8, 2025
b1805c8
XC-326: micro-lamport
gregorydemay May 8, 2025
e91f8fb
XC-326: remove redundant test since already in Rust docs
gregorydemay May 8, 2025
2609a63
XC-326: clean-up
gregorydemay May 8, 2025
feb169d
XC-326: add doc Candid PrioritizationFee
gregorydemay May 12, 2025
3efcf34
XC-326: test should_normalize_json_rpc_error
gregorydemay May 12, 2025
1c6a71f
XC-326: variable rename
gregorydemay May 12, 2025
fc041b0
XC-326: fix indenting
gregorydemay May 12, 2025
ccd4c64
XC-326: formatting
gregorydemay May 12, 2025
792bba2
XC-326: simplify Rust doc test
gregorydemay May 12, 2025
203e830
XC-326: typo.
gregorydemay May 12, 2025
6348bf8
XC-326: use Slot type in PrioritizationFee.
gregorydemay May 12, 2025
fa4ecf1
XC-326: serialize_if_ok.
gregorydemay May 12, 2025
e751e1b
XC-326: clippy.
gregorydemay May 12, 2025
4df3475
Merge branch 'main' into gdemay/XC-326-get-recent-prioritization-fees
gregorydemay May 12, 2025
5437114
XC-326: updated comment for should_normalize_response_with_non_contig…
gregorydemay May 13, 2025
b46fa53
XC-326: rename internal transform method and added docs
gregorydemay May 13, 2025
9a131f5
XC-326: clippy
gregorydemay May 13, 2025
7996070
XC-326: serialize to tuple
gregorydemay May 13, 2025
0ad144a
XC-326: use canonicalize_response
gregorydemay May 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +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 }
regex = "1.11.1"
serde = { version = "1.0.219", features = ["derive"] }
serde_bytes = "0.11.17"
Expand All @@ -66,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"
Expand Down Expand Up @@ -100,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" }
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
5 changes: 4 additions & 1 deletion canister/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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 }
Expand All @@ -48,6 +49,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 }

Expand Down
18 changes: 18 additions & 0 deletions canister/scripts/examples.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 } },
Expand Down
54 changes: 54 additions & 0 deletions canister/sol_rpc_canister.did
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,30 @@ type GetSlotRpcConfig = record {
roundingError : opt RoundingError;
};

// 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;
// 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;
};

// Defines a consensus strategy for combining responses from different providers.
type ConsensusStrategy = variant {
Equality;
Expand Down Expand Up @@ -315,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;
Expand Down Expand Up @@ -567,6 +595,24 @@ 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
};

// 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;

Expand Down Expand Up @@ -612,6 +658,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;
Expand Down Expand Up @@ -691,6 +741,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);
Expand Down
41 changes: 38 additions & 3 deletions canister/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ use sol_rpc_canister::{
};
use sol_rpc_types::{
AccountInfo, ConfirmedBlock, GetAccountInfoParams, GetBalanceParams, GetBlockParams,
GetSlotParams, GetSlotRpcConfig, GetTokenAccountBalanceParams, GetTransactionParams, Lamport,
MultiRpcResult, RpcAccess, RpcConfig, RpcResult, RpcSources, SendTransactionParams, Signature,
Slot, SupportedRpcProvider, SupportedRpcProviderId, TokenAmount, TransactionInfo,
GetRecentPrioritizationFeesParams, GetRecentPrioritizationFeesRpcConfig, GetSlotParams,
GetSlotRpcConfig, GetTokenAccountBalanceParams, GetTransactionParams, Lamport, MultiRpcResult,
PrioritizationFee, RpcAccess, RpcConfig, RpcResult, RpcSources, SendTransactionParams,
Signature, Slot, SupportedRpcProvider, SupportedRpcProviderId, TokenAmount, TransactionInfo,
};
use std::str::FromStr;

Expand Down Expand Up @@ -154,6 +155,40 @@ async fn get_block_cycles_cost(
.await
}

#[update(name = "getRecentPrioritizationFees")]
#[candid_method(rename = "getRecentPrioritizationFees")]
async fn get_recent_prioritization_fees(
source: RpcSources,
config: Option<GetRecentPrioritizationFeesRpcConfig>,
params: Option<GetRecentPrioritizationFeesParams>,
) -> MultiRpcResult<Vec<PrioritizationFee>> {
let request = MultiRpcRequest::get_recent_prioritization_fees(
source,
config.unwrap_or_default(),
params.unwrap_or_default(),
);
send_multi(request).await
}

#[query(name = "getRecentPrioritizationFeesCyclesCost")]
#[candid_method(query, rename = "getRecentPrioritizationFeesCyclesCost")]
async fn get_recent_prioritization_fees_cycles_cost(
source: RpcSources,
config: Option<GetRecentPrioritizationFeesRpcConfig>,
params: Option<GetRecentPrioritizationFeesParams>,
) -> RpcResult<u128> {
if read_state(State::is_demo_mode_active) {
return Ok(0);
}
MultiRpcRequest::get_recent_prioritization_fees(
source,
config.unwrap_or_default(),
params.unwrap_or_default(),
)?
.cycles_cost()
.await
}

#[update(name = "getSlot")]
#[candid_method(rename = "getSlot")]
async fn get_slot(
Expand Down
19 changes: 18 additions & 1 deletion canister/src/rpc_client/json/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -163,6 +163,23 @@ pub struct GetBlockConfig {
pub max_supported_transaction_version: Option<u8>,
}

#[skip_serializing_none]
#[derive(Serialize, Clone, Debug)]
#[serde(into = "(Vec<Pubkey>,)")]
pub struct GetRecentPrioritizationFeesParams(Vec<Pubkey>);

impl From<GetRecentPrioritizationFeesParams> for (Vec<Pubkey>,) {
fn from(value: GetRecentPrioritizationFeesParams) -> Self {
(value.0,)
}
}

impl From<sol_rpc_types::GetRecentPrioritizationFeesParams> 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<GetTokenAccountBalanceConfig>)")]
Expand Down
36 changes: 34 additions & 2 deletions canister/src/rpc_client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ use ic_cdk::api::management_canister::http_request::{
};
use serde::{de::DeserializeOwned, Serialize};
use sol_rpc_types::{
ConsensusStrategy, GetSlotRpcConfig, Lamport, 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};
Expand Down Expand Up @@ -193,6 +194,37 @@ impl GetSlotRequest {
}
}

pub type GetRecentPrioritizationFeesRequest =
MultiRpcRequest<json::GetRecentPrioritizationFeesParams, Vec<PrioritizationFee>>;

impl GetRecentPrioritizationFeesRequest {
pub fn get_recent_prioritization_fees<Params: Into<json::GetRecentPrioritizationFeesParams>>(
rpc_sources: RpcSources,
config: GetRecentPrioritizationFeesRpcConfig,
params: Params,
) -> Result<Self, ProviderError> {
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(8 * 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_length: config.max_length.unwrap_or(100),
},
ReductionStrategy::from(consensus_strategy),
))
}
}

pub type GetTokenAccountBalanceRequest = MultiRpcRequest<
json::GetTokenAccountBalanceParams,
solana_account_decoder_client_types::token::UiTokenAmount,
Expand Down
Loading