diff --git a/Cargo.lock b/Cargo.lock index 4a6cd8e0..129b0e30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -363,9 +363,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34a796731680be7931955498a16a10b2270c7762963d5d570fdbfe02dcbf314f" +checksum = "389a099b34312839e16420d499a9cad9650541715937ffbdd40d36f49e77eeb3" dependencies = [ "arrayref", "arrayvec 0.7.6", @@ -680,9 +680,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.17" +version = "1.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" +checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" dependencies = [ "jobserver", "libc", @@ -742,7 +742,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", - "half 2.5.0", + "half 2.6.0", ] [[package]] @@ -1065,9 +1065,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", ] @@ -1270,9 +1270,9 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", "windows-sys 0.59.0", @@ -1368,9 +1368,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", "miniz_oxide", @@ -1627,9 +1627,9 @@ checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" [[package]] name = "half" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ "cfg-if", "crunchy", @@ -2138,9 +2138,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -2222,10 +2222,11 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ + "getrandom 0.3.2", "libc", ] @@ -2514,9 +2515,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.5" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" dependencies = [ "adler2", ] @@ -3286,9 +3287,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.10" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" dependencies = [ "bitflags 2.9.0", ] @@ -3503,9 +3504,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.3" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" dependencies = [ "bitflags 2.9.0", "errno", @@ -3981,9 +3982,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "socket2" @@ -4007,7 +4008,6 @@ dependencies = [ "ciborium", "const_format", "derive_more", - "futures", "hex", "http 1.3.1", "ic-cdk", @@ -4022,7 +4022,10 @@ dependencies = [ "serde_bytes", "serde_json", "sol_rpc_types", + "solana-account", + "solana-account-decoder-client-types", "solana-clock", + "solana-pubkey", "thiserror 2.0.12", "tower", "tower-http", @@ -4040,7 +4043,10 @@ dependencies = [ "serde", "serde_json", "sol_rpc_types", + "solana-account", + "solana-account-decoder-client-types", "solana-clock", + "solana-pubkey", ] [[package]] @@ -4063,7 +4069,11 @@ dependencies = [ "sol_rpc_canister", "sol_rpc_client", "sol_rpc_types", + "solana-account", + "solana-account-decoder-client-types", "solana-client", + "solana-pubkey", + "solana-rpc-client-api", "tokio", ] @@ -4077,6 +4087,10 @@ dependencies = [ "ic-cdk", "regex", "serde", + "serde_json", + "solana-account", + "solana-account-decoder-client-types", + "solana-pubkey", "strum 0.27.1", "thiserror 2.0.12", "url", @@ -5840,9 +5854,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.44.1" +version = "1.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" dependencies = [ "backtrace", "bytes", @@ -6754,9 +6768,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" +checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index eca42014..14d213fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,7 +45,6 @@ ic-canister-log = "0.2.0" ic-cdk = "0.17.1" ic-ed25519 = "0.1.0" ic-metrics-encoder = "1.1" -ic-sha3 = "1.0.0" ic-stable-structures = "0.6.7" ic-test-utilities-load-wasm = { git = "https://github.com/dfinity/ic", tag = "release-2025-01-23_03-04-base" } maplit = "1.0.2" @@ -58,6 +57,8 @@ regex = "1.11.1" serde = { version = "1.0.219", features = ["derive"] } serde_bytes = "0.11.17" serde_json = "1.0.140" +solana-account = "2.2.0" +solana-account-decoder-client-types = "2.2.0" solana-clock = "2.2.0" solana-client = "2.2.0" solana-hash = "2.2.0" @@ -66,13 +67,13 @@ solana-message = "2.2.0" solana-nonce = "2.2.0" solana-program = "2.2.0" solana-pubkey = "2.2.0" +solana-rpc-client-api = "2.2.0" solana-signature = "2.2.0" -solana-transaction = { version = "2.2.0", features = ["bincode"] } +solana-transaction = "2.2.0" strum = { version = "0.27.0", features = ["derive"] } thiserror = "2.0.12" tokio = "1.44.1" tower = "0.5.2" -tower-layer = "0.3.3" tower-http = "0.6.2" url = "2.5" zeroize = { version = "1.8", features = ["zeroize_derive"] } @@ -82,6 +83,8 @@ zeroize = { version = "1.8", features = ["zeroize_derive"] } # are using forked version of the relevant repositories. # see [solana-sdk#117](https://github.com/anza-xyz/solana-sdk/issues/117) [patch.crates-io] +solana-account = { git = "https://github.com/dfinity/agave", tag = "323039e-js-feature-flag" } +solana-account-decoder-client-types = { git = "https://github.com/dfinity/agave", tag = "323039e-js-feature-flag" } 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-hash = { git = "https://github.com/dfinity/agave", tag = "323039e-js-feature-flag" } @@ -91,5 +94,6 @@ solana-message = { git = "https://github.com/dfinity/agave", tag = "323039e-js-f solana-nonce = { git = "https://github.com/dfinity/agave", tag = "323039e-js-feature-flag" } solana-program = { git = "https://github.com/dfinity/agave", tag = "323039e-js-feature-flag" } solana-pubkey = { git = "https://github.com/dfinity/agave", tag = "323039e-js-feature-flag" } +solana-rpc-client-api = { git = "https://github.com/dfinity/agave", tag = "323039e-js-feature-flag" } solana-signature = { git = "https://github.com/dfinity/agave", tag = "323039e-js-feature-flag" } solana-transaction = { git = "https://github.com/dfinity/agave", tag = "323039e-js-feature-flag" } diff --git a/canister/Cargo.toml b/canister/Cargo.toml index fd57e921..8593921a 100644 --- a/canister/Cargo.toml +++ b/canister/Cargo.toml @@ -20,7 +20,6 @@ canlog = { path = "../canlog", features = ["derive"] } ciborium = { workspace = true } const_format = { workspace = true } derive_more = { workspace = true } -futures = { workspace = true } hex = { workspace = true } http = { workspace = true } ic-cdk = { workspace = true } @@ -35,6 +34,9 @@ regex = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde_bytes = { workspace = true } +solana-account = { workspace = true, features = ["serde"] } +solana-account-decoder-client-types = { workspace = true } +solana-pubkey = { workspace = true } tower = { workspace = true } tower-http = { workspace = true, features = ["set-header", "util"] } url = { workspace = true } @@ -44,3 +46,10 @@ thiserror = { workspace = true } [dev-dependencies] candid_parser = { workspace = true } proptest = { workspace = true } + +[features] +# WARNING: Disabling this feature will lead to some functionality not working anymore +# such as decoding of Solana account data in base64+zstd format which in turn might +# lead to some runtime errors. +zstd = ["solana-account-decoder-client-types/zstd"] +default = ["zstd"] diff --git a/canister/scripts/examples.sh b/canister/scripts/examples.sh index 3be9d894..00ea79ab 100755 --- a/canister/scripts/examples.sh +++ b/canister/scripts/examples.sh @@ -24,3 +24,24 @@ GET_SLOT_PARAMS="( opt record { minContextSlot = null; commitment = opt variant { finalized } }, )" dfx canister call sol_rpc getSlot "$GET_SLOT_PARAMS" $FLAGS --with-cycles "$CYCLES" || exit 1 + +# Get the System Program account info on Mainnet with a 2-out-of-3 strategy +# TODO XC-321: get cycle cost by query method +CYCLES="2B" +GET_ACCOUNT_INFO_PARAMS="( + variant { Default = variant { Mainnet } }, + opt record { + responseConsensus = opt variant { + Threshold = record { min = 2 : nat8; total = opt (3 : nat8) } + }; + responseSizeEstimate = null; + }, + record { + pubkey = \"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v\"; + commitment = null; + encoding = opt variant{ base64 }; + dataSlice = null; + minContextSlot = null; + }, +)" +dfx canister call sol_rpc getAccountInfo "$GET_ACCOUNT_INFO_PARAMS" $FLAGS --with-cycles "$CYCLES" || exit 1 diff --git a/canister/sol_rpc_canister.did b/canister/sol_rpc_canister.did index dc2d32ad..26adaa83 100644 --- a/canister/sol_rpc_canister.did +++ b/canister/sol_rpc_canister.did @@ -150,6 +150,104 @@ type RejectionCode = variant { CanisterReject; }; +// Represents the encoding of the return value of the `getAccountInfo` Solana RPC method. +type GetAccountInfoEncoding = variant { + // Return the account data encoded in base-58. This is slow and limited to less than 129 bytes of account data. + base58; + // Return the account data encoded in base-64. This works for account data of any size. + base64; + // Compress the account data using the Zstandard library and return the base64-encoded result. + "base64+zstd"; + // Return the data as a JSON string. + // * jsonParsed encoding attempts to use program-specific state parsers to return more human-readable and explicit + // account state data. + // * If jsonParsed is requested but a parser cannot be found, the field falls back to base64 encoding, detectable + // when the data field is type string. + jsonParsed; +}; + +// Represents a slice of the return value of the `getAccountInfo` Solana RPC method. +// NOTE: Data slicing is only available for base58, base64, or base64+zstd encodings. +type DataSlice = record { + length: nat32; + offset: nat32; +}; + +// The parameters for a call to the `getAccountInfo` Solana RPC method. +type GetAccountInfoParams = record { + pubkey: Pubkey; + commitment: opt CommitmentLevel; + encoding: opt GetAccountInfoEncoding; + dataSlice: opt DataSlice; + minContextSlot: opt nat64; +}; + +// Address of a Solana account as a base-58 encoded string. +// Example: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" +type Pubkey = text; + +// Solana account info. +type AccountInfo = record { + // Number of lamports assigned to this account. + lamports: nat64; + // Data associated with the account. + data: AccountData; + // Pubkey of the program this account has been assigned to. + owner: Pubkey; + // Boolean indicating if the account contains a program (and is strictly read-only). + executable: bool; + // The epoch at which this account will next owe rent. + rentEpoch: nat64; + // The data size of the account. + space: nat64; +}; + +// Represents Solana account data. +type AccountData = variant { + // The data is formatted as a binary string. This is a legacy format retained for RPC backwards compatibility + legacyBinary: text; + // The data is formatted as a JSON `ParsedAccount`. + json: ParsedAccount; + // The data is formatted as a string containing the account data encoded according to one of + // the `AccountEncoding` formats. + binary: record { text; AccountEncoding }; +}; + +// Represents Solana account data parsed as JSON. +type ParsedAccount = record { + // The Solana program that interprets the data. + program: Pubkey; + // The account data parsed as JSON and formatted as a string. + parsed: text; + // The data size of the account. + space: nat64; +}; + +// Represents an encoding format for Solana account data. +type AccountEncoding = variant { + // The account data is formatted as a binary string. Legacy. Retained for RPC backwards compatibility. + binary; + // The account data is formatted as a base-58 string. + base58; + // The account data is formatted as a base-64 string. + base64; + // The account data was first compressed using Zstandard and the + // result was then formatted as a base-64 string. + "base64+zstd"; + // The account data is formatted as a JSON string. + jsonParsed; +}; + +// Represents the result of a call to the `getAccountInfo` Solana RPC method. +// If the requested account doesn't exist, the `Ok` variant will be null. +type GetAccountInfoResult = variant { Ok : opt AccountInfo; Err : RpcError }; + +// Represents an aggregated result from multiple RPC calls to the `getAccountInfo` Solana RPC method. +type MultiGetAccountInfoResult = variant { + Consistent : GetAccountInfoResult; + Inconsistent : vec record { RpcSource; GetAccountInfoResult }; +}; + // Represents a Solana slot type Slot = nat64; @@ -237,6 +335,9 @@ service : (InstallArgs,) -> { // The caller is the controller or a principal specified in `InstallArgs::manage_api_keys`. updateApiKeys : (vec record { SupportedProvider; opt text }) -> (); + // Call the Solana `getAccountInfo` RPC method and return the resulting slot. + getAccountInfo : (RpcSources, opt RpcConfig, GetAccountInfoParams) -> (MultiGetAccountInfoResult); + // Call the Solana `getSlot` RPC method and return the resulting slot. getSlot : (RpcSources, opt GetSlotRpcConfig, opt GetSlotParams) -> (MultiGetSlotResult); diff --git a/canister/src/candid_rpc/mod.rs b/canister/src/candid_rpc/mod.rs index e52887ae..2684cd56 100644 --- a/canister/src/candid_rpc/mod.rs +++ b/canister/src/candid_rpc/mod.rs @@ -9,10 +9,9 @@ use crate::{ use canhttp::multi::ReductionError; use serde::Serialize; use sol_rpc_types::{ - GetSlotParams, MultiRpcResult, RpcAccess, RpcAuth, RpcConfig, RpcResult, RpcSource, RpcSources, - SupportedRpcProvider, + AccountInfo, GetAccountInfoParams, GetSlotParams, MultiRpcResult, RpcAccess, RpcAuth, + RpcConfig, RpcResult, RpcSource, RpcSources, Slot, SupportedRpcProvider, }; -use solana_clock::Slot; use std::fmt::Debug; fn process_result(method: RpcMethod, result: ReducedResult) -> MultiRpcResult { @@ -52,7 +51,7 @@ pub fn hostname(provider: SupportedRpcProvider) -> Option { hostname_from_url(url.as_str()) } -/// Adapt the `EthRpcClient` to the `Candid` interface used by the EVM-RPC canister. +/// Adapt the `SolRpcClient` to the `Candid` interface used by the SOL-RPC canister. pub struct CandidRpcClient { client: SolRpcClient, } @@ -72,17 +71,29 @@ impl CandidRpcClient { }) } - pub async fn get_slot(&self, params: GetSlotParams) -> MultiRpcResult { + pub async fn get_account_info( + &self, + params: GetAccountInfoParams, + ) -> MultiRpcResult> { + process_result( + RpcMethod::GetAccountInfo, + self.client.get_account_info(params.into()).await, + ) + .map(|maybe_account| maybe_account.map(AccountInfo::from)) + } + + pub async fn get_slot(&self, params: Option) -> MultiRpcResult { process_result(RpcMethod::GetSlot, self.client.get_slot(params).await) } pub async fn raw_request( &self, request: canhttp::http::json::JsonRpcRequest, - ) -> MultiRpcResult + ) -> MultiRpcResult where I: Serialize + Clone + Debug, { process_result(RpcMethod::Generic, self.client.raw_request(request).await) + .map(|value| value.to_string()) } } diff --git a/canister/src/main.rs b/canister/src/main.rs index deb802e6..a04b5638 100644 --- a/canister/src/main.rs +++ b/canister/src/main.rs @@ -13,10 +13,9 @@ use sol_rpc_canister::{ types::RoundingError, }; use sol_rpc_types::{ - GetSlotParams, GetSlotRpcConfig, MultiRpcResult, RpcAccess, RpcConfig, RpcError, RpcSources, - SupportedRpcProvider, SupportedRpcProviderId, + AccountInfo, GetAccountInfoParams, GetSlotParams, GetSlotRpcConfig, MultiRpcResult, RpcAccess, + RpcConfig, RpcError, RpcSources, Slot, SupportedRpcProvider, SupportedRpcProviderId, }; -use solana_clock::Slot; use std::str::FromStr; pub fn require_api_key_principal_or_controller() -> Result<(), String> { @@ -75,6 +74,19 @@ async fn update_api_keys(api_keys: Vec<(SupportedRpcProviderId, Option)> } } +#[update(name = "getAccountInfo")] +#[candid_method(rename = "getAccountInfo")] +async fn get_account_info( + source: RpcSources, + config: Option, + params: GetAccountInfoParams, +) -> MultiRpcResult> { + match CandidRpcClient::new(source, config) { + Ok(client) => client.get_account_info(params).await, + Err(err) => Err(err).into(), + } +} + #[update(name = "getSlot")] #[candid_method(rename = "getSlot")] async fn get_slot( @@ -91,7 +103,7 @@ async fn get_slot( config.map(RpcConfig::from), rounding_error, ) { - Ok(client) => client.get_slot(params.unwrap_or_default()).await, + Ok(client) => client.get_slot(params).await, Err(err) => Err(err).into(), } } @@ -113,10 +125,7 @@ async fn request( } }; match CandidRpcClient::new(source, config) { - Ok(client) => client - .raw_request(request) - .await - .map(|value| value.to_string()), + Ok(client) => client.raw_request(request).await, Err(err) => Err(err).into(), } } diff --git a/canister/src/metrics/mod.rs b/canister/src/metrics/mod.rs index c65848f3..73c4893b 100644 --- a/canister/src/metrics/mod.rs +++ b/canister/src/metrics/mod.rs @@ -141,6 +141,7 @@ pub struct Metrics { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum RpcMethod { + GetAccountInfo, GetSlot, Generic, } @@ -148,6 +149,7 @@ pub enum RpcMethod { impl RpcMethod { fn name(self) -> &'static str { match self { + RpcMethod::GetAccountInfo => "getAccountInfo", RpcMethod::GetSlot => "getSlot", RpcMethod::Generic => "generic", } diff --git a/canister/src/rpc_client/json/mod.rs b/canister/src/rpc_client/json/mod.rs new file mode 100644 index 00000000..48999e9a --- /dev/null +++ b/canister/src/rpc_client/json/mod.rs @@ -0,0 +1,42 @@ +use serde::{Deserialize, Serialize}; +use sol_rpc_types::{CommitmentLevel, DataSlice, GetAccountInfoEncoding}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(into = "(String, Option)")] +pub struct GetAccountInfoParams(String, Option); + +impl From for GetAccountInfoParams { + fn from(params: sol_rpc_types::GetAccountInfoParams) -> Self { + let config = if params.is_default_config() { + None + } else { + Some(GetAccountInfoConfig { + commitment: params.commitment, + encoding: params.encoding, + data_slice: params.data_slice, + min_context_slot: params.min_context_slot, + }) + }; + Self(params.pubkey, config) + } +} + +impl From for (String, Option) { + fn from(params: GetAccountInfoParams) -> Self { + (params.0.to_string(), params.1) + } +} + +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct GetAccountInfoConfig { + /// The request returns the slot that has reached this or the default commitment level. + pub commitment: Option, + /// Encoding format for Account data. + pub encoding: Option, + /// Request a slice of the account's data. + #[serde(rename = "dataSlice")] + pub data_slice: Option, + /// The minimum slot that the request can be evaluated at. + #[serde(rename = "minContextSlot")] + pub min_context_slot: Option, +} diff --git a/canister/src/rpc_client/mod.rs b/canister/src/rpc_client/mod.rs index 1661faf7..5c0824f6 100644 --- a/canister/src/rpc_client/mod.rs +++ b/canister/src/rpc_client/mod.rs @@ -1,3 +1,4 @@ +pub mod json; mod sol_rpc; #[cfg(test)] mod tests; @@ -8,7 +9,10 @@ use crate::{ memory::read_state, metrics::MetricRpcMethod, providers::{request_builder, resolve_rpc_provider, Providers}, - rpc_client::sol_rpc::{ResponseSizeEstimate, ResponseTransform, HEADER_SIZE_LIMIT}, + rpc_client::{ + json::GetAccountInfoParams, + sol_rpc::{ResponseSizeEstimate, ResponseTransform, HEADER_SIZE_LIMIT}, + }, types::RoundingError, }; use canhttp::{ @@ -23,7 +27,6 @@ use sol_rpc_types::{ ConsensusStrategy, GetSlotParams, JsonRpcError, ProviderError, RpcConfig, RpcError, RpcSource, RpcSources, }; -use solana_clock::Slot; use std::{collections::BTreeSet, fmt::Debug}; use tower::ServiceExt; @@ -145,8 +148,26 @@ impl SolRpcClient { results } + /// Query the Solana [`getAccountInfo`](https://solana.com/docs/rpc/http/getaccountinfo) RPC method. + pub async fn get_account_info( + &self, + params: GetAccountInfoParams, + ) -> ReducedResult> { + self.parallel_call( + "getAccountInfo", + params, + self.response_size_estimate(1024 + HEADER_SIZE_LIMIT), + &Some(ResponseTransform::GetAccountInfo), + ) + .await + .reduce(self.reduction_strategy()) + } + /// Query the Solana [`getSlot`](https://solana.com/docs/rpc/http/getslot) RPC method. - pub async fn get_slot(&self, params: GetSlotParams) -> ReducedResult { + pub async fn get_slot( + &self, + params: Option, + ) -> ReducedResult { self.parallel_call( "getSlot", vec![params], @@ -166,7 +187,7 @@ impl SolRpcClient { { self.parallel_call( request.method(), - request.params(), + vec![request.params()], self.response_size_estimate(1024 + HEADER_SIZE_LIMIT), &Some(ResponseTransform::Raw), ) diff --git a/canister/src/rpc_client/sol_rpc/mod.rs b/canister/src/rpc_client/sol_rpc/mod.rs index 0c04e287..d6b7edb1 100644 --- a/canister/src/rpc_client/sol_rpc/mod.rs +++ b/canister/src/rpc_client/sol_rpc/mod.rs @@ -10,7 +10,8 @@ use ic_cdk::{ }; use minicbor::{Decode, Encode}; use serde::{de::DeserializeOwned, Serialize}; -use serde_json::{from_slice, to_vec}; +use serde_json::{from_slice, from_value, to_vec, Value}; +use solana_account_decoder_client_types::UiAccount; use solana_clock::Slot; use std::{fmt, fmt::Debug}; @@ -33,6 +34,8 @@ pub const MAX_PAYLOAD_SIZE: u64 = HTTP_MAX_SIZE - HEADER_SIZE_LIMIT; #[derive(Debug, Decode, Encode)] pub enum ResponseTransform { #[n(0)] + GetAccountInfo, + #[n(1)] GetSlot(#[n(1)] RoundingError), #[n(2)] Raw, @@ -40,23 +43,35 @@ pub enum ResponseTransform { impl ResponseTransform { fn apply(&self, body_bytes: &mut Vec) { - fn canonicalize(body_bytes: &mut Vec, f: impl FnOnce(T) -> T) + fn canonicalize_response(body_bytes: &mut Vec, f: impl FnOnce(T) -> R) where T: Serialize + DeserializeOwned, + R: Serialize + DeserializeOwned, { - if let Ok(Ok(bytes)) = from_slice::(body_bytes).map(f).as_ref().map(to_vec) { - *body_bytes = bytes + if let Ok(response) = from_slice::>(body_bytes) { + if let Ok(bytes) = to_vec(&response.map(f)) { + *body_bytes = bytes + } } } match self { + Self::GetAccountInfo => { + canonicalize_response::>( + body_bytes, + |result| match result["value"].clone() { + Value::Null => None, + value => Some( + from_value::(value).expect("Unable to deserialize account"), + ), + }, + ); + } Self::GetSlot(rounding_error) => { - canonicalize::>(body_bytes, |response| { - response.map(|slot| rounding_error.round(slot)) - }); + canonicalize_response::(body_bytes, |slot| rounding_error.round(slot)); } Self::Raw => { - canonicalize::(body_bytes, std::convert::identity); + canonicalize_response::(body_bytes, std::convert::identity); } } } diff --git a/canister/src/rpc_client/sol_rpc/tests.rs b/canister/src/rpc_client/sol_rpc/tests.rs index 1340f6bb..93963d30 100644 --- a/canister/src/rpc_client/sol_rpc/tests.rs +++ b/canister/src/rpc_client/sol_rpc/tests.rs @@ -1,7 +1,11 @@ use super::*; +use canhttp::http::json::Id; +use solana_account_decoder_client_types::{UiAccountData, UiAccountEncoding}; mod normalization_tests { use super::*; + use proptest::proptest; + use serde_json::json; #[test] fn should_normalize_raw_response() { @@ -41,19 +45,98 @@ mod normalization_tests { ); } - fn normalize_result(transform: &ResponseTransform, result: &str) -> String { + #[test] + fn should_normalize_get_account_info_response() { + assert_normalized( + &ResponseTransform::GetAccountInfo, + r#"{ + "context": { "apiVersion": "2.0.15", "slot": 341197053 }, + "value": { + "data": ["1234", "base58"], + "executable": false, + "lamports": 88849814690250, + "owner": "11111111111111111111111111111111", + "rentEpoch": 18446744073709551615, + "space": 0 + } + }"#, + Some(UiAccount { + lamports: 88849814690250, + data: UiAccountData::Binary("1234".to_string(), UiAccountEncoding::Base58), + owner: "11111111111111111111111111111111".to_string(), + executable: false, + rent_epoch: 18446744073709551615, + space: Some(0), + }), + ); + } + + proptest! { + #[test] + fn should_ignore_get_account_info_response_context(slot1: u64, slot2: u64) { + assert_normalized_equal( + &ResponseTransform::GetAccountInfo, + json!({ + "context": { "apiVersion": "2.0.15", "slot": slot1 }, + "value": { + "data": ["1234", "base58"], + "executable": false, + "lamports": 88849814690250u64, + "owner": "11111111111111111111111111111111", + "rentEpoch": 18446744073709551615u64, + "space": 0 + } + }).to_string(), + json!({ + "context": { "apiVersion": "2.0.15", "slot": slot2 }, + "value": { + "data": ["1234", "base58"], + "executable": false, + "lamports": 88849814690250u64, + "owner": "11111111111111111111111111111111", + "rentEpoch": 18446744073709551615u64, + "space": 0 + } + }).to_string(), + ); + } + } + + #[test] + fn should_normalize_empty_get_account_info_response() { + assert_normalized( + &ResponseTransform::GetAccountInfo, + r#"{"context": { "apiVersion": "2.0.15", "slot": 341197053 }}"#, + None::, + ); + } + + fn assert_normalized(transform: &ResponseTransform, result: &str, expected: T) + where + T: Debug + Serialize + DeserializeOwned, + { + let expected_response = to_vec(&JsonRpcResponse::from_ok(Id::Number(1), expected)).unwrap(); + let normalized_response = normalize_result(transform, result); + assert_eq!(expected_response, normalized_response); + } + + fn normalize_result(transform: &ResponseTransform, result: &str) -> Vec { fn add_envelope(reply: &str) -> Vec { format!("{{\"jsonrpc\": \"2.0\", \"id\": 1, \"result\": {}}}", reply).into_bytes() } let mut response = add_envelope(result); transform.apply(&mut response); - String::from_utf8(response).unwrap() + response } - fn assert_normalized_equal(transform: &ResponseTransform, left: &str, right: &str) { + fn assert_normalized_equal( + transform: &ResponseTransform, + left: impl AsRef, + right: impl AsRef, + ) { assert_eq!( - normalize_result(transform, left), - normalize_result(transform, right) + normalize_result(transform, left.as_ref()), + normalize_result(transform, right.as_ref()) ); } diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index e3b8235a..a47a39b7 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -22,6 +22,10 @@ serde_json = { workspace = true } sol_rpc_canister = { path = "../canister" } sol_rpc_client = { path = "../libs/client" } sol_rpc_types = { path = "../libs/types" } +solana-account = { workspace = true } +solana-pubkey = { workspace = true } +solana-account-decoder-client-types = { workspace = true } +solana-rpc-client-api = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } diff --git a/integration_tests/tests/solana_test_validator.rs b/integration_tests/tests/solana_test_validator.rs index 925bbc90..8ceca2d9 100644 --- a/integration_tests/tests/solana_test_validator.rs +++ b/integration_tests/tests/solana_test_validator.rs @@ -6,9 +6,12 @@ use futures::future; use pocket_ic::PocketIcBuilder; use sol_rpc_client::SolRpcClient; use sol_rpc_int_tests::PocketIcLiveModeRuntime; -use sol_rpc_types::{InstallArgs, MultiRpcResult, OverrideProvider, RegexSubstitution}; -use solana_client::rpc_client::RpcClient as SolanaRpcClient; -use std::future::Future; +use sol_rpc_types::{ + GetAccountInfoEncoding, GetAccountInfoParams, InstallArgs, OverrideProvider, RegexSubstitution, +}; +use solana_account_decoder_client_types::UiAccount; +use solana_client::rpc_client::{RpcClient as SolanaRpcClient, RpcClient}; +use std::{future::Future, str::FromStr}; #[tokio::test(flavor = "multi_thread")] async fn should_get_slot() { @@ -18,10 +21,11 @@ async fn should_get_slot() { .compare_client( |sol| sol.get_slot().expect("Failed to get slot"), |ic| async move { - match ic.get_slot().send().await { - MultiRpcResult::Consistent(Ok(slot)) => slot, - result => panic!("Failed to get slot, received: {:?}", result), - } + ic.get_slot() + .send() + .await + .expect_consistent() + .unwrap_or_else(|e| panic!("`getSlot` call failed: {e}")) }, ) .await; @@ -34,6 +38,78 @@ async fn should_get_slot() { setup.setup.drop().await; } +#[tokio::test(flavor = "multi_thread")] +async fn should_get_account_info() { + let setup = Setup::new().await; + let pubkey = solana_pubkey::Pubkey::from_str("11111111111111111111111111111111").unwrap(); + let params = GetAccountInfoParams { + pubkey: pubkey.to_string(), + commitment: None, + encoding: Some(GetAccountInfoEncoding::Base64), + data_slice: None, + min_context_slot: None, + }; + + let (sol_res, ic_res) = setup + .compare_client( + |sol| solana_rpc_client_get_account(&pubkey, sol, None), + |ic| async move { + ic.get_account_info(params) + .send() + .await + .expect_consistent() + .unwrap_or_else(|e| panic!("`getAccountInfo` call failed: {e}")) + .map(decode_ui_account) + }, + ) + .await; + + assert_eq!(sol_res, ic_res); + + setup.setup.drop().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn should_not_get_account_info() { + let setup = Setup::new().await; + let pubkey = + solana_pubkey::Pubkey::from_str("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v").unwrap(); + + let (sol_res, ic_res) = setup + .compare_client( + |sol| solana_rpc_client_get_account(&pubkey, sol, None), + |ic| async move { + ic.get_account_info(pubkey) + .send() + .await + .expect_consistent() + .unwrap_or_else(|e| panic!("`getAccountInfo` call failed: {e}")) + .map(decode_ui_account) + }, + ) + .await; + + assert_eq!(sol_res, ic_res); + + setup.setup.drop().await; +} + +fn solana_rpc_client_get_account( + pubkey: &solana_pubkey::Pubkey, + sol: &RpcClient, + config: Option, +) -> Option { + sol.get_account_with_config(pubkey, config.unwrap_or_default()) + .expect("Failed to get account") + .value +} + +fn decode_ui_account(account: UiAccount) -> solana_account::Account { + account + .decode::() + .unwrap_or_else(|| panic!("Failed to decode account")) +} + pub struct Setup { solana_client: SolanaRpcClient, setup: sol_rpc_int_tests::Setup, diff --git a/integration_tests/tests/tests.rs b/integration_tests/tests/tests.rs index dcbc55af..2961c69f 100644 --- a/integration_tests/tests/tests.rs +++ b/integration_tests/tests/tests.rs @@ -11,7 +11,8 @@ use sol_rpc_types::{ InstallArgs, Mode, ProviderError, RpcAccess, RpcAuth, RpcConfig, RpcEndpoint, RpcError, RpcResult, RpcSource, RpcSources, SolanaCluster, SupportedRpcProvider, SupportedRpcProviderId, }; -use std::str::FromStr; +use solana_account_decoder_client_types::{UiAccount, UiAccountData, UiAccountEncoding}; +use std::{iter::zip, str::FromStr}; const MOCK_REQUEST_URL: &str = "https://api.devnet.solana.com/"; const MOCK_REQUEST_PAYLOAD: &str = r#"{"jsonrpc":"2.0","id":0,"method":"getVersion"}"#; @@ -142,6 +143,93 @@ mod get_provider_tests { } } +mod get_account_info_tests { + use super::*; + + #[tokio::test] + async fn should_get_account_info() { + let setup = Setup::new().await.with_mock_api_keys().await; + + for (sources, first_id) in zip(rpc_sources(), vec![0_u8, 3, 6]) { + let client = setup.client().with_rpc_sources(sources); + let pubkey = + solana_pubkey::Pubkey::from_str("11111111111111111111111111111111").unwrap(); + + let results = client + .mock_sequential_json_rpc_responses::<3>( + 200, + json!({ + "id": first_id, + "jsonrpc": "2.0", + "result": { + "context": { "apiVersion": "2.0.15", "slot": 341197053 }, + "value": { + "data": ["1234", "base58"], + "executable": false, + "lamports": 88849814690250u64, + "owner": "11111111111111111111111111111111", + "rentEpoch": 18446744073709551615u64, + "space": 0 + } + }, + }), + ) + .build() + .get_account_info(pubkey) + .send() + .await + .expect_consistent(); + + assert_eq!( + results, + Ok(UiAccount { + lamports: 88849814690250, + data: UiAccountData::Binary("1234".to_string(), UiAccountEncoding::Base58), + owner: "11111111111111111111111111111111".to_string(), + executable: false, + rent_epoch: 18446744073709551615, + space: Some(0), + } + .into()) + ); + } + + setup.drop().await; + } + + #[tokio::test] + async fn should_not_get_account_info() { + let setup = Setup::new().await.with_mock_api_keys().await; + + for (sources, first_id) in zip(rpc_sources(), vec![0_u8, 3, 6]) { + let client = setup.client().with_rpc_sources(sources); + let pubkey = + solana_pubkey::Pubkey::from_str("11111111111111111111111111111111").unwrap(); + + let results = client + .mock_sequential_json_rpc_responses::<3>( + 200, + json!({ + "id": first_id, + "jsonrpc": "2.0", + "result": { + "context": { "apiVersion": "2.0.15", "slot": 341197053 } + }, + }), + ) + .build() + .get_account_info(pubkey) + .send() + .await + .expect_consistent(); + + assert_eq!(results, Ok(None)); + } + + setup.drop().await; + } +} + mod get_slot_tests { use super::*; use sol_rpc_types::{CommitmentLevel, GetSlotParams}; diff --git a/libs/client/Cargo.toml b/libs/client/Cargo.toml index ab68dba4..d514e26a 100644 --- a/libs/client/Cargo.toml +++ b/libs/client/Cargo.toml @@ -16,5 +16,8 @@ candid = { workspace = true } ic-cdk = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +solana-account = { workspace = true } +solana-account-decoder-client-types = { workspace = true } solana-clock = { workspace = true } +solana-pubkey = { workspace = true } sol_rpc_types = { path = "../types" } \ No newline at end of file diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs index 43326133..4c76c490 100644 --- a/libs/client/src/lib.rs +++ b/libs/client/src/lib.rs @@ -42,14 +42,14 @@ mod request; pub use request::{Request, RequestBuilder, SolRpcRequest}; -use crate::request::{GetSlotRequest, RawRequest}; +use crate::request::{GetAccountInfoRequest, GetSlotRequest, RawRequest}; use async_trait::async_trait; use candid::{utils::ArgumentEncoder, CandidType, Principal}; use ic_cdk::api::call::RejectionCode; use serde::de::DeserializeOwned; use sol_rpc_types::{ - GetSlotParams, GetSlotRpcConfig, RpcConfig, RpcSources, SolanaCluster, SupportedRpcProvider, - SupportedRpcProviderId, + GetAccountInfoParams, GetSlotParams, GetSlotRpcConfig, RpcConfig, RpcSources, SolanaCluster, + SupportedRpcProvider, SupportedRpcProviderId, }; use solana_clock::Slot; use std::sync::Arc; @@ -188,6 +188,24 @@ impl ClientBuilder { } impl SolRpcClient { + /// Call `getAccountInfo` on the SOL RPC canister. + pub fn get_account_info( + &self, + params: impl Into, + ) -> RequestBuilder< + R, + RpcConfig, + GetAccountInfoParams, + sol_rpc_types::MultiRpcResult>, + sol_rpc_types::MultiRpcResult>, + > { + RequestBuilder::new( + self.clone(), + GetAccountInfoRequest::new(params.into()), + 10_000_000_000, + ) + } + /// Call `getSlot` on the SOL RPC canister. pub fn get_slot( &self, @@ -196,6 +214,7 @@ impl SolRpcClient { GetSlotRpcConfig, Option, sol_rpc_types::MultiRpcResult, + sol_rpc_types::MultiRpcResult, > { RequestBuilder::new(self.clone(), GetSlotRequest::default(), 10_000_000_000) } @@ -204,7 +223,13 @@ impl SolRpcClient { pub fn raw_request( &self, json_request: serde_json::Value, - ) -> RequestBuilder> { + ) -> RequestBuilder< + R, + RpcConfig, + String, + sol_rpc_types::MultiRpcResult, + sol_rpc_types::MultiRpcResult, + > { RequestBuilder::new( self.clone(), RawRequest::try_from(json_request).expect("Client error: invalid JSON request"), @@ -237,18 +262,18 @@ impl SolRpcClient { .unwrap() } - async fn execute_request( + async fn execute_request( &self, - request: Request, + request: Request, ) -> Output where Config: CandidType + Send, Params: CandidType + Send, - Output: CandidType + DeserializeOwned, + CandidOutput: Into + CandidType + DeserializeOwned, { self.config .runtime - .update_call( + .update_call::<(RpcSources, Option, Params), CandidOutput>( self.config.sol_rpc_canister, &request.rpc_method, (request.rpc_sources, request.rpc_config, request.params), @@ -261,6 +286,7 @@ impl SolRpcClient { request.rpc_method ) }) + .into() } } diff --git a/libs/client/src/request/mod.rs b/libs/client/src/request/mod.rs index 3b3bb22c..fe742cf9 100644 --- a/libs/client/src/request/mod.rs +++ b/libs/client/src/request/mod.rs @@ -1,7 +1,9 @@ use crate::{Runtime, SolRpcClient}; use candid::CandidType; use serde::de::DeserializeOwned; -use sol_rpc_types::{GetSlotParams, GetSlotRpcConfig, RpcConfig, RpcSources}; +use sol_rpc_types::{ + AccountInfo, GetAccountInfoParams, GetSlotParams, GetSlotRpcConfig, RpcConfig, RpcSources, +}; use solana_clock::Slot; /// Solana RPC endpoint supported by the SOL RPC canister. @@ -10,6 +12,8 @@ pub trait SolRpcRequest { type Config; /// The type of parameters taken by this endpoint. type Params; + /// The Candid type returned when executing this request which is then converted to [`Self::Output`]. + type CandidOutput; /// The type returned by this endpoint. type Output; @@ -20,12 +24,38 @@ pub trait SolRpcRequest { fn params(self) -> Self::Params; } +#[derive(Debug, Clone)] +pub struct GetAccountInfoRequest(GetAccountInfoParams); + +impl GetAccountInfoRequest { + pub fn new(params: GetAccountInfoParams) -> Self { + Self(params) + } +} + +impl SolRpcRequest for GetAccountInfoRequest { + type Config = RpcConfig; + type Params = GetAccountInfoParams; + type CandidOutput = sol_rpc_types::MultiRpcResult>; + type Output = + sol_rpc_types::MultiRpcResult>; + + fn rpc_method(&self) -> &str { + "getAccountInfo" + } + + fn params(self) -> Self::Params { + self.0 + } +} + #[derive(Debug, Clone, Default)] pub struct GetSlotRequest(Option); impl SolRpcRequest for GetSlotRequest { type Config = GetSlotRpcConfig; type Params = Option; + type CandidOutput = Self::Output; type Output = sol_rpc_types::MultiRpcResult; fn rpc_method(&self) -> &str { @@ -52,6 +82,7 @@ impl TryFrom for RawRequest { impl SolRpcRequest for RawRequest { type Config = RpcConfig; type Params = String; + type CandidOutput = sol_rpc_types::MultiRpcResult; type Output = sol_rpc_types::MultiRpcResult; fn rpc_method(&self) -> &str { @@ -67,19 +98,26 @@ impl SolRpcRequest for RawRequest { /// /// To construct a [`RequestBuilder`], refer to the [`SolRpcClient`] documentation. #[must_use = "RequestBuilder does nothing until you 'send' it"] -pub struct RequestBuilder { +pub struct RequestBuilder { client: SolRpcClient, - request: Request, + request: Request, } -impl RequestBuilder { +impl + RequestBuilder +{ pub(super) fn new( client: SolRpcClient, rpc_request: RpcRequest, cycles: u128, ) -> Self where - RpcRequest: SolRpcRequest, + RpcRequest: SolRpcRequest< + Config = Config, + Params = Params, + CandidOutput = CandidOutput, + Output = Output, + >, Config: From, { let request = Request { @@ -88,9 +126,10 @@ impl RequestBuilder { client, request } } /// Change the amount of cycles to send for that request. @@ -106,19 +145,25 @@ impl RequestBuilder RequestBuilder { +impl + RequestBuilder +{ /// Constructs the [`Request`] and send it using the [`SolRpcClient`]. pub async fn send(self) -> Output where Config: CandidType + Send, Params: CandidType + Send, - Output: CandidType + DeserializeOwned, + CandidOutput: Into + CandidType + DeserializeOwned, { - self.client.execute_request(self.request).await + self.client + .execute_request::(self.request) + .await } } -impl RequestBuilder { +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() { @@ -135,16 +180,17 @@ impl RequestBuilder { +pub struct Request { pub(super) rpc_method: String, pub(super) rpc_sources: RpcSources, pub(super) rpc_config: Option, pub(super) params: Params, pub(super) cycles: u128, - pub(super) _marker: std::marker::PhantomData, + pub(super) _candid_marker: std::marker::PhantomData, + pub(super) _output_marker: std::marker::PhantomData, } -impl Request { +impl Request { /// Get a mutable reference to the cycles. #[inline] pub fn cycles_mut(&mut self) -> &mut u128 { diff --git a/libs/types/Cargo.toml b/libs/types/Cargo.toml index 72412e4a..adcd6037 100644 --- a/libs/types/Cargo.toml +++ b/libs/types/Cargo.toml @@ -17,6 +17,10 @@ derive_more = { workspace = true } ic-cdk = { workspace = true } regex = { workspace = true } serde = { workspace = true } +serde_json = { workspace = true } +solana-account = { workspace = true } +solana-account-decoder-client-types = { workspace = true } +solana-pubkey = { workspace = true } strum = { workspace = true } thiserror = { workspace = true } url = { workspace = true } diff --git a/libs/types/src/lib.rs b/libs/types/src/lib.rs index 61a9c901..fab915bf 100644 --- a/libs/types/src/lib.rs +++ b/libs/types/src/lib.rs @@ -16,4 +16,7 @@ pub use rpc_client::{ RpcEndpoint, RpcError, RpcResult, RpcSource, RpcSources, SolanaCluster, SupportedRpcProvider, SupportedRpcProviderId, }; -pub use solana::{CommitmentLevel, GetSlotParams}; +pub use solana::{ + AccountData, AccountEncoding, AccountInfo, CommitmentLevel, DataSlice, GetAccountInfoEncoding, + GetAccountInfoParams, GetSlotParams, ParsedAccount, Pubkey, Slot, +}; diff --git a/libs/types/src/solana/mod.rs b/libs/types/src/solana/mod.rs index c373d0e9..7796eb4f 100644 --- a/libs/types/src/solana/mod.rs +++ b/libs/types/src/solana/mod.rs @@ -1,7 +1,11 @@ +use crate::MultiRpcResult; use candid::CandidType; use serde::{Deserialize, Serialize}; use std::fmt::Debug; +/// A Solana [slot](https://solana.com/docs/references/terminology#slot). +pub type Slot = u64; + /// 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 { @@ -12,6 +16,45 @@ pub struct GetSlotParams { pub min_context_slot: Option, } +/// The parameters for a Solana [`getAccountInfo`](https://solana.com/docs/rpc/http/getAccountInfo) RPC method call. +#[derive(Debug, Clone, Deserialize, Serialize, CandidType)] +pub struct GetAccountInfoParams { + /// The public key of the account whose info to fetch formatted as a base-58 string. + pub pubkey: String, + /// The request returns the slot that has reached this or the default commitment level. + pub commitment: Option, + /// Encoding format for Account data. + pub encoding: Option, + /// Request a slice of the account's data. + #[serde(rename = "dataSlice")] + pub data_slice: Option, + /// The minimum slot that the request can be evaluated at. + #[serde(rename = "minContextSlot")] + pub min_context_slot: Option, +} + +impl GetAccountInfoParams { + /// Returns `true` if all of the optional config parameters are `None` and `false` otherwise. + pub fn is_default_config(&self) -> bool { + self.commitment.is_none() + && self.encoding.is_none() + && self.data_slice.is_none() + && self.min_context_slot.is_none() + } +} + +impl From for GetAccountInfoParams { + fn from(pubkey: solana_pubkey::Pubkey) -> Self { + Self { + pubkey: pubkey.to_string(), + commitment: None, + encoding: None, + data_slice: None, + min_context_slot: None, + } + } +} + /// [Commitment levels](https://solana.com/docs/rpc#configuring-state-commitment) in Solana, /// representing finality guarantees of transactions and memory queries. #[derive(Debug, Clone, Deserialize, Serialize, CandidType)] @@ -26,3 +69,215 @@ pub enum CommitmentLevel { #[serde(rename = "finalized")] Finalized, } + +/// Encoding for the return value of the Solana [`getAccountInfo`](https://solana.com/docs/rpc/http/getaccountinfo) RPC method. +#[derive(Debug, Clone, Deserialize, Serialize, CandidType)] +pub enum GetAccountInfoEncoding { + /// The account data is base-58 encoded. Limited to less than 129 bytes of data. + #[serde(rename = "base58")] + Base58, + /// The account data is base-64 encoded. + #[serde(rename = "base64")] + Base64, + /// Account data is first compressed using [Zstandard](http://facebook.github.io/zstd/) and the + /// result is then base-64 encoded. + #[serde(rename = "base64+zstd")] + Base64ZStd, + /// The encoding attempts to use program-specific state parsers to return more human-readable + /// and explicit account state data. If [`GetAccountInfoEncoding::JsonParsed`] is requested but + /// a parser cannot be found, the fallback is [`GetAccountInfoEncoding::Base64`] encoding. + #[serde(rename = "jsonParsed")] + JsonParsed, +} + +/// Represents a slice of the return value of the Solana [`getAccountInfo`](https://solana.com/docs/rpc/http/getAccountInfo) RPC method. +#[derive(Debug, Clone, Default, Deserialize, Serialize, CandidType)] +pub struct DataSlice { + /// Number of bytes to return. + length: u32, + /// Byte offset from which to start reading. + offset: u32, +} + +/// Solana Ed25519 [public key](`https://solana.com/docs/references/terminology#public-key-pubkey`). +#[derive(Debug, Clone, Deserialize, Serialize, CandidType, PartialEq)] +pub struct Pubkey(pub [u8; 32]); + +impl From for Pubkey { + fn from(pubkey: solana_pubkey::Pubkey) -> Self { + Pubkey(pubkey.to_bytes()) + } +} + +impl From for solana_pubkey::Pubkey { + fn from(pubkey: Pubkey) -> Self { + solana_pubkey::Pubkey::from(pubkey.0) + } +} + +/// Solana [account](https://solana.com/docs/references/terminology#account) information. +#[derive(Debug, Clone, Deserialize, Serialize, CandidType, PartialEq)] +pub struct AccountInfo { + /// Number of lamports assigned to this account. + pub lamports: u64, + /// Data associated with the account. + pub data: AccountData, + /// base-58 encoded Pubkey of the program this account has been assigned to. + pub owner: String, + /// Boolean indicating if the account contains a program (and is strictly read-only). + pub executable: bool, + /// The epoch at which this account will next owe rent. + #[serde(rename = "rentEpoch")] + pub rent_epoch: u64, + /// The data size of the account. + pub space: u64, +} + +impl From>> + for MultiRpcResult> +{ + fn from(result: MultiRpcResult>) -> Self { + result.map(|maybe_account| maybe_account.map(|account| account.into())) + } +} + +impl From for AccountInfo { + fn from(account: solana_account_decoder_client_types::UiAccount) -> Self { + AccountInfo { + lamports: account.lamports, + data: account.data.into(), + owner: account.owner, + executable: account.executable, + rent_epoch: account.rent_epoch, + space: account.space.expect("'space' field should not be null"), + } + } +} + +impl From for solana_account_decoder_client_types::UiAccount { + fn from(account: AccountInfo) -> Self { + solana_account_decoder_client_types::UiAccount { + lamports: account.lamports, + data: account.data.into(), + owner: account.owner, + executable: account.executable, + rent_epoch: account.rent_epoch, + space: Some(account.space), + } + } +} + +/// Represents the data stored in a Solana [account](https://solana.com/docs/references/terminology#account). +#[derive(Debug, Clone, Deserialize, Serialize, CandidType, PartialEq)] +pub enum AccountData { + /// The data is formatted as a binary string. This is a legacy format retained for RPC backwards compatibility + #[serde(rename = "legacyBinary")] + LegacyBinary(String), + /// The data is formatted as a JSON [`ParsedAccount`]. + #[serde(rename = "json")] + Json(ParsedAccount), + /// The data is formatted as a string containing the account data encoded according to one of + /// the [`AccountEncoding`] formats. + #[serde(rename = "binary")] + Binary(String, AccountEncoding), +} + +impl From for AccountData { + fn from(data: solana_account_decoder_client_types::UiAccountData) -> Self { + use solana_account_decoder_client_types::UiAccountData; + match data { + UiAccountData::LegacyBinary(value) => Self::LegacyBinary(value), + UiAccountData::Json(value) => Self::Json(value.into()), + UiAccountData::Binary(value, encoding) => Self::Binary(value, encoding.into()), + } + } +} + +impl From for solana_account_decoder_client_types::UiAccountData { + fn from(data: AccountData) -> Self { + use solana_account_decoder_client_types::UiAccountData; + match data { + AccountData::LegacyBinary(value) => UiAccountData::LegacyBinary(value), + AccountData::Json(value) => UiAccountData::Json(value.into()), + AccountData::Binary(value, encoding) => UiAccountData::Binary(value, encoding.into()), + } + } +} + +/// Represents parsed Solana [account](https://solana.com/docs/references/terminology#account) data. +#[derive(Debug, Clone, Deserialize, Serialize, CandidType, PartialEq)] +pub struct ParsedAccount { + /// The Solana [program](https://solana.com/docs/references/terminology#program) that interprets the data. + pub program: String, + /// The account data parsed a JSON and formatted as a string. + pub parsed: String, + /// The data size of the account. + pub space: u64, +} + +impl From for ParsedAccount { + fn from(account: solana_account_decoder_client_types::ParsedAccount) -> Self { + Self { + program: account.program, + parsed: serde_json::to_string(&account.parsed) + .expect("Unable to convert JSON to string"), + space: account.space, + } + } +} + +impl From for solana_account_decoder_client_types::ParsedAccount { + fn from(account: ParsedAccount) -> Self { + Self { + program: account.program, + parsed: serde_json::from_str(&account.parsed).expect("Unable to parse string as JSON"), + space: account.space, + } + } +} + +/// Represents an encoding format for Solana [account](https://solana.com/docs/references/terminology#account) data. +#[derive(Debug, Clone, Deserialize, Serialize, CandidType, PartialEq)] +pub enum AccountEncoding { + /// The account data is formatted as a binary string. + #[serde(rename = "binary")] + Binary, // Legacy. Retained for RPC backwards compatibility + /// The account data is formatted as a base-58 string. + #[serde(rename = "base58")] + Base58, + /// The account data is formatted as a base-64 string. + #[serde(rename = "base64")] + Base64, + /// The account data was first compressed using [Zstandard](http://facebook.github.io/zstd/) and the + /// result was then formatted as a base-64 string. + #[serde(rename = "base64+zstd")] + Base64Zstd, + /// The account data is formatted as a JSON string. + #[serde(rename = "jsonParsed")] + JsonParsed, +} + +impl From for AccountEncoding { + fn from(encoding: solana_account_decoder_client_types::UiAccountEncoding) -> Self { + use solana_account_decoder_client_types::UiAccountEncoding; + match encoding { + UiAccountEncoding::Binary => Self::Binary, + UiAccountEncoding::Base58 => Self::Base58, + UiAccountEncoding::Base64 => Self::Base64, + UiAccountEncoding::JsonParsed => Self::JsonParsed, + UiAccountEncoding::Base64Zstd => Self::Base64Zstd, + } + } +} + +impl From for solana_account_decoder_client_types::UiAccountEncoding { + fn from(encoding: AccountEncoding) -> Self { + match encoding { + AccountEncoding::Binary => Self::Binary, + AccountEncoding::Base58 => Self::Base58, + AccountEncoding::Base64 => Self::Base64, + AccountEncoding::JsonParsed => Self::JsonParsed, + AccountEncoding::Base64Zstd => Self::Base64Zstd, + } + } +}