Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
4 changes: 3 additions & 1 deletion Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ 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" }
canhttp = { git = "https://github.com/dfinity/evm-rpc-canister", rev = "1aeeca3bdcb86ce4493b71e4117f65ab78475396" }
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
Expand Down
2 changes: 1 addition & 1 deletion canister/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ thiserror = { workspace = true }

[dev-dependencies]
candid_parser = { workspace = true }
proptest = { workspace = true }
proptest = { workspace = true }
8 changes: 7 additions & 1 deletion canister/sol_rpc_canister.did
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ type GetSlotParams = record {
minContextSlot: opt nat64;
};

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

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

Expand Down Expand Up @@ -205,4 +208,7 @@ service : (InstallArgs,) -> {

// Call the Solana `getSlot` RPC method and return the resulting slot.
getSlot : (RpcSources, opt RpcConfig, opt GetSlotParams) -> (MultiGetSlotResult);
};

// Make a generic RPC request that sends the given json_rpc_payload.
request : (RpcSource, json_rpc_paylod: text, max_response_bytes: nat64) -> (RequestResult)
};
1 change: 1 addition & 0 deletions canister/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// as processing fee.
pub const COLLATERAL_CYCLES_PER_NODE: u128 = 10_000_000;

pub const CONTENT_TYPE_HEADER_LOWERCASE: &str = "content-type";
pub const CONTENT_TYPE_VALUE: &str = "application/json";

pub const API_KEY_REPLACE_STRING: &str = "{API_KEY}";
Expand Down
9 changes: 8 additions & 1 deletion canister/src/http/errors.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use canhttp::{
http::{
json::{JsonRequestConversionError, JsonResponseConversionError},
json::{
ConsistentResponseIdFilterError, JsonRequestConversionError,
JsonResponseConversionError,
},
FilterNonSuccessfulHttpResponseError, HttpRequestConversionError,
HttpResponseConversionError,
},
Expand All @@ -22,6 +25,8 @@ pub enum HttpClientError {
UnsuccessfulHttpResponse(FilterNonSuccessfulHttpResponseError<Vec<u8>>),
#[error("Error converting response to JSON: {0}")]
InvalidJsonResponse(JsonResponseConversionError),
#[error("Invalid JSON-RPC response ID: {0}")]
InvalidJsonResponseId(ConsistentResponseIdFilterError),
}

impl From<HttpRequestConversionError> for HttpClientError {
Expand Down Expand Up @@ -71,6 +76,7 @@ impl From<HttpClientError> for RpcError {
body: String::from_utf8_lossy(response.body()).to_string(),
parsing_error: None,
}),
HttpClientError::InvalidJsonResponseId(e) => RpcError::ValidationError(e.to_string()),
}
}
}
Expand All @@ -82,6 +88,7 @@ impl HttpsOutcallError for HttpClientError {
HttpClientError::NotHandledError(_)
| HttpClientError::CyclesAccountingError(_)
| HttpClientError::UnsuccessfulHttpResponse(_)
| HttpClientError::InvalidJsonResponseId(_)
| HttpClientError::InvalidJsonResponse(_) => false,
}
}
Expand Down
4 changes: 3 additions & 1 deletion canister/src/http/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ use canhttp::{
convert::ConvertRequestLayer,
http::{
json::{
HttpJsonRpcRequest, HttpJsonRpcResponse, JsonRequestConverter, JsonResponseConverter,
CreateJsonRpcIdFilter, HttpJsonRpcRequest, HttpJsonRpcResponse, JsonRequestConverter,
JsonResponseConverter,
},
FilterNonSuccessfulHttpResponse, HttpRequestConverter, HttpResponseConverter,
},
Expand Down Expand Up @@ -73,6 +74,7 @@ where
);
}),
)
.filter_response(CreateJsonRpcIdFilter::new())
.layer(service_request_builder())
.convert_response(JsonResponseConverter::new())
.convert_response(FilterNonSuccessfulHttpResponse)
Expand Down
20 changes: 18 additions & 2 deletions canister/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ use sol_rpc_canister::{
http_types, lifecycle,
logs::Priority,
providers::{get_provider, PROVIDERS},
rpc_client,
state::{mutate_state, read_state},
};
use sol_rpc_types::{
GetSlotParams, MultiRpcResult, RpcAccess, RpcConfig, RpcSources, SupportedRpcProvider,
SupportedRpcProviderId,
GetSlotParams, MultiRpcResult, RpcAccess, RpcConfig, RpcError, RpcResult, RpcSource,
RpcSources, SupportedRpcProvider, SupportedRpcProviderId,
};
use solana_clock::Slot;
use std::str::FromStr;
Expand Down Expand Up @@ -84,6 +85,21 @@ async fn get_slot(
}
}

#[update]
#[candid_method]
async fn request(
provider: RpcSource,
json_rpc_payload: String,
max_response_bytes: u64,
) -> RpcResult<String> {
let request: canhttp::http::json::JsonRpcRequest<serde_json::Value> =
serde_json::from_str(&json_rpc_payload)
.map_err(|e| RpcError::ValidationError(format!("Invalid JSON RPC request: {e}")))?;
rpc_client::call(&provider, request, max_response_bytes)
.await
.map(|value: serde_json::Value| value.to_string())
}

#[query(hidden = true)]
fn http_request(request: http_types::HttpRequest) -> http_types::HttpResponse {
match request.path() {
Expand Down
27 changes: 24 additions & 3 deletions canister/src/rpc_client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::{
providers::Providers,
rpc_client::sol_rpc::{ResponseSizeEstimate, ResponseTransform, HEADER_SIZE_LIMIT},
};
use canhttp::http::json::JsonRpcRequest;
use canlog::log;
use serde::{de::DeserializeOwned, Serialize};
use sol_rpc_types::{
Expand All @@ -19,6 +20,25 @@ use std::{
fmt::Debug,
};

pub async fn call<I, O>(
provider: &RpcSource,
request: JsonRpcRequest<I>,
max_response_size: u64,
) -> Result<O, RpcError>
where
I: Serialize + Clone + Debug,
O: Debug + DeserializeOwned,
{
sol_rpc::call::<_, _>(
false,
provider,
request,
ResponseSizeEstimate::new(max_response_size),
&Some(ResponseTransform::Raw),
)
.await
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SolRpcClient {
providers: Providers,
Expand Down Expand Up @@ -58,7 +78,7 @@ impl SolRpcClient {
/// there is no single point of failure.
async fn parallel_call<I, O>(
&self,
method: impl Into<String> + Clone,
method: impl Into<String>,
params: I,
response_size_estimate: ResponseSizeEstimate,
response_transform: &Option<ResponseTransform>,
Expand All @@ -68,6 +88,7 @@ impl SolRpcClient {
O: Debug + DeserializeOwned,
{
let providers = self.providers();
let request = JsonRpcRequest::new(method, params);
let results = {
let mut fut = Vec::with_capacity(providers.len());
for provider in providers {
Expand All @@ -78,9 +99,9 @@ impl SolRpcClient {
);
fut.push(async {
sol_rpc::call::<_, _>(
true,
provider,
method.clone(),
params.clone(),
request.clone(),
response_size_estimate,
response_transform,
)
Expand Down
26 changes: 20 additions & 6 deletions canister/src/rpc_client/sol_rpc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,26 +41,40 @@ pub const MAX_PAYLOAD_SIZE: u64 = HTTP_MAX_SIZE - HEADER_SIZE_LIMIT;
pub enum ResponseTransform {
#[n(0)]
GetSlot,
#[n(1)]
Raw,
}

impl ResponseTransform {
fn apply(&self, body_bytes: &mut Vec<u8>) {
use serde_json::{from_slice, to_vec, Value};

fn redact_response<T>(body: &mut Vec<u8>)
where
T: Serialize + DeserializeOwned,
{
let response: JsonRpcResponse<T> = match serde_json::from_slice(body) {
let response: JsonRpcResponse<T> = match from_slice(body) {
Ok(response) => response,
Err(_) => return,
};
*body = serde_json::to_vec(&response).expect("BUG: failed to serialize response");
*body = to_vec(&response).expect("BUG: failed to serialize response");
}

fn canonicalize(text: &[u8]) -> Option<Vec<u8>> {
let json = from_slice::<Value>(text).ok()?;
to_vec(&json).ok()
}

match self {
// TODO XC-292: Add rounding to the response transform and
// add a unit test simulating consensus when the providers
// return slightly differing results.
Self::GetSlot => redact_response::<Slot>(body_bytes),
Self::Raw => {
if let Some(bytes) = canonicalize(body_bytes) {
*body_bytes = bytes
}
}
}
}
}
Expand Down Expand Up @@ -104,9 +118,9 @@ impl fmt::Display for ResponseSizeEstimate {

/// Calls a JSON-RPC method at the specified URL.
pub async fn call<I, O>(
retry: bool,
provider: &RpcSource,
method: impl Into<String>,
params: I,
request_body: JsonRpcRequest<I>,
response_size_estimate: ResponseSizeEstimate,
response_transform: &Option<ResponseTransform>,
) -> Result<O, RpcError>
Expand Down Expand Up @@ -135,10 +149,10 @@ where
"cleanup_response".to_owned(),
transform_op.clone(),
))
.body(JsonRpcRequest::new(method, params))
.body(request_body)
.expect("BUG: invalid request");

let mut client = http_client(true);
let mut client = http_client(retry);
let response = client.call(request).await?;
match response.into_body().into_result() {
Ok(r) => Ok(r),
Expand Down
1 change: 1 addition & 0 deletions integration_tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ sol_rpc_client = { path = "../libs/client" }
sol_rpc_types = { path = "../libs/types" }

[dev-dependencies]
assert_matches = { workspace = true }
futures = { workspace = true }
solana-client = { workspace = true }
tokio = { workspace = true }
Loading