Skip to content

feat: Implement a generic request canister interface#18

Closed
ninegua wants to merge 14 commits intomainfrom
paulliu/generic-canister-request
Closed

feat: Implement a generic request canister interface#18
ninegua wants to merge 14 commits intomainfrom
paulliu/generic-canister-request

Conversation

@ninegua
Copy link
Member

@ninegua ninegua commented Feb 25, 2025

Implement a generic request interface to query Solana API.

@ninegua ninegua force-pushed the paulliu/generic-canister-request branch from b34110b to 9713a60 Compare February 25, 2025 13:11
mod rpc_client;

pub use lifecycle::InstallArgs;
pub use evm_rpc_types::{
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it directly re-exports a few definitions from evm_rpc_types, which are generic enough and require no modification.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As commented above, I would personally avoid having direct dependencies on EVM RPC if possible. If anything I would maybe move some of the types to a new crate that can be shared between the two repositories.

async-trait = "0.1.86"
candid = "0.10.13"
candid_parser = "0.1.4"
canhttp = { git = "https://github.com/dfinity/evm-rpc-canister", rev = "bd02c54bba5d4df424909cbf809f5a1a07739467" }
Copy link
Member Author

@ninegua ninegua Feb 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not the latest, but maybe we can wait a bit until canhttp stabilizes?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, this is totally fine. Just FRY I think there are still 2 things missing from canhttp to be fully functional:

  1. JSON-RPC layer
  2. Retry layer, double max response size if request failed because response was too big.

@ninegua ninegua marked this pull request as ready for review February 28, 2025 13:42
@ninegua ninegua requested a review from a team as a code owner February 28, 2025 13:42
Copy link
Contributor

@gregorydemay gregorydemay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot @ninegua for this PR! A preliminary review

candid_parser = "0.1.4"
canhttp = { git = "https://github.com/dfinity/evm-rpc-canister", rev = "bd02c54bba5d4df424909cbf809f5a1a07739467" }
ciborium = "0.2.2"
evm_rpc_types = "1.3.0"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

understanding question: is-it mean to be temporary (e.g. until canhttp stabilizes)? I think a dependency there on an EVM-related crate is unexpected.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few types that are not EVM specific and have been used here with no modification:

HttpOutcallError, JsonRpcError, MultiRpcResult, ProviderError, RpcError, RpcResult, ValidationError

I think it is best to avoid duplication. But at the same time I don't know if they warrant a separate crate. What's your thought on this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would start by duplicating the types and removing everything that we currently don't need:

  1. The error types in the EVM RPC canister grew historically and where initially implemented when providers could be added dynamically which is no longer the case and not something that we want to support with SOL RPC. That means that some error variants are useless (e.g. ProviderError::NoPermission, ValidationError::InvalidHex) and I wouldn't want to start a new project with some tech debt of another project.
  2. Generally I think it will probably make sense to have a generic RPC canister crate to put those common types as well as common logic regarding reduction of multiple results but I have the impression that's something that can wait.

Does it make sense?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also avoid direct dependencies on EVM RPC unless they are meant to be temporary, but then I would for sure add a TODO so we don't forget to remove the dependencies. Otherwise ideally I would just copy the necessary error types as suggested by Grégory.

async-trait = "0.1.86"
candid = "0.10.13"
candid_parser = "0.1.4"
canhttp = { git = "https://github.com/dfinity/evm-rpc-canister", rev = "bd02c54bba5d4df424909cbf809f5a1a07739467" }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, this is totally fine. Just FRY I think there are still 2 things missing from canhttp to be fully functional:

  1. JSON-RPC layer
  2. Retry layer, double max response size if request failed because response was too big.

getProviders : () -> (vec Provider) query;
updateApiKeys : (vec record { ProviderId; opt text }) -> ();
}; No newline at end of file
request : (RpcService, json : text, maxResponseBytes : nat64) -> (RequestResult);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think we should start also documenting the Candid API, like we did here for example.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some docs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

Comment on lines 235 to 248
async fn request(
&self,
service: RpcService,
json_rpc_payload: &str,
max_response_bytes: u64,
) -> CallFlow<'_, RpcResult<String>> {
CallFlow::from_update(
self.runtime.clone(),
self.sol_rpc_canister,
"request",
Encode!(&service, &json_rpc_payload, &max_response_bytes).unwrap(),
)
.await
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should have a test implementation here. We should use the client from sol_rpc_client (and add the corresponding request method)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did some refactoring and now the request method is part of the rpc client, but we need to setup mocks in the test client before making the call.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Cool idea setting up the mock as part of the PocketIcRuntime 💡

@ninegua ninegua requested a review from gregorydemay March 6, 2025 08:48
Copy link
Contributor

@gregorydemay gregorydemay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot @ninegua for this PR! I left some comments but otherwise the code LGTM!

getProviders : () -> (vec Provider) query;
updateApiKeys : (vec record { ProviderId; opt text }) -> ();
}; No newline at end of file
request : (RpcService, json : text, maxResponseBytes : nat64) -> (RequestResult);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

candid_parser = "0.1.4"
canhttp = { git = "https://github.com/dfinity/evm-rpc-canister", rev = "bd02c54bba5d4df424909cbf809f5a1a07739467" }
ciborium = "0.2.2"
evm_rpc_types = "1.3.0"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would start by duplicating the types and removing everything that we currently don't need:

  1. The error types in the EVM RPC canister grew historically and where initially implemented when providers could be added dynamically which is no longer the case and not something that we want to support with SOL RPC. That means that some error variants are useless (e.g. ProviderError::NoPermission, ValidationError::InvalidHex) and I wouldn't want to start a new project with some tech debt of another project.
  2. Generally I think it will probably make sense to have a generic RPC canister crate to put those common types as well as common logic regarding reduction of multiple results but I have the impression that's something that can wait.

Does it make sense?

Comment on lines 235 to 248
async fn request(
&self,
service: RpcService,
json_rpc_payload: &str,
max_response_bytes: u64,
) -> CallFlow<'_, RpcResult<String>> {
CallFlow::from_update(
self.runtime.clone(),
self.sol_rpc_canister,
"request",
Encode!(&service, &json_rpc_payload, &max_response_bytes).unwrap(),
)
.await
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Cool idea setting up the mock as part of the PocketIcRuntime 💡

async fn retrieve_logs(&self, priority: &str) -> Vec<LogEntry<Priority>>;
fn with_caller<T: Into<Principal>>(self, id: T) -> Self;
fn mock_http(self, mock: impl Into<MockOutcall>) -> Self;
fn mock_http_once(self, mock: impl Into<MockOutcall>) -> Self;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this comes a bit from the EVM RPC canister, but do you see a need for mock_http/mock_http_n_times? My impression is that we should be very explicit as to when an HTTPs outcall is made and for this only mock_http_once seems necessary. WDYT?

.submit_call(id, self.caller, method, PocketIcRuntime::encode_args(args))
.await
.expect("failed to submit call");
self.execute_mock().await;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice! 🎉

};
use std::collections::BTreeSet;

pub struct MockOutcallBody(pub Vec<u8>);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it might be also useful to construct a MockOutcallBody from a serde_json::Value so that we can use the json! macro in test. E.g.

let mock = MockOutcallBuilder::new(200, json!({"foo": "bar"}));

0,
)
.await,
Ok(_)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the response doesn't seem to be ever modified, shouldn't we assert that response is MOCK_REQUEST_RESPONSE to strengthen the test?.

It's not yet clear to me what the tests in mock_request_tests bring in addition to the ones in generic_request_tests? Are they just some smoke tests?

Copy link
Contributor

@lpahlavi lpahlavi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for this PR @ninegua, a few small comments but overall it LGTM!

It seems there is a bit of overlap with my PR here, although it's not tragic IMO. Some of the biggest differences are due to my PR being a little bit more recent and hence making use of the new canhttp layers. Let us discuss offline how best to resolve the conflicts.

candid_parser = "0.1.4"
canhttp = { git = "https://github.com/dfinity/evm-rpc-canister", rev = "bd02c54bba5d4df424909cbf809f5a1a07739467" }
ciborium = "0.2.2"
evm_rpc_types = "1.3.0"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also avoid direct dependencies on EVM RPC unless they are meant to be temporary, but then I would for sure add a TODO so we don't forget to remove the dependencies. Otherwise ideally I would just copy the necessary error types as suggested by Grégory.

};
use tower::{BoxError, Service, ServiceBuilder};

pub fn json_rpc_request_arg(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the JSON layer was added to canhttp it would be nice to use that here, but it can also wait for a follow-up PR.


/// Mode of operation
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, CandidType, Deserialize, Serialize)]
pub enum Mode {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! That's much nicer than the boolean value used in the EVM RPC canister.

mod rpc_client;

pub use lifecycle::InstallArgs;
pub use evm_rpc_types::{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As commented above, I would personally avoid having direct dependencies on EVM RPC if possible. If anything I would maybe move some of the types to a new crate that can be shared between the two repositories.

@ninegua
Copy link
Member Author

ninegua commented Mar 18, 2025

As discussed offline with @lpahlavi , I will close this one for now and make a new PR after rebasing onto #33

@ninegua ninegua closed this Mar 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants