feat: Implement a generic request canister interface#18
feat: Implement a generic request canister interface#18
Conversation
b34110b to
9713a60
Compare
| mod rpc_client; | ||
|
|
||
| pub use lifecycle::InstallArgs; | ||
| pub use evm_rpc_types::{ |
There was a problem hiding this comment.
Here it directly re-exports a few definitions from evm_rpc_types, which are generic enough and require no modification.
There was a problem hiding this comment.
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" } |
There was a problem hiding this comment.
It is not the latest, but maybe we can wait a bit until canhttp stabilizes?
There was a problem hiding this comment.
sure, this is totally fine. Just FRY I think there are still 2 things missing from canhttp to be fully functional:
- JSON-RPC layer
- Retry layer, double max response size if request failed because response was too big.
gregorydemay
left a comment
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
I think I would start by duplicating the types and removing everything that we currently don't need:
- 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. - 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?
There was a problem hiding this comment.
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" } |
There was a problem hiding this comment.
sure, this is totally fine. Just FRY I think there are still 2 things missing from canhttp to be fully functional:
- JSON-RPC layer
- Retry layer, double max response size if request failed because response was too big.
canister/sol_rpc_canister.did
Outdated
| getProviders : () -> (vec Provider) query; | ||
| updateApiKeys : (vec record { ProviderId; opt text }) -> (); | ||
| }; No newline at end of file | ||
| request : (RpcService, json : text, maxResponseBytes : nat64) -> (RequestResult); |
There was a problem hiding this comment.
nit: I think we should start also documenting the Candid API, like we did here for example.
integration_tests/src/lib.rs
Outdated
| 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 | ||
| } |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Thanks! Cool idea setting up the mock as part of the PocketIcRuntime 💡
gregorydemay
left a comment
There was a problem hiding this comment.
Thanks a lot @ninegua for this PR! I left some comments but otherwise the code LGTM!
canister/sol_rpc_canister.did
Outdated
| getProviders : () -> (vec Provider) query; | ||
| updateApiKeys : (vec record { ProviderId; opt text }) -> (); | ||
| }; No newline at end of file | ||
| request : (RpcService, json : text, maxResponseBytes : nat64) -> (RequestResult); |
| 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" |
There was a problem hiding this comment.
I think I would start by duplicating the types and removing everything that we currently don't need:
- 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. - 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?
integration_tests/src/lib.rs
Outdated
| 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 | ||
| } |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
| }; | ||
| use std::collections::BTreeSet; | ||
|
|
||
| pub struct MockOutcallBody(pub Vec<u8>); |
There was a problem hiding this comment.
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(_) |
There was a problem hiding this comment.
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?
lpahlavi
left a comment
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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::{ |
There was a problem hiding this comment.
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.
Implement a generic request interface to query Solana API.