Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions Cargo.lock

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

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ solana-rpc-client-nonce-utils = "2.2.0"
solana-signature = "2.2.0"
solana-signer = "2.2.0"
solana-transaction = "2.2.0"
solana-transaction-context = "2.2.0"
solana-transaction-error = "2.2.0"
solana-transaction-status-client-types = "2.2.0"
strum = { version = "0.27.0", features = ["derive"] }
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ The SOL RPC canister reaches the Solana JSON-RPC providers using [HTTPS outcalls
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`)
[//]: # (TODO: XC-291: mention also `getSignatureStatuses#confirmations`)

## Reproducible Build

Expand Down
22 changes: 20 additions & 2 deletions canister/scripts/examples.sh
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,25 @@ GET_BLOCK_PARAMS="(
)"
CYCLES=$(dfx canister call sol_rpc getBlockCyclesCost "$GET_BLOCK_PARAMS" $FLAGS --output json | jq '.Ok' --raw-output || exit 1)
GET_BLOCK_OUTPUT=$(dfx canister call sol_rpc getBlock "$GET_BLOCK_PARAMS" $FLAGS --output json --with-cycles "$CYCLES" || exit 1)
SIGNATURE=$(jq --raw-output '.Consistent.Ok[0].signatures[0][0]' <<< "$GET_BLOCK_OUTPUT")
FIRST_SIGNATURE=$(jq --raw-output '.Consistent.Ok[0].signatures[0][0]' <<< "$GET_BLOCK_OUTPUT")
SECOND_SIGNATURE=$(jq --raw-output '.Consistent.Ok[0].signatures[0][1]' <<< "$GET_BLOCK_OUTPUT")

# Fetch the statuses of the first two transactions in the received block
GET_SIGNATURE_STATUSES_PARAMS="(
variant { Default = variant { Mainnet } },
opt record {
responseConsensus = opt variant {
Threshold = record { min = 2 : nat8; total = opt (3 : nat8) }
};
responseSizeEstimate = null;
},
record {
signatures = vec { \"${FIRST_SIGNATURE}\"; \"${SECOND_SIGNATURE}\" };
searchTransactionHistory = null;
},
)"
CYCLES=$(dfx canister call sol_rpc getSignatureStatusesCyclesCost "$GET_SIGNATURE_STATUSES_PARAMS" $FLAGS --output json | jq '.Ok' --raw-output || exit 1)
dfx canister call sol_rpc getSignatureStatuses "$GET_SIGNATURE_STATUSES_PARAMS" $FLAGS --with-cycles "$CYCLES" || exit 1

# Fetch the first transaction in the retrieved block
GET_TRANSACTION_PARAMS="(
Expand All @@ -73,7 +91,7 @@ GET_TRANSACTION_PARAMS="(
responseSizeEstimate = null;
},
record {
signature = \"${SIGNATURE}\";
signature = \"${FIRST_SIGNATURE}\";
commitment = opt variant { finalized };
encoding = opt variant{ base64 };
maxSupportedTransactionVersion = opt (0 : nat8);
Expand Down
63 changes: 61 additions & 2 deletions canister/sol_rpc_canister.did
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ type GetSlotRpcConfig = record {
// 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).
// 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;
Expand Down Expand Up @@ -420,19 +420,23 @@ type CompiledInstruction = record {
programIdIndex : nat8;
stackHeight : opt nat32;
};

// Encoding of a Solana transaction.
type EncodedTransaction = variant {
binary : record { text; variant { base58; base64 } };
legacyBinary : text;
};

// Specific operation executed by a Solana transaction.
type Instruction = variant { compiled : CompiledInstruction };

// List of inner instructions executed by a Solana transaction.
// See the [Solana documentation](https://solana.com/de/docs/rpc/json-structures#inner-instructions) for more details.
type InnerInstructions = record {
instructions : vec Instruction;
index : nat8;
};

// Errors that can occur during the execution of a specific instruction within a Solana transaction.
// See the [Solana documentation](https://github.com/solana-labs/solana/blob/7700cb3128c1f19820de67b81aa45d18f73d2ac0/sdk/program/src/instruction.rs#L33) for more details.
type InstructionError = variant {
Expand Down Expand Up @@ -491,8 +495,10 @@ type InstructionError = variant {
ReadonlyLamportChange;
InsufficientFunds;
};

// Transaction addresses loaded from address lookup tables.
type LoadedAddresses = record { writable : vec Pubkey; readonly : vec Pubkey };

// Reward or penalty applied to an account for fees, rent, voting, or staking activity.
type Reward = record {
lamports : int64;
Expand All @@ -501,6 +507,7 @@ type Reward = record {
rewardType : opt variant { fee; rent; voting; staking };
postBalance : nat64;
};

// A human-readable representation of a token amount.
type TokenAmount = record {
decimals : nat8;
Expand Down Expand Up @@ -555,6 +562,7 @@ type TransactionError = variant {
CommitCancelled;
BlockhashNotFound;
};

// The result of a Solana `getTransaction` RPC method call.
type TransactionInfo = record {
// Estimated production time, as Unix timestamp (seconds since the Unix epoch) of when the transaction was processed.
Expand All @@ -570,6 +578,7 @@ type TransactionInfo = record {
// Undefined if `maxSupportedTransactionVersion` is not set in request params.
version : opt variant { legacy; number : nat8 };
};

// Transaction status metadata.
// See the [Solana documentation](https://solana.com/docs/rpc/json-structures#transaction-status-metadata) for more details.
type TransactionStatusMeta = record {
Expand All @@ -586,6 +595,7 @@ type TransactionStatusMeta = record {
preTokenBalances : opt vec TransactionTokenBalance;
computeUnitsConsumed : opt nat64;
};

// Balance of a specific SPL token account.
type TransactionTokenBalance = record {
owner : opt Pubkey;
Expand All @@ -595,6 +605,43 @@ type TransactionTokenBalance = record {
uiTokenAmount : TokenAmount;
};

// Solana transaction status as returned by the `getSignatureStatuses` RPC method.
//
// *WARNING*: The optional `confirmations` field in the `getSignatureStatuses` response is not
// included in this type. This value is ignored when processing the RPC response because it
// changes with every Solana block (approximately every 400ms) which is too quick to achieve
// consensus between the different nodes performing the request.
type TransactionStatus = record {
// The slot the transaction was processed.
slot: Slot;
// *DEPRECATED*: Transaction status:
// * Ok - Transaction was successful
// * Err - Transaction failed with `TransactionError`
status: variant { Ok; Err : TransactionError };
// Error if transaction failed, null if transaction succeeded.
err: opt TransactionError;
// The transaction's cluster confirmation status; Either `processed`, `confirmed`, or `finalized`.
// See [Commitment](https://solana.com/docs/rpc#configuring-state-commitment) for more on
// optimistic confirmation.
confirmationStatus: opt TransactionConfirmationStatus;
};

// A Solana transaction confirmation status.
type TransactionConfirmationStatus = variant {
processed;
confirmed;
finalized;
};

// Represents the result of a call to the `getSignatureStatuses` Solana RPC method.
type GetSignatureStatusesResult = variant { Ok : vec opt TransactionStatus; Err : RpcError };

// Represents an aggregated result from multiple RPC calls to the `getSignatureStatuses` Solana RPC method.
type MultiGetSignatureStatusesResult = variant {
Consistent : GetSignatureStatusesResult;
Inconsistent : vec record { RpcSource; GetSignatureStatusesResult };
};

// Represents the result of a call to the `getTransaction` Solana RPC method.
type GetTransactionResult = variant { Ok : opt TransactionInfo; Err : RpcError };

Expand Down Expand Up @@ -671,6 +718,14 @@ type CommitmentLevel = variant {
// 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 `getSignatureStatuses` Solana RPC method.
type GetSignatureStatusesParams = record {
// An array of transaction signatures to confirm, as base-58 encoded strings (up to a maximum of 256)
signatures: vec Signature;
// If set to true, a Solana node will search its ledger cache for any signatures not found in the recent status cache.
searchTransactionHistory: opt bool;
};

// The parameters for a call to the `getSlot` Solana RPC method.
type GetSlotParams = record {
commitment: opt CommitmentLevel;
Expand Down Expand Up @@ -750,11 +805,15 @@ 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 `getSignatureStatuses` RPC method and return the resulting statuses.
getSignatureStatuses : (RpcSources, opt RpcConfig, GetSignatureStatusesParams) -> (MultiGetSignatureStatusesResult);
getSignatureStatusesCyclesCost : (RpcSources, opt RpcConfig, GetSignatureStatusesParams) -> (RequestCostResult) query;

// Call the Solana `getSlot` RPC method and return the resulting slot.
getSlot : (RpcSources, opt GetSlotRpcConfig, opt GetSlotParams) -> (MultiGetSlotResult);
getSlotCyclesCost : (RpcSources, opt GetSlotRpcConfig, opt GetSlotParams) -> (RequestCostResult) query;
Expand Down
36 changes: 32 additions & 4 deletions canister/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ use sol_rpc_canister::{
};
use sol_rpc_types::{
AccountInfo, ConfirmedBlock, GetAccountInfoParams, GetBalanceParams, GetBlockParams,
GetRecentPrioritizationFeesParams, GetRecentPrioritizationFeesRpcConfig, GetSlotParams,
GetSlotRpcConfig, GetTokenAccountBalanceParams, GetTransactionParams, Lamport, MultiRpcResult,
PrioritizationFee, RpcAccess, RpcConfig, RpcResult, RpcSources, SendTransactionParams,
Signature, Slot, SupportedRpcProvider, SupportedRpcProviderId, TokenAmount, TransactionInfo,
GetRecentPrioritizationFeesParams, GetRecentPrioritizationFeesRpcConfig,
GetSignatureStatusesParams, GetSlotParams, GetSlotRpcConfig, GetTokenAccountBalanceParams,
GetTransactionParams, Lamport, MultiRpcResult, PrioritizationFee, RpcAccess, RpcConfig,
RpcResult, RpcSources, SendTransactionParams, Signature, Slot, SupportedRpcProvider,
SupportedRpcProviderId, TokenAmount, TransactionInfo, TransactionStatus,
};
use std::str::FromStr;

Expand Down Expand Up @@ -189,6 +190,33 @@ async fn get_recent_prioritization_fees_cycles_cost(
.await
}

#[update(name = "getSignatureStatuses")]
#[candid_method(rename = "getSignatureStatuses")]
async fn get_signature_statuses(
source: RpcSources,
config: Option<RpcConfig>,
params: GetSignatureStatusesParams,
) -> MultiRpcResult<Vec<Option<TransactionStatus>>> {
let request =
MultiRpcRequest::get_signature_statuses(source, config.unwrap_or_default(), params);
send_multi(request).await.into()
}

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

#[update(name = "getSlot")]
#[candid_method(rename = "getSlot")]
async fn get_slot(
Expand Down
6 changes: 4 additions & 2 deletions canister/src/rpc_client/cbor/rounding_error.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use minicbor::decode::Decoder;
use minicbor::encode::{Encoder, Write};
use minicbor::{
decode::Decoder,
encode::{Encoder, Write},
};
use sol_rpc_types::RoundingError;

pub fn decode<Ctx>(
Expand Down
6 changes: 4 additions & 2 deletions canister/src/rpc_client/cbor/tests.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use minicbor::{Decode, Encode};
use proptest::prelude::{any, TestCaseError};
use proptest::{prop_assert_eq, proptest};
use proptest::{
prelude::{any, TestCaseError},
prop_assert_eq, proptest,
};
use sol_rpc_types::RoundingError;

proptest! {
Expand Down
38 changes: 37 additions & 1 deletion canister/src/rpc_client/json/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use derive_more::From;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use sol_rpc_types::{
CommitmentLevel, DataSlice, GetAccountInfoEncoding, GetBlockCommitmentLevel,
GetTransactionEncoding, Pubkey, SendTransactionEncoding, Slot, TransactionDetails,
GetTransactionEncoding, Pubkey, SendTransactionEncoding, Signature, Slot, TransactionDetails,
};
use solana_transaction_status_client_types::UiTransactionEncoding;

Expand Down Expand Up @@ -180,6 +181,41 @@ impl From<sol_rpc_types::GetRecentPrioritizationFeesParams> for GetRecentPriorit
}
}

#[skip_serializing_none]
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(into = "(Vec<Signature>, Option<GetSignatureStatusesConfig>)")]
pub struct GetSignatureStatusesParams(Vec<Signature>, Option<GetSignatureStatusesConfig>);

impl GetSignatureStatusesParams {
pub fn num_signatures(&self) -> usize {
self.0.len()
}
}

impl From<sol_rpc_types::GetSignatureStatusesParams> for GetSignatureStatusesParams {
fn from(params: sol_rpc_types::GetSignatureStatusesParams) -> Self {
Self(
params.signatures.into(),
params
.search_transaction_history
.map(GetSignatureStatusesConfig::from),
)
}
}

impl From<GetSignatureStatusesParams> for (Vec<Signature>, Option<GetSignatureStatusesConfig>) {
fn from(params: GetSignatureStatusesParams) -> Self {
(params.0, params.1)
}
}

#[skip_serializing_none]
#[derive(Serialize, Deserialize, Clone, Debug, From)]
pub struct GetSignatureStatusesConfig {
#[serde(rename = "searchTransactionHistory")]
pub search_transaction_history: bool,
}

#[skip_serializing_none]
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(into = "(String, Option<GetTokenAccountBalanceConfig>)")]
Expand Down
28 changes: 28 additions & 0 deletions canister/src/rpc_client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,34 @@ impl GetBlockRequest {
}
}

pub type GetSignatureStatusesRequest = MultiRpcRequest<
json::GetSignatureStatusesParams,
Vec<Option<solana_transaction_status_client_types::TransactionStatus>>,
>;

impl GetSignatureStatusesRequest {
pub fn get_signature_statuses<Params: Into<json::GetSignatureStatusesParams>>(
rpc_sources: RpcSources,
config: RpcConfig,
params: Params,
) -> Result<Self, ProviderError> {
let params = params.into();
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(128 + (params.num_signatures() as u64 * 256) + HEADER_SIZE_LIMIT);

Ok(MultiRpcRequest::new(
providers,
JsonRpcRequest::new("getSignatureStatuses", params),
max_response_bytes,
ResponseTransform::GetSignatureStatuses,
ReductionStrategy::from(consensus_strategy),
))
}
}

pub type GetSlotRequest = MultiRpcRequest<json::GetSlotParams, Slot>;

impl GetSlotRequest {
Expand Down
Loading