Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
e12f0f0
XC-321: move retrieve_logs and verify_api_keys to setup
gregorydemay Apr 1, 2025
1e7fcd8
XC-321: client builder
gregorydemay Apr 1, 2025
30ca1aa
XC-321: mock API keys
gregorydemay Apr 1, 2025
513783b
XC-321: request builder
gregorydemay Apr 1, 2025
ff61b97
XC-321: refactor getSlot
gregorydemay Apr 1, 2025
5a72ec8
XC-321: refactor request
gregorydemay Apr 1, 2025
15a9fd6
XC-321: added docs
gregorydemay Apr 1, 2025
778d17f
XC-321: prod canister
gregorydemay Apr 1, 2025
e2ab03e
XC-321: examples
gregorydemay Apr 1, 2025
11b58b8
XC-321: fix docs
gregorydemay Apr 1, 2025
6cfd8c8
Merge branch 'main' into gdemay/XC-321-client-request-builder
gregorydemay Apr 1, 2025
3f1af2f
XC-321: generic over RPC config
gregorydemay Apr 1, 2025
620c38c
XC-321: refactor to use SolRpcRequest trait
gregorydemay Apr 1, 2025
7dd9a2a
XC-327: map client output
gregorydemay Mar 28, 2025
66f59c6
XC-327: requestCost skeletton
gregorydemay Mar 28, 2025
45b26ce
XC-327: PoC
gregorydemay Mar 31, 2025
c572adf
XC-321: getSlot test
gregorydemay Mar 31, 2025
05b753d
XC-321: sol_rpc_canister_cycles_balance method
gregorydemay Mar 31, 2025
597d88b
XC-321: integration test with cycles
gregorydemay Mar 31, 2025
cf1bffe
XC-321: Clone on RequestBuilder
gregorydemay Apr 2, 2025
8a06f7b
XC-321: fix GetSlotRpcConfig
gregorydemay Apr 2, 2025
9269669
XC-321: skeletton request_cost as part of RequestBuilder
gregorydemay Apr 2, 2025
ced0dd4
XC-321: add request cost
gregorydemay Apr 2, 2025
f862c27
Merge branch 'main' into gdemay/XC-321-cycles-cost
gregorydemay Apr 4, 2025
644c95b
XC-321: delete empty file
gregorydemay Apr 4, 2025
c69dad9
XC-321: ensure that cycles cost is tight
gregorydemay Apr 4, 2025
175c380
XC-321: remove useless logs
gregorydemay Apr 4, 2025
e5d5041
XC-321: refactor use MultiRpcRequest for getSlot
gregorydemay Apr 4, 2025
ea16dbf
XC-321: refactor use MultiRpcRequest for raw request
gregorydemay Apr 4, 2025
2b96a49
XC-321: move tests for Providers::new
gregorydemay Apr 4, 2025
bf19605
XC-321: clean-up
gregorydemay Apr 4, 2025
8fc1735
XC-321: rename request endpoint to jsonRequest
gregorydemay Apr 4, 2025
1e53cb7
XC-321: rename cost methods
gregorydemay Apr 4, 2025
2ff42ca
XC-321: jsonRequestCyclesCost
gregorydemay Apr 4, 2025
e6a4924
XC-321: rename
gregorydemay Apr 7, 2025
745915e
XC-321: generify cycles cost tests
gregorydemay Apr 7, 2025
4d8da70
XC-321: ensure that cycle tests do not get forgotten
gregorydemay Apr 7, 2025
7662a40
XC-321: update e2e examples
gregorydemay Apr 7, 2025
ec51585
Merge branch 'main' into gdemay/XC-321-cycles-cost
gregorydemay Apr 8, 2025
60bcdb9
XC-321: MultiRpcRequest for GetAccountInfo
gregorydemay Apr 8, 2025
2c77436
XC-321: fix candid interface
gregorydemay Apr 8, 2025
285d47f
XC-321: fix tests
gregorydemay Apr 8, 2025
9c75632
XC-321: fix getAccountInfo JSON response type
gregorydemay Apr 8, 2025
6ad0b91
XC-321: fix should_get_exact_cycles_cost
gregorydemay Apr 8, 2025
5964964
XC-321: rename to jsonRequest
gregorydemay Apr 8, 2025
1a71829
XC-321: added test should_be_zero_when_in_demo_mode
gregorydemay Apr 8, 2025
1e57fce
XC-321: fix cycles in examples
gregorydemay Apr 8, 2025
540f2f8
XC-321: refactor based on reviewers' feedback
gregorydemay Apr 8, 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
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.

8 changes: 3 additions & 5 deletions canister/scripts/examples.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ FLAGS="--network=$NETWORK --identity=$IDENTITY --wallet=$WALLET"
dfx canister call sol_rpc getProviders $FLAGS || exit 1

# Get the last finalized slot on Mainnet with a 2-out-of-3 strategy
# TODO XC-321: get cycle cost by query method
CYCLES="2B"
GET_SLOT_PARAMS="(
variant { Default = variant { Mainnet } },
opt record {
Expand All @@ -23,11 +21,10 @@ GET_SLOT_PARAMS="(
},
opt record { minContextSlot = null; commitment = opt variant { finalized } },
)"
CYCLES=$(dfx canister call sol_rpc getSlotCyclesCost "$GET_SLOT_PARAMS" $FLAGS --output json | jq '.Ok' --raw-output || exit 1)
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 the USDC mint account info on Mainnet with a 2-out-of-3 strategy
GET_ACCOUNT_INFO_PARAMS="(
variant { Default = variant { Mainnet } },
opt record {
Expand All @@ -44,4 +41,5 @@ GET_ACCOUNT_INFO_PARAMS="(
minContextSlot = null;
},
)"
CYCLES=$(dfx canister call sol_rpc getAccountInfoCyclesCost "$GET_ACCOUNT_INFO_PARAMS" $FLAGS --output json | jq '.Ok' --raw-output || exit 1)
dfx canister call sol_rpc getAccountInfo "$GET_ACCOUNT_INFO_PARAMS" $FLAGS --with-cycles "$CYCLES" || exit 1
15 changes: 11 additions & 4 deletions canister/sol_rpc_canister.did
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ type RejectionCode = variant {
CanisterReject;
};

// Cycles cost of a request made to the SOL RPC canister.
// E.g., the cycle cost for `getSlot` can be retrieved by calling `getSlotCyclesCost`.
type RequestCostResult = variant { Ok : nat; Err : RpcError };

// 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.
Expand Down Expand Up @@ -273,10 +277,10 @@ type GetSlotParams = record {
minContextSlot: opt nat64;
};

// Represents the result of a generic RPC request.
// Represents the result of a raw JSON-RPC request.
type RequestResult = variant { Ok : text; Err : RpcError };

// Represents an aggregated result from multiple RPC calls for a generic RPC request.
// Represents an aggregated result from multiple RPC calls for a raw JSON-RPC request.
type MultiRequestResult = variant {
Consistent : RequestResult;
Inconsistent : vec record { RpcSource; RequestResult };
Expand Down Expand Up @@ -337,10 +341,13 @@ service : (InstallArgs,) -> {

// Call the Solana `getAccountInfo` RPC method and return the resulting slot.
getAccountInfo : (RpcSources, opt RpcConfig, GetAccountInfoParams) -> (MultiGetAccountInfoResult);
getAccountInfoCyclesCost : (RpcSources, opt RpcConfig, GetAccountInfoParams) -> (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;

// Make a generic RPC request that sends the given json_rpc_payload.
request : (RpcSources, opt RpcConfig, json_rpc_paylod: text) -> (MultiRequestResult)
// Make a raw JSON-RPC request that sends the given json_rpc_payload.
jsonRequest : (RpcSources, opt RpcConfig, json_rpc_payload: text) -> (MultiRequestResult);
jsonRequestCyclesCost : (RpcSources, opt RpcConfig, json_rpc_payload: text) -> (RequestCostResult) query;
};
64 changes: 7 additions & 57 deletions canister/src/candid_rpc/mod.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
use crate::{
add_metric_entry,
metrics::RpcMethod,
providers::get_provider,
rpc_client::{ReducedResult, SolRpcClient},
types::RoundingError,
add_metric_entry, metrics::RpcMethod, providers::get_provider, rpc_client::ReducedResult,
util::hostname_from_url,
};
use canhttp::multi::ReductionError;
use serde::Serialize;
use sol_rpc_types::{
AccountInfo, GetAccountInfoParams, GetSlotParams, MultiRpcResult, RpcAccess, RpcAuth,
RpcConfig, RpcResult, RpcSource, RpcSources, Slot, SupportedRpcProvider,
MultiRpcResult, RpcAccess, RpcAuth, RpcError, RpcSource, SupportedRpcProvider,
};
use std::fmt::Debug;

fn process_result<T>(method: RpcMethod, result: ReducedResult<T>) -> MultiRpcResult<T> {
pub fn process_result<T>(method: RpcMethod, result: ReducedResult<T>) -> MultiRpcResult<T> {
match result {
Ok(value) => MultiRpcResult::Consistent(Ok(value)),
Err(err) => match err {
Expand All @@ -40,6 +33,10 @@ fn process_result<T>(method: RpcMethod, result: ReducedResult<T>) -> MultiRpcRes
}
}

pub fn process_error<T, E: Into<RpcError>>(error: E) -> MultiRpcResult<T> {
MultiRpcResult::Consistent(Err(error.into()))
}

pub fn hostname(provider: SupportedRpcProvider) -> Option<String> {
let url = match provider.access {
RpcAccess::Authenticated { auth, .. } => match auth {
Expand All @@ -50,50 +47,3 @@ pub fn hostname(provider: SupportedRpcProvider) -> Option<String> {
};
hostname_from_url(url.as_str())
}

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

impl CandidRpcClient {
pub fn new(source: RpcSources, config: Option<RpcConfig>) -> RpcResult<Self> {
Self::new_with_rounding_error(source, config, None)
}

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

pub async fn get_account_info(
&self,
params: GetAccountInfoParams,
) -> MultiRpcResult<Option<AccountInfo>> {
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<GetSlotParams>) -> MultiRpcResult<Slot> {
process_result(RpcMethod::GetSlot, self.client.get_slot(params).await)
}

pub async fn raw_request<I>(
&self,
request: canhttp::http::json::JsonRpcRequest<I>,
) -> MultiRpcResult<String>
where
I: Serialize + Clone + Debug,
{
process_result(RpcMethod::Generic, self.client.raw_request(request).await)
.map(|value| value.to_string())
}
}
21 changes: 17 additions & 4 deletions canister/src/http/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
mod errors;
pub mod errors;

use crate::{
add_metric_entry,
Expand Down Expand Up @@ -27,7 +27,7 @@ use canlog::log;
use http::{header::CONTENT_TYPE, HeaderValue};
use ic_cdk::api::management_canister::http_request::CanisterHttpRequestArgument;
use serde::{de::DeserializeOwned, Serialize};
use sol_rpc_types::{Mode, RpcError};
use sol_rpc_types::{JsonRpcError, RpcError};
use std::fmt::Debug;
use tower::{
layer::util::{Identity, Stack},
Expand All @@ -40,7 +40,7 @@ use tower_http::{set_header::SetRequestHeaderLayer, ServiceBuilderExt};
pub fn http_client<I, O>(
rpc_method: MetricRpcMethod,
retry: bool,
) -> impl Service<HttpJsonRpcRequest<I>, Response = HttpJsonRpcResponse<O>, Error = RpcError>
) -> impl Service<HttpJsonRpcRequest<I>, Response = O, Error = RpcError>
where
I: Serialize + Clone + Debug,
O: DeserializeOwned + Debug,
Expand All @@ -56,6 +56,7 @@ where
None
};
ServiceBuilder::new()
.map_result(extract_json_rpc_response)
.map_err(|e: HttpClientError| RpcError::from(e))
.option_layer(maybe_retry)
.option_layer(maybe_unique_id)
Expand Down Expand Up @@ -157,6 +158,18 @@ where
.service(canhttp::Client::new_with_error::<HttpClientError>())
}

fn extract_json_rpc_response<O>(
result: Result<HttpJsonRpcResponse<O>, RpcError>,
) -> Result<O, RpcError> {
match result?.into_body().into_result() {
Ok(value) => Ok(value),
Err(json_rpc_error) => Err(RpcError::JsonRpcError(JsonRpcError {
code: json_rpc_error.code,
message: json_rpc_error.message,
})),
}
}

fn generate_request_id<I>(request: HttpJsonRpcRequest<I>) -> HttpJsonRpcRequest<I> {
let (parts, mut body) = request.into_parts();
body.set_id(next_request_id());
Expand Down Expand Up @@ -220,7 +233,7 @@ impl ChargingPolicyWithCollateral {
fn new_from_state(s: &State) -> Self {
Self::new(
s.get_num_subnet_nodes(),
!matches!(s.get_mode(), Mode::Demo),
!s.is_demo_mode_active(),
COLLATERAL_CYCLES_PER_NODE,
)
}
Expand Down
99 changes: 68 additions & 31 deletions canister/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
use candid::candid_method;
use canhttp::http::json::JsonRpcRequest;
use canlog::{log, Log, Sort};
use ic_cdk::{api::is_controller, query, update};
use ic_metrics_encoder::MetricsEncoder;
use sol_rpc_canister::{
candid_rpc::CandidRpcClient,
candid_rpc::{process_error, process_result},
http_types, lifecycle,
logs::Priority,
memory::State,
memory::{mutate_state, read_state},
metrics::encode_metrics,
metrics::RpcMethod,
providers::{get_provider, PROVIDERS},
types::RoundingError,
rpc_client::MultiRpcRequest,
};
use sol_rpc_types::{
AccountInfo, GetAccountInfoParams, GetSlotParams, GetSlotRpcConfig, MultiRpcResult, RpcAccess,
RpcConfig, RpcError, RpcSources, Slot, SupportedRpcProvider, SupportedRpcProviderId,
RpcConfig, RpcResult, RpcSources, Slot, SupportedRpcProvider, SupportedRpcProviderId,
};
use std::str::FromStr;

Expand Down Expand Up @@ -81,10 +82,27 @@ async fn get_account_info(
config: Option<RpcConfig>,
params: GetAccountInfoParams,
) -> MultiRpcResult<Option<AccountInfo>> {
match CandidRpcClient::new(source, config) {
Ok(client) => client.get_account_info(params).await,
Err(err) => Err(err).into(),
match MultiRpcRequest::get_account_info(source, config.unwrap_or_default(), params) {
Ok(request) => {
process_result(RpcMethod::GetAccountInfo, request.send_and_reduce().await).into()
}
Err(e) => process_error(e),
}
}

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

#[update(name = "getSlot")]
Expand All @@ -94,42 +112,61 @@ async fn get_slot(
config: Option<GetSlotRpcConfig>,
params: Option<GetSlotParams>,
) -> MultiRpcResult<Slot> {
let rounding_error = config
.as_ref()
.and_then(|c| c.rounding_error)
.map(RoundingError::from);
match CandidRpcClient::new_with_rounding_error(
match MultiRpcRequest::get_slot(
source,
config.map(RpcConfig::from),
rounding_error,
config.unwrap_or_default(),
params.unwrap_or_default(),
) {
Ok(client) => client.get_slot(params).await,
Err(err) => Err(err).into(),
Ok(request) => process_result(RpcMethod::GetSlot, request.send_and_reduce().await),
Err(e) => process_error(e),
}
}

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

#[update]
#[candid_method]
async fn request(
#[update(name = "jsonRequest")]
#[candid_method(rename = "jsonRequest")]
async fn json_request(
source: RpcSources,
config: Option<RpcConfig>,
json_rpc_payload: String,
) -> MultiRpcResult<String> {
let request: JsonRpcRequest<serde_json::Value> = match serde_json::from_str(&json_rpc_payload) {
Ok(req) => req,
Err(e) => {
return Err(RpcError::ValidationError(format!(
"Invalid JSON RPC request: {e}"
)))
.into()
}
};
match CandidRpcClient::new(source, config) {
Ok(client) => client.raw_request(request).await,
Err(err) => Err(err).into(),
match MultiRpcRequest::json_request(source, config.unwrap_or_default(), json_rpc_payload) {
Ok(request) => process_result(RpcMethod::JsonRequest, request.send_and_reduce().await)
.map(|value| value.to_string()),
Err(e) => process_error(e),
}
}

#[query(name = "jsonRequestCyclesCost")]
#[candid_method(query, rename = "jsonRequestCyclesCost")]
async fn json_request_cycles_cost(
source: RpcSources,
config: Option<RpcConfig>,
json_rpc_payload: String,
) -> RpcResult<u128> {
MultiRpcRequest::json_request(source, config.unwrap_or_default(), json_rpc_payload)?
.cycles_cost()
.await
}

#[query(hidden = true)]
fn http_request(request: http_types::HttpRequest) -> http_types::HttpResponse {
match request.path() {
Expand Down
4 changes: 4 additions & 0 deletions canister/src/memory/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ impl State {
self.mode
}

pub fn is_demo_mode_active(&self) -> bool {
self.mode == Mode::Demo
}

pub fn set_mode(&mut self, mode: Mode) {
self.mode = mode
}
Expand Down
Loading