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
536 changes: 356 additions & 180 deletions Cargo.lock

Large diffs are not rendered by default.

14 changes: 13 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,30 @@ opt-level = 's'
inherits = "release"

[workspace.dependencies]
assert_matches = "1.5.0"
async-trait = "0.1.88"
candid = "0.10.13"
candid_parser = "0.1.4"
canhttp = { git = "https://github.com/dfinity/evm-rpc-canister", rev = "c1d9a12eb9d3af92671dc07963c8887ec178fcbc" }
ciborium = "0.2.2"
# Transitive dependency of ic-ed25519
# See https://forum.dfinity.org/t/module-imports-function-wbindgen-describe-from-wbindgen-placeholder-that-is-not-exported-by-the-runtime/11545/8
const_format = "0.2.34"
derive_more = { version = "2.0.1", features = ["from"] }
futures = "0.3.31"
getrandom = { version = "*", default-features = false, features = ["custom"] }
hex = "0.4.3"
http = "1.2.0"
ic-canister-log = "0.2.0"
ic-cdk = "0.17.1"
ic-ed25519 = "0.1.0"
ic-stable-structures = "0.6.8"
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"
minicbor = { version = "0.26.1", features = ["alloc", "derive"] }
num = "0.4.3"
num-traits = "0.2.19"
pocket-ic = "7.0.0"
proptest = "1.6.0"
regex = "1.11.1"
Expand All @@ -60,7 +68,11 @@ solana-pubkey = "2.2.0"
solana-signature = "2.2.0"
solana-transaction = { version = "2.2.0", features = ["bincode"] }
strum = { version = "0.27.0", features = ["derive"] }
thiserror = "1.0.69"
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"] }

Expand Down
14 changes: 13 additions & 1 deletion canister/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,33 @@ name = "sol_rpc_canister"
path = "src/main.rs"

[dependencies]
assert_matches = { workspace = true }
candid = { workspace = true }
canhttp = { workspace = true, features = ["json"] }
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 }
ic-sha3 = { workspace = true }
ic-stable-structures = { workspace = true }
maplit = { workspace = true }
minicbor = { workspace = true }
num-traits = { workspace = true }
solana-clock = { workspace = true }
sol_rpc_types = { path = "../libs/types" }
regex = { workspace = true }
serde = { workspace = true }
serde_json = {workspace = true}
serde_json = { workspace = true }
serde_bytes = { workspace = true }
tower = { workspace = true }
tower-http = { workspace = true, features = ["set-header", "util"] }
url = { workspace = true }
zeroize = { workspace = true }
thiserror = { workspace = true }

[dev-dependencies]
candid_parser = { workspace = true }
Expand Down
85 changes: 81 additions & 4 deletions canister/sol_rpc_canister.did
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,70 @@ type HttpHeader = record {
name : text
};

// Represents an error that occurred while trying to perform an RPC call.
type RpcError = variant {
JsonRpcError : JsonRpcError;
ProviderError : ProviderError;
ValidationError : text;
HttpOutcallError : HttpOutcallError;
};

// Represents a JSON-RPC error.
type JsonRpcError = record { code : int64; message : text };

// Represents an error with an RPC provider.
type ProviderError = variant {
TooFewCycles : record { expected : nat; received : nat };
InvalidRpcConfig : text;
UnsupportedCluster : text;
};

// Represents an HTTP outcall error.
type HttpOutcallError = variant {
IcError : record { code: RejectionCode; message: text };
InvalidHttpJsonRpcResponse : record {
status : nat16;
body : text;
parsingError : opt text;
};
};

// Represents an IC rejection code for an HTTP outcall.
type RejectionCode = variant {
NoError;
CanisterError;
SysTransient;
DestinationInvalid;
Unknown;
SysFatal;
CanisterReject;
};

// Represents a Solana slot
type Slot = nat64;

// Represents the result of a call to the `getSlot` Solana RPC method.
type GetSlotResult = variant { Ok : Slot; Err : RpcError };

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

// Commitment levels in Solana, representing finality guarantees of transactions and state queries.
type CommitmentLevel = variant {
processed;
confirmed;
finalized;
};

// The parameters for a call to the `getSlot` Solana RPC method.
type GetSlotParams = record {
commitment: opt CommitmentLevel;
minContextSlot: opt nat64;
};

// A string used as a regex pattern.
type Regex = text;

Expand All @@ -108,11 +172,24 @@ type LogFilter = variant {
HidePattern : Regex;
};

/// The installation args for the Solana RPC canister.
// The number of nodes in the subnet
type NumSubnetNodes = nat32;

// The canister operation mode. Default is 'Normal'.
type Mode = variant {
// Normal mode, where cycle payment is required for certain operations.
Normal;
// Demo mode, where cycle payment is not required.
Demo;
};

// The installation args for the Solana RPC canister.
type InstallArgs = record {
manageApiKeys: opt vec principal;
overrideProvider: opt OverrideProvider;
logFilter: opt LogFilter;
numSubnetNodes : opt NumSubnetNodes;
mode : opt Mode;
};

service : (InstallArgs,) -> {
Expand All @@ -123,9 +200,9 @@ service : (InstallArgs,) -> {
//
// # Preconditions
//
// The caller is the controller or a principal specified in `Installargs::manage_api_keys`.
// The caller is the controller or a principal specified in `InstallArgs::manage_api_keys`.
updateApiKeys : (vec record { SupportedProvider; opt text }) -> ();

// TODO XC-292: change signature
getSlot : (RpcSources, opt RpcConfig) -> (nat64);
// Call the Solana `getSlot` RPC method and return the resulting slot.
getSlot : (RpcSources, opt RpcConfig, opt GetSlotParams) -> (MultiGetSlotResult);
};
39 changes: 39 additions & 0 deletions canister/src/candid_rpc/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use crate::{
rpc_client::{MultiCallError, SolRpcClient},
types::MultiRpcResult,
};
use sol_rpc_types::{GetSlotParams, RpcConfig, RpcResult, RpcSources};
use solana_clock::Slot;

fn process_result<T>(result: Result<T, MultiCallError<T>>) -> MultiRpcResult<T> {
match result {
Ok(value) => MultiRpcResult::Consistent(Ok(value)),
Err(err) => match err {
MultiCallError::ConsistentError(err) => MultiRpcResult::Consistent(Err(err)),
MultiCallError::InconsistentResults(multi_call_results) => {
let results = multi_call_results.into_vec();
results.iter().for_each(|(_service, _service_result)| {
// TODO XC-296: Add metrics for inconsistent providers
});
MultiRpcResult::Inconsistent(results)
}
},
}
}

/// Adapt the `EthRpcClient` to the `Candid` interface used by the EVM-RPC canister.
pub struct CandidRpcClient {
client: SolRpcClient,
}

impl CandidRpcClient {
pub fn new(source: RpcSources, config: Option<RpcConfig>) -> RpcResult<Self> {
Ok(Self {
client: SolRpcClient::new(source, config)?,
})
}

pub async fn get_slot(&self, params: GetSlotParams) -> MultiRpcResult<Slot> {
process_result(self.client.get_slot(params).await)
}
}
6 changes: 6 additions & 0 deletions canister/src/constants.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
// Cycles (per node) which must be passed with each RPC request
// as processing fee.
pub const COLLATERAL_CYCLES_PER_NODE: u128 = 10_000_000;

pub const CONTENT_TYPE_VALUE: &str = "application/json";

pub const API_KEY_REPLACE_STRING: &str = "{API_KEY}";
pub const API_KEY_MAX_SIZE: usize = 512;
pub const VALID_API_KEY_CHARS: &str =
Expand Down
88 changes: 88 additions & 0 deletions canister/src/http/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
use canhttp::{
http::{
json::{JsonRequestConversionError, JsonResponseConversionError},
FilterNonSuccessfulHttpResponseError, HttpRequestConversionError,
HttpResponseConversionError,
},
CyclesAccountingError, HttpsOutcallError, IcError,
};
use derive_more::From;
use sol_rpc_types::{HttpOutcallError, ProviderError, RpcError};
use thiserror::Error;

#[derive(Clone, Debug, Error, From)]
pub enum HttpClientError {
#[error("IC error: {0}")]
IcError(IcError),
#[error("unknown error (most likely sign of a bug): {0}")]
NotHandledError(String),
#[error("cycles accounting error: {0}")]
CyclesAccountingError(CyclesAccountingError),
#[error("HTTP response was not successful: {0}")]
UnsuccessfulHttpResponse(FilterNonSuccessfulHttpResponseError<Vec<u8>>),
#[error("Error converting response to JSON: {0}")]
InvalidJsonResponse(JsonResponseConversionError),
}

impl From<HttpRequestConversionError> for HttpClientError {
fn from(value: HttpRequestConversionError) -> Self {
HttpClientError::NotHandledError(value.to_string())
}
}

impl From<HttpResponseConversionError> for HttpClientError {
fn from(value: HttpResponseConversionError) -> Self {
// Replica should return valid http::Response
HttpClientError::NotHandledError(value.to_string())
}
}

impl From<JsonRequestConversionError> for HttpClientError {
fn from(value: JsonRequestConversionError) -> Self {
HttpClientError::NotHandledError(value.to_string())
}
}

impl From<HttpClientError> for RpcError {
fn from(error: HttpClientError) -> Self {
match error {
HttpClientError::IcError(IcError { code, message }) => {
RpcError::HttpOutcallError(HttpOutcallError::IcError { code, message })
}
HttpClientError::NotHandledError(e) => RpcError::ValidationError(e),
HttpClientError::CyclesAccountingError(
CyclesAccountingError::InsufficientCyclesError { expected, received },
) => RpcError::ProviderError(ProviderError::TooFewCycles { expected, received }),
HttpClientError::InvalidJsonResponse(
JsonResponseConversionError::InvalidJsonResponse {
status,
body,
parsing_error,
},
) => RpcError::HttpOutcallError(HttpOutcallError::InvalidHttpJsonRpcResponse {
status,
body,
parsing_error: Some(parsing_error),
}),
HttpClientError::UnsuccessfulHttpResponse(
FilterNonSuccessfulHttpResponseError::UnsuccessfulResponse(response),
) => RpcError::HttpOutcallError(HttpOutcallError::InvalidHttpJsonRpcResponse {
status: response.status().as_u16(),
body: String::from_utf8_lossy(response.body()).to_string(),
parsing_error: None,
}),
}
}
}

impl HttpsOutcallError for HttpClientError {
fn is_response_too_large(&self) -> bool {
match self {
HttpClientError::IcError(e) => e.is_response_too_large(),
HttpClientError::NotHandledError(_)
| HttpClientError::CyclesAccountingError(_)
| HttpClientError::UnsuccessfulHttpResponse(_)
| HttpClientError::InvalidJsonResponse(_) => false,
}
}
}
Loading