From 0a46cd27c2c9a8350332ca6de871606850a2eca8 Mon Sep 17 00:00:00 2001 From: rvanasa Date: Mon, 20 Nov 2023 14:58:33 -0700 Subject: [PATCH 01/27] Refactor to use same HTTP request logic in JSON- and Candid-RPC endpoints --- Cargo.lock | 54 +++++++++++++++++++++++------------------------ src/candid_rpc.rs | 40 ++++++++++++----------------------- src/http.rs | 25 ++++++++++++++++++---- 3 files changed, 61 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 476b8e70..4db88c60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2348,7 +2348,7 @@ dependencies = [ [[package]] name = "ic-base-types" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#3bf517f13737118a21af090052fc4f7254383d47" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" dependencies = [ "base32", "byte-unit", @@ -2391,7 +2391,7 @@ dependencies = [ [[package]] name = "ic-btc-types-internal" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#3bf517f13737118a21af090052fc4f7254383d47" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" dependencies = [ "candid", "ic-btc-interface", @@ -2509,7 +2509,7 @@ dependencies = [ [[package]] name = "ic-canisters-http-types" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#3bf517f13737118a21af090052fc4f7254383d47" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" dependencies = [ "candid", "serde", @@ -2641,7 +2641,7 @@ dependencies = [ [[package]] name = "ic-cketh-minter" version = "0.1.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#3bf517f13737118a21af090052fc4f7254383d47" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" dependencies = [ "askama", "async-trait", @@ -2776,7 +2776,7 @@ dependencies = [ [[package]] name = "ic-crypto-ecdsa-secp256k1" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#3bf517f13737118a21af090052fc4f7254383d47" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" dependencies = [ "k256", "lazy_static", @@ -3095,7 +3095,7 @@ dependencies = [ [[package]] name = "ic-crypto-internal-sha2" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#3bf517f13737118a21af090052fc4f7254383d47" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" dependencies = [ "sha2 0.10.8", ] @@ -3261,7 +3261,7 @@ dependencies = [ [[package]] name = "ic-crypto-sha2" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#3bf517f13737118a21af090052fc4f7254383d47" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" dependencies = [ "ic-crypto-internal-sha2 0.9.0", ] @@ -3269,7 +3269,7 @@ dependencies = [ [[package]] name = "ic-crypto-sha3" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#3bf517f13737118a21af090052fc4f7254383d47" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" dependencies = [ "sha3 0.9.1", ] @@ -3483,7 +3483,7 @@ dependencies = [ [[package]] name = "ic-error-types" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#3bf517f13737118a21af090052fc4f7254383d47" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" dependencies = [ "ic-utils 0.9.0", "serde", @@ -3586,7 +3586,7 @@ dependencies = [ [[package]] name = "ic-ic00-types" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#3bf517f13737118a21af090052fc4f7254383d47" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" dependencies = [ "candid", "ic-base-types 0.9.0", @@ -3876,7 +3876,7 @@ dependencies = [ [[package]] name = "ic-protobuf" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#3bf517f13737118a21af090052fc4f7254383d47" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" dependencies = [ "bincode", "candid", @@ -4198,7 +4198,7 @@ dependencies = [ [[package]] name = "ic-sys" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#3bf517f13737118a21af090052fc4f7254383d47" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" dependencies = [ "hex", "ic-crypto-sha2 0.9.0", @@ -4340,7 +4340,7 @@ dependencies = [ [[package]] name = "ic-utils" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#3bf517f13737118a21af090052fc4f7254383d47" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" dependencies = [ "cvt", "hex", @@ -4357,7 +4357,7 @@ dependencies = [ [[package]] name = "ic-utils-ensure" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#3bf517f13737118a21af090052fc4f7254383d47" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" [[package]] name = "ic-utils-lru-cache" @@ -4435,7 +4435,7 @@ dependencies = [ [[package]] name = "icrc-ledger-client" version = "0.1.2" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#3bf517f13737118a21af090052fc4f7254383d47" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" dependencies = [ "async-trait", "candid", @@ -4446,7 +4446,7 @@ dependencies = [ [[package]] name = "icrc-ledger-client-cdk" version = "0.1.2" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#3bf517f13737118a21af090052fc4f7254383d47" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" dependencies = [ "async-trait", "candid", @@ -4472,7 +4472,7 @@ dependencies = [ [[package]] name = "icrc-ledger-types" version = "0.1.4" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#3bf517f13737118a21af090052fc4f7254383d47" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" dependencies = [ "base32", "candid", @@ -5549,7 +5549,7 @@ dependencies = [ [[package]] name = "phantom_newtype" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#3bf517f13737118a21af090052fc4f7254383d47" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" dependencies = [ "candid", "serde", @@ -6278,9 +6278,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ef35bf3e7fe15a53c4ab08a998e42271eab13eb0db224126bc7bc4c4bad96d" +checksum = "6a3211b01eea83d80687da9eef70e39d65144a3894866a5153a2723e425a157f" dependencies = [ "const-oid", "digest 0.10.7", @@ -7754,9 +7754,9 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.37.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d135e8940b69dbee0f5b0a0be9c1cd6fa8b71d774904c13a3fcfc5dc265e43d" +checksum = "7b09bc5df933a3dabbdb72ae4b6b71be8ae07f58774d5aa41bd20adcd41a235a" dependencies = [ "leb128", ] @@ -7961,21 +7961,21 @@ dependencies = [ [[package]] name = "wast" -version = "68.0.0" +version = "69.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bf3081ac6bcb3a5b72a401693b3566feb529dc2b7e7b62ea544c8a30d0f4d05" +checksum = "efa51b5ad1391943d1bfad537e50f28fe938199ee76b115be6bae83802cd5185" dependencies = [ "leb128", "memchr", "unicode-width", - "wasm-encoder 0.37.0", + "wasm-encoder 0.38.0", ] [[package]] name = "wat" -version = "1.0.80" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fabe07d22a837b3bd5662ba9e980d73de115c040923659a1801934c7ccebe49" +checksum = "74a4c2488d058326466e086a43f5d4ea448241a8d0975e3eb0642c0828be1eb3" dependencies = [ "wast", ] diff --git a/src/candid_rpc.rs b/src/candid_rpc.rs index f76299b9..1de3c210 100644 --- a/src/candid_rpc.rs +++ b/src/candid_rpc.rs @@ -3,8 +3,8 @@ use std::str::FromStr; use async_trait::async_trait; use cketh_common::{ eth_rpc::{ - into_nat, Block, FeeHistory, GetLogsParam, Hash, HttpOutcallError, JsonRpcReply, LogEntry, - ProviderError, RpcError, SendRawTransactionResult, ValidationError, + into_nat, Block, FeeHistory, GetLogsParam, Hash, LogEntry, ProviderError, RpcError, + SendRawTransactionResult, ValidationError, }, eth_rpc_client::{ providers::{RpcApi, RpcNodeProvider}, @@ -13,7 +13,10 @@ use cketh_common::{ }, lifecycle::EthereumNetwork, }; -use serde::de::DeserializeOwned; +use ic_cdk::api::{ + call::CallResult, + management_canister::http_request::{CanisterHttpRequestArgument, HttpResponse}, +}; use crate::*; @@ -27,34 +30,17 @@ impl RpcTransport for CanisterTransport { METADATA.with(|m| m.borrow().get().nodes_in_subnet) } - fn resolve_api(provider: RpcNodeProvider) -> Result { + fn resolve_api(provider: &RpcNodeProvider) -> Result { // TODO: https://github.com/internet-computer-protocol/ic-eth-rpc/issues/73 Ok(provider.api()) } - async fn call_json_rpc( - provider: RpcNodeProvider, - json: &str, - max_response_bytes: u64, - ) -> Result { - let response = do_http_request( - ic_cdk::caller(), - ResolvedSource::Api(Self::resolve_api(provider)?), - json, - max_response_bytes, - ) - .await - .unwrap(); - let status = get_http_response_status(response.status.clone()); - let body = get_http_response_body(response)?; - let json: JsonRpcReply = serde_json::from_str(&body).unwrap_or_else(|e| { - Err(HttpOutcallError::InvalidHttpJsonRpcResponse { - status, - body, - parsing_error: Some(format!("JSON response parse error: {e}")), - }) - })?; - json.result.into() + async fn http_request( + provider: &RpcNodeProvider, + request: CanisterHttpRequestArgument, + cost: u128, + ) -> CallResult { + make_http_request(request, cost).await } } diff --git a/src/http.rs b/src/http.rs index 82d8a9a3..7a3526cc 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,8 +1,10 @@ use cketh_common::eth_rpc::{HttpOutcallError, ProviderError, RpcError, ValidationError}; use ic_canister_log::log; -use ic_cdk::api::management_canister::http_request::{ - http_request as make_http_request, CanisterHttpRequestArgument, HttpHeader, HttpMethod, - HttpResponse, TransformContext, +use ic_cdk::api::{ + call::CallResult, + management_canister::http_request::{ + CanisterHttpRequestArgument, HttpHeader, HttpMethod, HttpResponse, TransformContext, + }, }; use num_traits::ToPrimitive; @@ -77,7 +79,7 @@ pub async fn do_http_request( )), }; match make_http_request(request, cost).await { - Ok((response,)) => Ok(response), + Ok(response) => Ok(response), Err((code, message)) => { inc_metric!(request_err_http); Err(HttpOutcallError::IcError { code, message }.into()) @@ -99,3 +101,18 @@ pub fn get_http_response_body(response: HttpResponse) -> Result CallResult { + #[cfg(test)] + { + // todo!("mock HTTPS outcalls") + } + Ok( + ic_cdk::api::management_canister::http_request::http_request(request, cycles) + .await? + .0, + ) +} From 6761c5fe9fb1d49ccd1a2871bc2adfe342fb4249 Mon Sep 17 00:00:00 2001 From: rvanasa Date: Mon, 20 Nov 2023 16:34:27 -0700 Subject: [PATCH 02/27] Set up 'MockOutcall' --- src/candid_rpc.rs | 2 +- src/http.rs | 119 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 117 insertions(+), 4 deletions(-) diff --git a/src/candid_rpc.rs b/src/candid_rpc.rs index 1de3c210..db390b76 100644 --- a/src/candid_rpc.rs +++ b/src/candid_rpc.rs @@ -40,7 +40,7 @@ impl RpcTransport for CanisterTransport { request: CanisterHttpRequestArgument, cost: u128, ) -> CallResult { - make_http_request(request, cost).await + perform_http_request(request, cost).await } } diff --git a/src/http.rs b/src/http.rs index 7a3526cc..a60b8743 100644 --- a/src/http.rs +++ b/src/http.rs @@ -78,7 +78,7 @@ pub async fn do_http_request( vec![], )), }; - match make_http_request(request, cost).await { + match perform_http_request(request, cost).await { Ok(response) => Ok(response), Err((code, message)) => { inc_metric!(request_err_http); @@ -102,13 +102,24 @@ pub fn get_http_response_body(response: HttpResponse) -> Result CallResult { #[cfg(test)] { - // todo!("mock HTTPS outcalls") + if let Some(response) = mock::MOCK_OUTCALL.with(|mock| { + let mut mock = mock.borrow_mut(); + match mock.take() { + None => None, + Some(m) => { + *mock = None; + Some(m.response) + } + } + }) { + return Ok(response); + } } Ok( ic_cdk::api::management_canister::http_request::http_request(request, cycles) @@ -116,3 +127,105 @@ pub async fn make_http_request( .0, ) } + +#[cfg(test)] +pub mod mock { + use std::cell::RefCell; + + use ic_cdk::api::management_canister::http_request::{HttpHeader, HttpMethod, HttpResponse}; + thread_local! { + pub static MOCK_OUTCALL: RefCell> = RefCell::new(None); + } + + pub struct MockOutcallBody(pub Vec); + + impl From for MockOutcallBody { + fn from(string: String) -> Self { + MockOutcallBody(string.as_bytes().to_vec()) + } + } + + impl From> for MockOutcallBody { + fn from(bytes: Vec) -> Self { + MockOutcallBody(bytes) + } + } + + pub struct MockOutcallBuilder(MockOutcall); + + impl MockOutcallBuilder { + pub fn new(status: u16, body: MockOutcallBody) -> Self { + Self(MockOutcall { + method: None, + url: None, + request_headers: None, + request_body: None, + response: HttpResponse { + status: status.into(), + headers: vec![], + body: body.0, + }, + }) + } + + pub fn method(mut self, method: HttpMethod) -> Self { + self.0.method = Some(method); + self + } + + pub fn url(mut self, url: impl Into) -> Self { + self.0.url = Some(url.into()); + self + } + + pub fn request_headers(mut self, headers: Vec) -> Self { + self.0.request_headers = Some(headers); + self + } + + pub fn request_body(mut self, headers: Vec) -> Self { + self.0.request_headers = Some(headers); + self + } + + pub fn build(self) -> MockOutcall { + self.0 + } + } + + #[derive(Clone, Debug)] + pub struct MockOutcall { + pub method: Option, + pub url: Option, + pub request_headers: Option>, + pub request_body: Option, + pub response: HttpResponse, + } + + impl From for MockOutcall { + fn from(response: HttpResponse) -> Self { + Self { + method: None, + url: None, + request_headers: None, + request_body: None, + response, + } + } + } + + pub fn assert_no_mock_http_request() { + assert!( + MOCK_OUTCALL.with(|mock| mock.borrow().is_none()), + "Previous mock HTTPS outcall was not used" + ) + } + + pub fn mock_http_request(mock: MockOutcall) { + assert_no_mock_http_request(); + MOCK_OUTCALL.with(|current_mock| { + let mut current_mock = current_mock.borrow_mut(); + *current_mock = Some(mock) + }) + } +} From 1ea698a34da654958ec0355486028b2cdd6fb6c3 Mon Sep 17 00:00:00 2001 From: rvanasa Date: Mon, 20 Nov 2023 17:19:33 -0700 Subject: [PATCH 03/27] Set up HTTPS outcall mocking in state machine tests --- Cargo.lock | 1 + Cargo.toml | 4 ++++ src/candid_rpc.rs | 2 +- src/http.rs | 28 +++++++++++++++++++--------- tests/tests.rs | 44 +++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 68 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4db88c60..376fb975 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1670,6 +1670,7 @@ version = "0.1.0" dependencies = [ "async-trait", "candid", + "evm_rpc", "hex", "ic-base-types 0.8.0", "ic-canister-log 0.2.0 (git+https://github.com/dfinity/ic?rev=release-2023-09-27_23-01)", diff --git a/Cargo.toml b/Cargo.toml index 32da394b..260daad5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,9 @@ opt-level = 'z' [profile.canister-release] inherits = "release" +[features] +mock = [] + [dependencies] candid = { workspace = true } ic-canister-log = { workspace = true } @@ -38,6 +41,7 @@ async-trait = "0.1" hex = "0.4" [dev-dependencies] +evm_rpc = { path = ".", features = ["mock"] } # assert_matches = "1.5.0" # ethers-core = "2.0.8" ic-ic00-types = { git = "https://github.com/dfinity/ic", rev = "release-2023-09-27_23-01" } diff --git a/src/candid_rpc.rs b/src/candid_rpc.rs index db390b76..5026933b 100644 --- a/src/candid_rpc.rs +++ b/src/candid_rpc.rs @@ -36,7 +36,7 @@ impl RpcTransport for CanisterTransport { } async fn http_request( - provider: &RpcNodeProvider, + _provider: &RpcNodeProvider, request: CanisterHttpRequestArgument, cost: u128, ) -> CallResult { diff --git a/src/http.rs b/src/http.rs index a60b8743..9539b2ed 100644 --- a/src/http.rs +++ b/src/http.rs @@ -106,9 +106,9 @@ pub async fn perform_http_request( request: CanisterHttpRequestArgument, cycles: u128, ) -> CallResult { - #[cfg(test)] + #[cfg(feature = "mock")] { - if let Some(response) = mock::MOCK_OUTCALL.with(|mock| { + if let Some(response) = mock_http::MOCK_OUTCALL.with(|mock| { let mut mock = mock.borrow_mut(); match mock.take() { None => None, @@ -128,8 +128,8 @@ pub async fn perform_http_request( ) } -#[cfg(test)] -pub mod mock { +#[cfg(feature = "mock")] +pub mod mock_http { use std::cell::RefCell; use ic_cdk::api::management_canister::http_request::{HttpHeader, HttpMethod, HttpResponse}; @@ -141,10 +141,14 @@ pub mod mock { impl From for MockOutcallBody { fn from(string: String) -> Self { - MockOutcallBody(string.as_bytes().to_vec()) + string.as_bytes().to_vec().into() + } + } + impl<'a> From<&'a str> for MockOutcallBody { + fn from(string: &'a str) -> Self { + string.to_string().into() } } - impl From> for MockOutcallBody { fn from(bytes: Vec) -> Self { MockOutcallBody(bytes) @@ -154,7 +158,7 @@ pub mod mock { pub struct MockOutcallBuilder(MockOutcall); impl MockOutcallBuilder { - pub fn new(status: u16, body: MockOutcallBody) -> Self { + pub fn new(status: u16, body: impl Into) -> Self { Self(MockOutcall { method: None, url: None, @@ -163,7 +167,7 @@ pub mod mock { response: HttpResponse { status: status.into(), headers: vec![], - body: body.0, + body: body.into().0, }, }) } @@ -202,6 +206,12 @@ pub mod mock { pub response: HttpResponse, } + impl MockOutcall { + pub fn mock_once(self) { + mock_http_request(self) + } + } + impl From for MockOutcall { fn from(response: HttpResponse) -> Self { Self { @@ -221,7 +231,7 @@ pub mod mock { ) } - pub fn mock_http_request(mock: MockOutcall) { + fn mock_http_request(mock: MockOutcall) { assert_no_mock_http_request(); MOCK_OUTCALL.with(|current_mock| { let mut current_mock = current_mock.borrow_mut(); diff --git a/tests/tests.rs b/tests/tests.rs index 1a38bd2d..8566ce54 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1,13 +1,14 @@ use std::rc::Rc; use candid::{CandidType, Decode, Encode, Nat}; -use evm_rpc::*; use ic_base_types::{CanisterId, PrincipalId}; use ic_ic00_types::BoundedVec; use ic_state_machine_tests::{CanisterSettingsArgs, StateMachine, StateMachineBuilder, WasmResult}; use ic_test_utilities_load_wasm::load_wasm; use serde::de::DeserializeOwned; +use evm_rpc::*; + const DEFAULT_CALLER_TEST_ID: u64 = 10352385; const DEFAULT_CONTROLLER_TEST_ID: u64 = 10352386; @@ -113,6 +114,14 @@ impl EvmRpcSetup { .unwrap() } + pub fn mock_http( + &self, + status: u16, + body: impl Into, + ) -> mock_http::MockOutcallBuilder { + mock_http::MockOutcallBuilder::new(status, body) + } + pub fn authorize(&self, principal: &PrincipalId, auth: Auth) { self.call_update("authorize", Encode!(&principal.0, &auth).unwrap()) } @@ -150,6 +159,24 @@ impl EvmRpcSetup { Encode!(&source, &json_rpc_payload, &max_response_bytes).unwrap(), ) } + + pub fn request( + &self, + source: Source, + json_rpc_payload: &str, + max_response_bytes: u64, + ) -> RpcResult { + self.call_update( + "request", + Encode!(&source, &json_rpc_payload, &max_response_bytes).unwrap(), + ) + } +} + +impl Drop for EvmRpcSetup { + fn drop(&mut self) { + mock_http::assert_no_mock_http_request(); + } } #[test] @@ -211,3 +238,18 @@ fn test_provider_registry() { ] ) } + +#[test] +fn test_free_rpc() { + let setup = EvmRpcSetup::new().authorize_caller(Auth::FreeRpc); + let expected_result = "{\"id\":1,\"jsonrpc\":\"2.0\",\"result\":\"0x112233\"}"; + setup.mock_http(200, expected_result).build().mock_once(); + let result = setup + .request( + Source::Provider(0), + "{\"jsonrpc\":\"2.0\",\"method\":\"eth_gasPrice\",\"params\":null,\"id\":1}", + 1000, + ) + .expect("request()"); + assert_eq!(result, expected_result); +} From db2916170ddf49fa24d421fcd59e96773ddcdf6a Mon Sep 17 00:00:00 2001 From: rvanasa Date: Mon, 20 Nov 2023 17:21:40 -0700 Subject: [PATCH 04/27] Add 'mock' feature to generated Wasm file --- tests/tests.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/tests.rs b/tests/tests.rs index 8566ce54..54a70597 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -13,7 +13,11 @@ const DEFAULT_CALLER_TEST_ID: u64 = 10352385; const DEFAULT_CONTROLLER_TEST_ID: u64 = 10352386; fn evm_rpc_wasm() -> Vec { - load_wasm(std::env::var("CARGO_MANIFEST_DIR").unwrap(), "evm_rpc", &[]) + load_wasm( + std::env::var("CARGO_MANIFEST_DIR").unwrap(), + "evm_rpc", + &["mock"], + ) } fn assert_reply(result: WasmResult) -> Vec { From b59d80a7c39a3644e7f5a3793d9a83c3807201c3 Mon Sep 17 00:00:00 2001 From: rvanasa Date: Mon, 20 Nov 2023 17:39:15 -0700 Subject: [PATCH 05/27] Misc --- tests/tests.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/tests.rs b/tests/tests.rs index 54a70597..8e26f0a9 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -142,14 +142,12 @@ impl EvmRpcSetup { self.call_update("register_provider", Encode!(&args).unwrap()) } - pub fn authorize_caller(self, auth: Auth) -> Self { + pub fn authorize_caller(&self, auth: Auth) { self.as_controller().authorize(&self.caller, auth); - self } - pub fn deauthorize_caller(self, auth: Auth) -> Self { + pub fn deauthorize_caller(&self, auth: Auth) { self.as_controller().deauthorize(&self.caller, auth); - self } pub fn request_cost( @@ -184,8 +182,10 @@ impl Drop for EvmRpcSetup { } #[test] -fn test_provider_registry() { - let setup = EvmRpcSetup::new().authorize_caller(Auth::RegisterProvider); +fn should_register_providers() { + let mut setup = EvmRpcSetup::new(); + setup.authorize_caller(Auth::RegisterProvider); + assert_eq!( setup .get_providers() @@ -244,14 +244,16 @@ fn test_provider_registry() { } #[test] -fn test_free_rpc() { - let setup = EvmRpcSetup::new().authorize_caller(Auth::FreeRpc); - let expected_result = "{\"id\":1,\"jsonrpc\":\"2.0\",\"result\":\"0x112233\"}"; +fn should_allow_free_rpc() { + let mut setup = EvmRpcSetup::new(); + setup.authorize_caller(Auth::FreeRpc); + + let expected_result = "{\"id\":1,\"jsonrpc\":\"2.0\",\"result\":\"0x00112233\"}"; setup.mock_http(200, expected_result).build().mock_once(); let result = setup .request( Source::Provider(0), - "{\"jsonrpc\":\"2.0\",\"method\":\"eth_gasPrice\",\"params\":null,\"id\":1}", + "{\"id\":1,\"jsonrpc\":\"2.0\",\"method\":\"eth_gasPrice\",\"params\":null}", 1000, ) .expect("request()"); From ec26702774510ae537d23658d3a623385521c0e2 Mon Sep 17 00:00:00 2001 From: rvanasa Date: Mon, 20 Nov 2023 17:40:54 -0700 Subject: [PATCH 06/27] Improve readability of JSON literals --- tests/tests.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/tests.rs b/tests/tests.rs index 8e26f0a9..158ac507 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -8,6 +8,7 @@ use ic_test_utilities_load_wasm::load_wasm; use serde::de::DeserializeOwned; use evm_rpc::*; +use serde_json::json; const DEFAULT_CALLER_TEST_ID: u64 = 10352385; const DEFAULT_CONTROLLER_TEST_ID: u64 = 10352386; @@ -248,12 +249,12 @@ fn should_allow_free_rpc() { let mut setup = EvmRpcSetup::new(); setup.authorize_caller(Auth::FreeRpc); - let expected_result = "{\"id\":1,\"jsonrpc\":\"2.0\",\"result\":\"0x00112233\"}"; + let expected_result = r#"{"id":1,"jsonrpc":"2.0","result":"0x00112233"}"#; setup.mock_http(200, expected_result).build().mock_once(); let result = setup .request( Source::Provider(0), - "{\"id\":1,\"jsonrpc\":\"2.0\",\"method\":\"eth_gasPrice\",\"params\":null}", + r#"{"id":1,"jsonrpc":"2.0","method":"eth_gasPrice","params":null}"#, 1000, ) .expect("request()"); From 62fa0235b913ce38428aca8d8cae67df37331054 Mon Sep 17 00:00:00 2001 From: rvanasa Date: Mon, 20 Nov 2023 17:51:03 -0700 Subject: [PATCH 07/27] Use mocking for both test env and 'mock' flag --- src/http.rs | 4 ++-- tests/tests.rs | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/http.rs b/src/http.rs index 9539b2ed..72f6382e 100644 --- a/src/http.rs +++ b/src/http.rs @@ -106,7 +106,7 @@ pub async fn perform_http_request( request: CanisterHttpRequestArgument, cycles: u128, ) -> CallResult { - #[cfg(feature = "mock")] + #[cfg(any(feature = "mock", test))] { if let Some(response) = mock_http::MOCK_OUTCALL.with(|mock| { let mut mock = mock.borrow_mut(); @@ -128,7 +128,7 @@ pub async fn perform_http_request( ) } -#[cfg(feature = "mock")] +#[cfg(any(feature = "mock", test))] pub mod mock_http { use std::cell::RefCell; diff --git a/tests/tests.rs b/tests/tests.rs index 158ac507..02db95ba 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -7,7 +7,7 @@ use ic_state_machine_tests::{CanisterSettingsArgs, StateMachine, StateMachineBui use ic_test_utilities_load_wasm::load_wasm; use serde::de::DeserializeOwned; -use evm_rpc::*; +use evm_rpc::{*, mock_http::MockOutcall}; use serde_json::json; const DEFAULT_CALLER_TEST_ID: u64 = 10352385; @@ -121,10 +121,9 @@ impl EvmRpcSetup { pub fn mock_http( &self, - status: u16, - body: impl Into, - ) -> mock_http::MockOutcallBuilder { - mock_http::MockOutcallBuilder::new(status, body) + mock:MockOutcall, + ) { + self.env.canister_http_request_contexts() } pub fn authorize(&self, principal: &PrincipalId, auth: Auth) { @@ -250,7 +249,7 @@ fn should_allow_free_rpc() { setup.authorize_caller(Auth::FreeRpc); let expected_result = r#"{"id":1,"jsonrpc":"2.0","result":"0x00112233"}"#; - setup.mock_http(200, expected_result).build().mock_once(); + setup.mock_http(mock_http::MockOutcallBuilder::new(200, expected_result).build()); let result = setup .request( Source::Provider(0), From f4471051d3716497a5ac71169aa086c32bfd054f Mon Sep 17 00:00:00 2001 From: rvanasa Date: Mon, 20 Nov 2023 18:48:02 -0700 Subject: [PATCH 08/27] Refactor --- src/http.rs | 3 + tests/tests.rs | 163 +++++++++++++++++++++++++++++++++---------------- 2 files changed, 113 insertions(+), 53 deletions(-) diff --git a/src/http.rs b/src/http.rs index 72f6382e..b6d2adc6 100644 --- a/src/http.rs +++ b/src/http.rs @@ -128,6 +128,9 @@ pub async fn perform_http_request( ) } +#[cfg(any(feature = "mock", test))] +pub use mock_http::*; + #[cfg(any(feature = "mock", test))] pub mod mock_http { use std::cell::RefCell; diff --git a/tests/tests.rs b/tests/tests.rs index 02db95ba..83a1e6d9 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1,18 +1,21 @@ -use std::rc::Rc; +use std::{marker::PhantomData, rc::Rc, time::Duration}; use candid::{CandidType, Decode, Encode, Nat}; use ic_base_types::{CanisterId, PrincipalId}; use ic_ic00_types::BoundedVec; -use ic_state_machine_tests::{CanisterSettingsArgs, StateMachine, StateMachineBuilder, WasmResult}; +use ic_state_machine_tests::{ + CanisterSettingsArgs, MessageId, StateMachine, StateMachineBuilder, WasmResult, +}; use ic_test_utilities_load_wasm::load_wasm; use serde::de::DeserializeOwned; -use evm_rpc::{*, mock_http::MockOutcall}; -use serde_json::json; +use evm_rpc::*; const DEFAULT_CALLER_TEST_ID: u64 = 10352385; const DEFAULT_CONTROLLER_TEST_ID: u64 = 10352386; +const MAX_TICKS: usize = 10; + fn evm_rpc_wasm() -> Vec { load_wasm( std::env::var("CARGO_MANIFEST_DIR").unwrap(), @@ -35,7 +38,7 @@ pub struct EvmRpcSetup { pub env: Rc, pub caller: PrincipalId, pub controller: PrincipalId, - pub evm_rpc_id: CanisterId, + pub canister_id: CanisterId, } impl Default for EvmRpcSetup { @@ -67,7 +70,7 @@ impl EvmRpcSetup { env, caller, controller, - evm_rpc_id, + canister_id: evm_rpc_id, } } @@ -89,26 +92,19 @@ impl EvmRpcSetup { setup } - fn call_update(&self, method: &str, input: Vec) -> R { - Decode!( - &assert_reply( - self.env - .execute_ingress_as(self.caller, self.evm_rpc_id, method, input,) - .unwrap_or_else(|err| panic!( - "error during update call to `{}()`: {}", - method, err - )) - ), - R - ) - .unwrap() + fn call_update( + &self, + method: &str, + input: Vec, + ) -> MessageFlow { + MessageFlow::from_update(self, method, input) } fn call_query(&self, method: &str, input: Vec) -> R { Decode!( &assert_reply( self.env - .query_as(self.caller, self.evm_rpc_id, method, input,) + .query_as(self.caller, self.canister_id, method, input,) .unwrap_or_else(|err| panic!( "error during query call to `{}()`: {}", method, err @@ -119,35 +115,38 @@ impl EvmRpcSetup { .unwrap() } - pub fn mock_http( - &self, - mock:MockOutcall, - ) { - self.env.canister_http_request_contexts() + pub fn tick_until_http_request(&self) { + for _ in 0..MAX_TICKS { + if !self.env.canister_http_request_contexts().is_empty() { + break; + } + self.env.tick(); + self.env.advance_time(Duration::from_nanos(1)); + } } - pub fn authorize(&self, principal: &PrincipalId, auth: Auth) { + pub fn authorize(&self, principal: &PrincipalId, auth: Auth) -> MessageFlow<()> { self.call_update("authorize", Encode!(&principal.0, &auth).unwrap()) } - pub fn deauthorize(&self, principal: &PrincipalId, auth: Auth) { + pub fn deauthorize(&self, principal: &PrincipalId, auth: Auth) -> MessageFlow<()> { self.call_update("deauthorize", Encode!(&principal.0, &auth).unwrap()) } pub fn get_providers(&self) -> Vec { - self.call_update("get_providers", Encode!().unwrap()) + self.call_query("get_providers", Encode!().unwrap()) } - pub fn register_provider(&self, args: RegisterProviderArgs) -> u64 { + pub fn register_provider(&self, args: RegisterProviderArgs) -> MessageFlow { self.call_update("register_provider", Encode!(&args).unwrap()) } - pub fn authorize_caller(&self, auth: Auth) { - self.as_controller().authorize(&self.caller, auth); + pub fn authorize_caller(&self, auth: Auth) -> MessageFlow<()> { + self.as_controller().authorize(&self.caller, auth) } - pub fn deauthorize_caller(&self, auth: Auth) { - self.as_controller().deauthorize(&self.caller, auth); + pub fn deauthorize_caller(&self, auth: Auth) -> MessageFlow<()> { + self.as_controller().deauthorize(&self.caller, auth) } pub fn request_cost( @@ -167,7 +166,7 @@ impl EvmRpcSetup { source: Source, json_rpc_payload: &str, max_response_bytes: u64, - ) -> RpcResult { + ) -> MessageFlow> { self.call_update( "request", Encode!(&source, &json_rpc_payload, &max_response_bytes).unwrap(), @@ -181,9 +180,62 @@ impl Drop for EvmRpcSetup { } } +struct MessageFlow<'a, 'b, R> { + setup: &'a EvmRpcSetup, + method: &'b str, + message_id: MessageId, + phantom: PhantomData, +} + +impl<'setup, 'method, R: CandidType + DeserializeOwned> MessageFlow<'setup, 'method, R> { + pub fn from_update(setup: &'setup EvmRpcSetup, method: &str, input: Vec) -> Self { + let message_id = setup + .env + .send_ingress(setup.caller, setup.canister_id, method, input); + MessageFlow::new(setup, method, message_id) + } + + pub fn new(setup: &'setup EvmRpcSetup, method: &'method str, message_id: MessageId) -> Self { + Self { + setup, + method, + message_id, + phantom: Default::default(), + } + } + + pub fn mock_http(self, mock: MockOutcall) -> Self { + assert_eq!(self.setup.env.canister_http_request_contexts().len(), 0); + self.setup.tick_until_http_request(); + let request = self + .setup + .env + .canister_http_request_contexts() + .first_entry() + .unwrap(); + todo!("mock_http"); + // self + } + + pub fn wait(self) -> R { + Decode!( + &assert_reply( + self.setup + .env + .await_ingress(self.message_id, MAX_TICKS) + .unwrap_or_else(|err| { + panic!("error during update call to `{}()`: {}", self.method, err) + }) + ), + R + ) + .unwrap() + } +} + #[test] fn should_register_providers() { - let mut setup = EvmRpcSetup::new(); + let setup = EvmRpcSetup::new(); setup.authorize_caller(Auth::RegisterProvider); assert_eq!( @@ -198,22 +250,26 @@ fn should_register_providers() { .collect::>() ); let n_providers = 2; - let a_id = setup.register_provider(RegisterProviderArgs { - chain_id: 1, - hostname: "cloudflare-eth.com".to_string(), - credential_path: "".to_string(), - credential_headers: None, - cycles_per_call: 0, - cycles_per_message_byte: 0, - }); - let b_id = setup.register_provider(RegisterProviderArgs { - chain_id: 5, - hostname: "ethereum.publicnode.com".to_string(), - credential_path: "".to_string(), - credential_headers: None, - cycles_per_call: 0, - cycles_per_message_byte: 0, - }); + let a_id = setup + .register_provider(RegisterProviderArgs { + chain_id: 1, + hostname: "cloudflare-eth.com".to_string(), + credential_path: "".to_string(), + credential_headers: None, + cycles_per_call: 0, + cycles_per_message_byte: 0, + }) + .wait(); + let b_id = setup + .register_provider(RegisterProviderArgs { + chain_id: 5, + hostname: "ethereum.publicnode.com".to_string(), + credential_path: "".to_string(), + credential_headers: None, + cycles_per_call: 0, + cycles_per_message_byte: 0, + }) + .wait(); assert_eq!(a_id + 1, b_id); let providers = setup.get_providers(); assert_eq!(providers.len(), get_default_providers().len() + n_providers); @@ -245,17 +301,18 @@ fn should_register_providers() { #[test] fn should_allow_free_rpc() { - let mut setup = EvmRpcSetup::new(); + let setup = EvmRpcSetup::new(); setup.authorize_caller(Auth::FreeRpc); let expected_result = r#"{"id":1,"jsonrpc":"2.0","result":"0x00112233"}"#; - setup.mock_http(mock_http::MockOutcallBuilder::new(200, expected_result).build()); let result = setup .request( Source::Provider(0), r#"{"id":1,"jsonrpc":"2.0","method":"eth_gasPrice","params":null}"#, 1000, ) + .mock_http(MockOutcallBuilder::new(200, expected_result).build()) + .wait() .expect("request()"); assert_eq!(result, expected_result); } From 3d96260d3c43418f3543bd5255013427b92ce610 Mon Sep 17 00:00:00 2001 From: rvanasa Date: Mon, 20 Nov 2023 18:49:17 -0700 Subject: [PATCH 09/27] Rename 'MessageFlow' -> 'CallFlow' --- tests/tests.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/tests.rs b/tests/tests.rs index 83a1e6d9..ee19de50 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -96,8 +96,8 @@ impl EvmRpcSetup { &self, method: &str, input: Vec, - ) -> MessageFlow { - MessageFlow::from_update(self, method, input) + ) -> CallFlow { + CallFlow::from_update(self, method, input) } fn call_query(&self, method: &str, input: Vec) -> R { @@ -125,11 +125,11 @@ impl EvmRpcSetup { } } - pub fn authorize(&self, principal: &PrincipalId, auth: Auth) -> MessageFlow<()> { + pub fn authorize(&self, principal: &PrincipalId, auth: Auth) -> CallFlow<()> { self.call_update("authorize", Encode!(&principal.0, &auth).unwrap()) } - pub fn deauthorize(&self, principal: &PrincipalId, auth: Auth) -> MessageFlow<()> { + pub fn deauthorize(&self, principal: &PrincipalId, auth: Auth) -> CallFlow<()> { self.call_update("deauthorize", Encode!(&principal.0, &auth).unwrap()) } @@ -137,15 +137,15 @@ impl EvmRpcSetup { self.call_query("get_providers", Encode!().unwrap()) } - pub fn register_provider(&self, args: RegisterProviderArgs) -> MessageFlow { + pub fn register_provider(&self, args: RegisterProviderArgs) -> CallFlow { self.call_update("register_provider", Encode!(&args).unwrap()) } - pub fn authorize_caller(&self, auth: Auth) -> MessageFlow<()> { + pub fn authorize_caller(&self, auth: Auth) -> CallFlow<()> { self.as_controller().authorize(&self.caller, auth) } - pub fn deauthorize_caller(&self, auth: Auth) -> MessageFlow<()> { + pub fn deauthorize_caller(&self, auth: Auth) -> CallFlow<()> { self.as_controller().deauthorize(&self.caller, auth) } @@ -166,7 +166,7 @@ impl EvmRpcSetup { source: Source, json_rpc_payload: &str, max_response_bytes: u64, - ) -> MessageFlow> { + ) -> CallFlow> { self.call_update( "request", Encode!(&source, &json_rpc_payload, &max_response_bytes).unwrap(), @@ -180,19 +180,19 @@ impl Drop for EvmRpcSetup { } } -struct MessageFlow<'a, 'b, R> { +struct CallFlow<'a, 'b, R> { setup: &'a EvmRpcSetup, method: &'b str, message_id: MessageId, phantom: PhantomData, } -impl<'setup, 'method, R: CandidType + DeserializeOwned> MessageFlow<'setup, 'method, R> { +impl<'setup, 'method, R: CandidType + DeserializeOwned> CallFlow<'setup, 'method, R> { pub fn from_update(setup: &'setup EvmRpcSetup, method: &str, input: Vec) -> Self { let message_id = setup .env .send_ingress(setup.caller, setup.canister_id, method, input); - MessageFlow::new(setup, method, message_id) + CallFlow::new(setup, method, message_id) } pub fn new(setup: &'setup EvmRpcSetup, method: &'method str, message_id: MessageId) -> Self { From c17764e34871baa6eeff6cc797e0ec8e3921adb2 Mon Sep 17 00:00:00 2001 From: rvanasa Date: Tue, 21 Nov 2023 09:45:11 -0700 Subject: [PATCH 10/27] Appease type system --- tests/tests.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/tests.rs b/tests/tests.rs index ee19de50..d2b57001 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -74,18 +74,21 @@ impl EvmRpcSetup { } } + /// Shorthand for deriving an `EvmRpcSetup` with the caller as the canister controller. pub fn as_controller(&self) -> Self { let mut setup = self.clone(); setup.caller = self.controller; setup } + /// Shorthand for deriving an `EvmRpcSetup` with an anonymous caller. pub fn as_anonymous(&self) -> Self { let mut setup = self.clone(); setup.caller = PrincipalId::new_anonymous(); setup } + /// Shorthand for deriving an `EvmRpcSetup` with an arbitrary caller. pub fn as_caller(&self, id: PrincipalId) -> Self { let mut setup = self.clone(); setup.caller = id; @@ -97,7 +100,7 @@ impl EvmRpcSetup { method: &str, input: Vec, ) -> CallFlow { - CallFlow::from_update(self, method, input) + CallFlow::from_update(self.clone(), method, input) } fn call_query(&self, method: &str, input: Vec) -> R { @@ -180,25 +183,25 @@ impl Drop for EvmRpcSetup { } } -struct CallFlow<'a, 'b, R> { - setup: &'a EvmRpcSetup, - method: &'b str, +pub struct CallFlow { + setup: EvmRpcSetup, + method: String, message_id: MessageId, phantom: PhantomData, } -impl<'setup, 'method, R: CandidType + DeserializeOwned> CallFlow<'setup, 'method, R> { - pub fn from_update(setup: &'setup EvmRpcSetup, method: &str, input: Vec) -> Self { +impl CallFlow { + pub fn from_update(setup: EvmRpcSetup, method: &str, input: Vec) -> Self { let message_id = setup .env .send_ingress(setup.caller, setup.canister_id, method, input); CallFlow::new(setup, method, message_id) } - pub fn new(setup: &'setup EvmRpcSetup, method: &'method str, message_id: MessageId) -> Self { + pub fn new(setup: EvmRpcSetup, method: impl ToString, message_id: MessageId) -> Self { Self { setup, - method, + method: method.to_string(), message_id, phantom: Default::default(), } From cbfef1e087a87c3dee29a140008dc77c577ca144 Mon Sep 17 00:00:00 2001 From: rvanasa Date: Tue, 21 Nov 2023 09:48:20 -0700 Subject: [PATCH 11/27] Add 'no pending HTTP request' expect message --- tests/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests.rs b/tests/tests.rs index d2b57001..f733a6e6 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -215,7 +215,7 @@ impl CallFlow { .env .canister_http_request_contexts() .first_entry() - .unwrap(); + .expect("no pending HTTP request"); todo!("mock_http"); // self } From fbad30d52f89a696dc9d64eb3243a85b3d680685 Mon Sep 17 00:00:00 2001 From: rvanasa Date: Tue, 21 Nov 2023 10:09:13 -0700 Subject: [PATCH 12/27] Add initial cycles to canister in state machine tests --- tests/tests.rs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/tests/tests.rs b/tests/tests.rs index f733a6e6..f7509260 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -2,9 +2,10 @@ use std::{marker::PhantomData, rc::Rc, time::Duration}; use candid::{CandidType, Decode, Encode, Nat}; use ic_base_types::{CanisterId, PrincipalId}; -use ic_ic00_types::BoundedVec; +use ic_ic00_types::{BoundedVec, CanisterSettingsArgsBuilder}; use ic_state_machine_tests::{ - CanisterSettingsArgs, MessageId, StateMachine, StateMachineBuilder, WasmResult, + CanisterSettingsArgs, Cycles, IngressState, IngressStatus, MessageId, StateMachine, + StateMachineBuilder, WasmResult, }; use ic_test_utilities_load_wasm::load_wasm; use serde::de::DeserializeOwned; @@ -14,6 +15,8 @@ use evm_rpc::*; const DEFAULT_CALLER_TEST_ID: u64 = 10352385; const DEFAULT_CONTROLLER_TEST_ID: u64 = 10352386; +const INITIAL_CYCLES: u128 = 100_000_000_000_000_000; + const MAX_TICKS: usize = 10; fn evm_rpc_wasm() -> Vec { @@ -56,11 +59,15 @@ impl EvmRpcSetup { ); let controller = PrincipalId::new_user_test_id(DEFAULT_CONTROLLER_TEST_ID); - let evm_rpc_id = env.create_canister(Some({ - let mut args: CanisterSettingsArgs = Default::default(); - args.controllers = Some(BoundedVec::new(vec![controller])); - args - })); + let evm_rpc_id = env.create_canister_with_cycles( + None, + Cycles::new(INITIAL_CYCLES), + Some( + CanisterSettingsArgsBuilder::default() + .with_controller(controller) + .build(), + ), + ); env.install_existing_canister(evm_rpc_id, evm_rpc_wasm(), Encode!(&()).unwrap()) .unwrap(); @@ -210,6 +217,10 @@ impl CallFlow { pub fn mock_http(self, mock: MockOutcall) -> Self { assert_eq!(self.setup.env.canister_http_request_contexts().len(), 0); self.setup.tick_until_http_request(); + match self.setup.env.ingress_status(&self.message_id) { + IngressStatus::Known { state, .. } if state != IngressState::Processing => return self, + _ => (), + } let request = self .setup .env From d27496c4acb52ea79d397fecb0c0c0470ceb1b05 Mon Sep 17 00:00:00 2001 From: rvanasa Date: Tue, 21 Nov 2023 10:41:54 -0700 Subject: [PATCH 13/27] Implement HTTP request mocking --- Cargo.lock | 44 +++++++++++++++++------------------ tests/tests.rs | 62 +++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 73 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 376fb975..77a40abb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2349,7 +2349,7 @@ dependencies = [ [[package]] name = "ic-base-types" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" dependencies = [ "base32", "byte-unit", @@ -2392,7 +2392,7 @@ dependencies = [ [[package]] name = "ic-btc-types-internal" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" dependencies = [ "candid", "ic-btc-interface", @@ -2510,7 +2510,7 @@ dependencies = [ [[package]] name = "ic-canisters-http-types" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" dependencies = [ "candid", "serde", @@ -2642,7 +2642,7 @@ dependencies = [ [[package]] name = "ic-cketh-minter" version = "0.1.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" dependencies = [ "askama", "async-trait", @@ -2777,7 +2777,7 @@ dependencies = [ [[package]] name = "ic-crypto-ecdsa-secp256k1" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" dependencies = [ "k256", "lazy_static", @@ -3096,7 +3096,7 @@ dependencies = [ [[package]] name = "ic-crypto-internal-sha2" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" dependencies = [ "sha2 0.10.8", ] @@ -3262,7 +3262,7 @@ dependencies = [ [[package]] name = "ic-crypto-sha2" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" dependencies = [ "ic-crypto-internal-sha2 0.9.0", ] @@ -3270,7 +3270,7 @@ dependencies = [ [[package]] name = "ic-crypto-sha3" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" dependencies = [ "sha3 0.9.1", ] @@ -3484,7 +3484,7 @@ dependencies = [ [[package]] name = "ic-error-types" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" dependencies = [ "ic-utils 0.9.0", "serde", @@ -3587,7 +3587,7 @@ dependencies = [ [[package]] name = "ic-ic00-types" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" dependencies = [ "candid", "ic-base-types 0.9.0", @@ -3877,7 +3877,7 @@ dependencies = [ [[package]] name = "ic-protobuf" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" dependencies = [ "bincode", "candid", @@ -4199,7 +4199,7 @@ dependencies = [ [[package]] name = "ic-sys" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" dependencies = [ "hex", "ic-crypto-sha2 0.9.0", @@ -4341,7 +4341,7 @@ dependencies = [ [[package]] name = "ic-utils" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" dependencies = [ "cvt", "hex", @@ -4358,7 +4358,7 @@ dependencies = [ [[package]] name = "ic-utils-ensure" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" [[package]] name = "ic-utils-lru-cache" @@ -4436,7 +4436,7 @@ dependencies = [ [[package]] name = "icrc-ledger-client" version = "0.1.2" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" dependencies = [ "async-trait", "candid", @@ -4447,7 +4447,7 @@ dependencies = [ [[package]] name = "icrc-ledger-client-cdk" version = "0.1.2" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" dependencies = [ "async-trait", "candid", @@ -4473,7 +4473,7 @@ dependencies = [ [[package]] name = "icrc-ledger-types" version = "0.1.4" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" dependencies = [ "base32", "candid", @@ -5550,7 +5550,7 @@ dependencies = [ [[package]] name = "phantom_newtype" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#886f17ea70dfbaf4ef225bd68a08623a47b395c8" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" dependencies = [ "candid", "serde", @@ -6500,9 +6500,9 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" -version = "1.0.192" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] @@ -6528,9 +6528,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.192" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", diff --git a/tests/tests.rs b/tests/tests.rs index f7509260..5fb58a23 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -2,11 +2,15 @@ use std::{marker::PhantomData, rc::Rc, time::Duration}; use candid::{CandidType, Decode, Encode, Nat}; use ic_base_types::{CanisterId, PrincipalId}; -use ic_ic00_types::{BoundedVec, CanisterSettingsArgsBuilder}; +use ic_cdk::api::management_canister::http_request::{ + HttpResponse as OutCallHttpResponse, TransformArgs, +}; +use ic_ic00_types::CanisterSettingsArgsBuilder; use ic_state_machine_tests::{ - CanisterSettingsArgs, Cycles, IngressState, IngressStatus, MessageId, StateMachine, - StateMachineBuilder, WasmResult, + CanisterHttpResponsePayload, Cycles, IngressState, IngressStatus, MessageId, PayloadBuilder, + StateMachine, StateMachineBuilder, WasmResult, }; + use ic_test_utilities_load_wasm::load_wasm; use serde::de::DeserializeOwned; @@ -221,14 +225,50 @@ impl CallFlow { IngressStatus::Known { state, .. } if state != IngressState::Processing => return self, _ => (), } - let request = self - .setup - .env - .canister_http_request_contexts() - .first_entry() - .expect("no pending HTTP request"); - todo!("mock_http"); - // self + let contexts = self.setup.env.canister_http_request_contexts(); + let (id, context) = contexts.first_key_value().expect("no pending HTTP request"); + + let mut response = OutCallHttpResponse { + status: mock.response.status.into(), + headers: mock.response.headers, + body: mock.response.body, + }; + if let Some(transform) = &context.transform { + let transform_args = TransformArgs { + response, + context: transform.context.to_vec(), + }; + response = Decode!( + &assert_reply( + self.setup + .env + .execute_ingress( + self.setup.canister_id, + transform.method_name.clone(), + Encode!(&transform_args).unwrap(), + ) + .expect("failed to query transform HTTP response") + ), + OutCallHttpResponse + ) + .unwrap(); + } + let http_response = CanisterHttpResponsePayload { + status: response.status.0.try_into().unwrap(), + headers: response + .headers + .into_iter() + .map(|h| ic_ic00_types::HttpHeader { + name: h.name, + value: h.value, + }) + .collect(), + body: response.body, + }; + let payload = PayloadBuilder::new().http_response(id.clone(), &http_response); + self.setup.env.execute_payload(payload); + + self } pub fn wait(self) -> R { From dc7d1823db15de01cf3652425c29efc080239d19 Mon Sep 17 00:00:00 2001 From: rvanasa Date: Tue, 21 Nov 2023 13:36:59 -0700 Subject: [PATCH 14/27] Refactor --- Cargo.lock | 2 + Cargo.toml | 10 +--- lib/rust/src/lib.rs | 4 +- lib/rust/src/rpc.rs | 2 +- src/http.rs | 127 +++++++++++++++++++++++++++++++++++--------- src/main.rs | 12 ++--- tests/tests.rs | 29 ++++++---- 7 files changed, 130 insertions(+), 56 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 77a40abb..098bb32e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1668,9 +1668,11 @@ dependencies = [ name = "evm_rpc" version = "0.1.0" dependencies = [ + "assert_matches", "async-trait", "candid", "evm_rpc", + "futures", "hex", "ic-base-types 0.8.0", "ic-canister-log 0.2.0 (git+https://github.com/dfinity/ic?rev=release-2023-09-27_23-01)", diff --git a/Cargo.toml b/Cargo.toml index 260daad5..071f3ddc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,22 +39,16 @@ serde_json = "1.0" url = "2.4" async-trait = "0.1" hex = "0.4" +futures = "0.3" [dev-dependencies] evm_rpc = { path = ".", features = ["mock"] } -# assert_matches = "1.5.0" -# ethers-core = "2.0.8" ic-ic00-types = { git = "https://github.com/dfinity/ic", rev = "release-2023-09-27_23-01" } ic-base-types = { git = "https://github.com/dfinity/ic", rev = "release-2023-09-27_23-01" } ic-config = { git = "https://github.com/dfinity/ic", rev = "release-2023-09-27_23-01" } -# ic-crypto-test-utils-reproducible-rng = { git = "https://github.com/dfinity/ic", rev = "release-2023-09-27_23-01" } -# ic-icrc1-ledger = { git = "https://github.com/dfinity/ic", rev = "release-2023-09-27_23-01" } ic-state-machine-tests = { git = "https://github.com/dfinity/ic", rev = "release-2023-09-27_23-01" } ic-test-utilities-load-wasm = { git = "https://github.com/dfinity/ic", rev = "release-2023-09-27_23-01" } -# maplit = "1" -# proptest = "1.0" -# rand = "0.8" -# scraper = "0.17.1" +assert_matches = "1.5" [workspace.dependencies] candid = { version = "0.9", features = ["parser"] } diff --git a/lib/rust/src/lib.rs b/lib/rust/src/lib.rs index f398f59d..b5d07ea7 100644 --- a/lib/rust/src/lib.rs +++ b/lib/rust/src/lib.rs @@ -8,8 +8,8 @@ pub use ethers_core as core; pub use rpc::{call_contract, get_provider, request}; -#[ic_cdk_macros::query(name = "__transform_ic_evm_rpc")] -pub fn transform_evm_rpc(args: TransformArgs) -> HttpResponse { +#[ic_cdk_macros::query(name = "__ic_evm_transform_json_rpc")] +pub fn transform_json_rpc(args: TransformArgs) -> HttpResponse { HttpResponse { status: args.response.status, body: args.response.body, diff --git a/lib/rust/src/rpc.rs b/lib/rust/src/rpc.rs index 675d646a..99d8bb7b 100644 --- a/lib/rust/src/rpc.rs +++ b/lib/rust/src/rpc.rs @@ -137,7 +137,7 @@ pub async fn request<'a, T: Serialize>( headers: request_headers, body: Some(json_rpc_payload.as_bytes().to_vec()), transform: Some(TransformContext::from_name( - "__transform_ic_evm_rpc".to_string(), + "__ic_evm_transform_json_rpc".to_string(), vec![], )), }; diff --git a/src/http.rs b/src/http.rs index b6d2adc6..a546991e 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,9 +1,11 @@ +use candid::Decode; use cketh_common::eth_rpc::{HttpOutcallError, ProviderError, RpcError, ValidationError}; use ic_canister_log::log; use ic_cdk::api::{ call::CallResult, management_canister::http_request::{ - CanisterHttpRequestArgument, HttpHeader, HttpMethod, HttpResponse, TransformContext, + CanisterHttpRequestArgument, HttpHeader, HttpMethod, HttpResponse, TransformArgs, + TransformContext, }, }; use num_traits::ToPrimitive; @@ -21,7 +23,6 @@ pub async fn do_http_request( inc_metric!(request_err_no_permission); return Err(ProviderError::NoPermission.into()); } - let cycles_available = ic_cdk::api::call::msg_cycles_available128(); let cost = get_request_cost(&source, json_rpc_payload, max_response_bytes); let (api, provider) = match source { ResolvedSource::Api(api) => (api, None), @@ -41,6 +42,7 @@ pub async fn do_http_request( return Err(ValidationError::HostNotAllowed(host.to_string()).into()); } if !is_authorized(&caller, Auth::FreeRpc) { + let cycles_available = ic_cdk::api::call::msg_cycles_available128(); if cycles_available < cost { return Err(ProviderError::TooFewCycles { expected: cost, @@ -74,7 +76,7 @@ pub async fn do_http_request( headers: request_headers, body: Some(json_rpc_payload.as_bytes().to_vec()), transform: Some(TransformContext::from_name( - "__transform_evm_rpc".to_string(), + "__transform_json_rpc".to_string(), vec![], )), }; @@ -87,6 +89,16 @@ pub async fn do_http_request( } } +pub fn do_transform_http_request(args: TransformArgs) -> HttpResponse { + HttpResponse { + status: args.response.status, + body: canonicalize_json(&args.response.body).unwrap_or(args.response.body), + // Strip headers as they contain the Date which is not necessarily the same + // and will prevent consensus on the result. + headers: Vec::::new(), + } +} + pub fn get_http_response_status(status: candid::Nat) -> u16 { status.0.to_u16().unwrap_or(u16::MAX) } @@ -104,28 +116,35 @@ pub fn get_http_response_body(response: HttpResponse) -> Result CallResult { #[cfg(any(feature = "mock", test))] { - if let Some(response) = mock_http::MOCK_OUTCALL.with(|mock| { - let mut mock = mock.borrow_mut(); - match mock.take() { - None => None, - Some(m) => { - *mock = None; - Some(m.response) + if let Some(mock) = mock_http::MOCK_OUTCALL.with(|mock| mock.borrow_mut().take()) { + mock.assert_matches(&request); + let mut response = mock.response; + if let Some(transform) = request.transform { + let method = transform.function.0.method; + response = match method.as_str() { + "__transform_json_rpc" => do_transform_http_request( + Decode!(&transform.context, TransformArgs).unwrap(), + ), + _ => panic!("Unsupported transform: {}", method), } } - }) { - return Ok(response); + Ok(response) + } else { + panic!("No mock configured for HTTP request") } } - Ok( - ic_cdk::api::management_canister::http_request::http_request(request, cycles) - .await? - .0, - ) + #[cfg(not(any(feature = "mock", test)))] + { + Ok( + ic_cdk::api::management_canister::http_request::http_request(request, cycles) + .await? + .0, + ) + } } #[cfg(any(feature = "mock", test))] @@ -135,7 +154,9 @@ pub use mock_http::*; pub mod mock_http { use std::cell::RefCell; - use ic_cdk::api::management_canister::http_request::{HttpHeader, HttpMethod, HttpResponse}; + use ic_cdk::api::management_canister::http_request::{ + CanisterHttpRequestArgument, HttpHeader, HttpMethod, HttpResponse, + }; thread_local! { pub static MOCK_OUTCALL: RefCell> = RefCell::new(None); } @@ -180,18 +201,18 @@ pub mod mock_http { self } - pub fn url(mut self, url: impl Into) -> Self { - self.0.url = Some(url.into()); + pub fn expect_url(mut self, url: impl ToString) -> Self { + self.0.url = Some(url.to_string()); self } - pub fn request_headers(mut self, headers: Vec) -> Self { + pub fn expect_headers(mut self, headers: Vec) -> Self { self.0.request_headers = Some(headers); self } - pub fn request_body(mut self, headers: Vec) -> Self { - self.0.request_headers = Some(headers); + pub fn expect_body(mut self, body: impl Into) -> Self { + self.0.request_body = Some(body.into().0); self } @@ -205,7 +226,7 @@ pub mod mock_http { pub method: Option, pub url: Option, pub request_headers: Option>, - pub request_body: Option, + pub request_body: Option>, pub response: HttpResponse, } @@ -213,6 +234,18 @@ pub mod mock_http { pub fn mock_once(self) { mock_http_request(self) } + + pub fn assert_matches(&self, request: &CanisterHttpRequestArgument) { + if let Some(ref url) = self.url { + assert_eq!(url, &request.url); + } + if let Some(ref headers) = self.request_headers { + assert_eq!(headers, &request.headers); + } + if let Some(ref body) = self.request_body { + assert_eq!(body, &request.body.as_deref().unwrap_or_default()); + } + } } impl From for MockOutcall { @@ -242,3 +275,47 @@ pub mod mock_http { }) } } + +#[cfg(test)] +mod test { + use assert_matches::assert_matches; + use cketh_common::eth_rpc_client::providers::RpcApi; + use futures::executor::block_on; + use ic_cdk::api::management_canister::http_request::HttpHeader; + + use crate::*; + + #[test] + fn test_do_http_request() { + let principal = Principal::anonymous(); + do_authorize(principal, Auth::Rpc); + do_authorize(principal, Auth::FreeRpc); + + let payload = r#"{"id":1,"jsonrpc":"2.0","method":"eth_gasPrice","params":null}"#; + let expected_result = r#"{"id":1,"jsonrpc":"2.0","result":"0x00112233"}"#; + let url = "https://cloudflare-eth.com"; + let headers = vec![HttpHeader { + name: CONTENT_TYPE_HEADER.to_string(), + value: "application/json".to_string(), + }]; + MockOutcallBuilder::new(200, expected_result) + .expect_url(url) + .expect_body(payload) + .expect_headers(headers.clone()) + .build() + .mock_once(); + + assert_matches!( + block_on(do_http_request( + principal, + ResolvedSource::Api(RpcApi { + url: url.to_string(), + headers + }), + payload, + 1000 + )), + Ok(_) + ); + } +} diff --git a/src/main.rs b/src/main.rs index df178f83..9c51db6c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use ic_canister_log::log; use ic_canisters_http_types::{ HttpRequest as AssetHttpRequest, HttpResponse as AssetHttpResponse, HttpResponseBuilder, }; -use ic_cdk::api::management_canister::http_request::{HttpHeader, HttpResponse, TransformArgs}; +use ic_cdk::api::management_canister::http_request::{HttpResponse, TransformArgs}; use ic_cdk::{query, update}; use ic_nervous_system_common::{serve_logs, serve_logs_v2, serve_metrics}; @@ -222,15 +222,9 @@ async fn withdraw_accumulated_cycles(provider_id: u64, canister_id: Principal) { }; } -#[query(name = "__transform_evm_rpc")] +#[query(name = "__transform_json_rpc")] fn transform(args: TransformArgs) -> HttpResponse { - HttpResponse { - status: args.response.status, - body: canonicalize_json(&args.response.body).unwrap_or(args.response.body), - // Strip headers as they contain the Date which is not necessarily the same - // and will prevent consensus on the result. - headers: Vec::::new(), - } + do_transform_http_request(args) } #[ic_cdk::init] diff --git a/tests/tests.rs b/tests/tests.rs index 5fb58a23..034729f5 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -24,11 +24,7 @@ const INITIAL_CYCLES: u128 = 100_000_000_000_000_000; const MAX_TICKS: usize = 10; fn evm_rpc_wasm() -> Vec { - load_wasm( - std::env::var("CARGO_MANIFEST_DIR").unwrap(), - "evm_rpc", - &["mock"], - ) + load_wasm(std::env::var("CARGO_MANIFEST_DIR").unwrap(), "evm_rpc", &[]) } fn assert_reply(result: WasmResult) -> Vec { @@ -188,12 +184,6 @@ impl EvmRpcSetup { } } -impl Drop for EvmRpcSetup { - fn drop(&mut self) { - mock_http::assert_no_mock_http_request(); - } -} - pub struct CallFlow { setup: EvmRpcSetup, method: String, @@ -228,6 +218,23 @@ impl CallFlow { let contexts = self.setup.env.canister_http_request_contexts(); let (id, context) = contexts.first_key_value().expect("no pending HTTP request"); + // mock.assert_matches(&CanisterHttpRequestArgument { + // url: context.url, + // max_response_bytes: context.max_response_bytes.map(|n|n.), + // method: match context.http_method { + // HttpMethod::GET => CanisterHttpMethod::GET, + // }, + // headers: context + // .headers + // .iter() + // .map(|h| HttpHeader { + // name: h.name, + // value: h.value, + // }) + // .collect(), + // body: context.body, + // transform: context.transform, + // }); let mut response = OutCallHttpResponse { status: mock.response.status.into(), headers: mock.response.headers, From c3942d6e9a52bfdb7a9770e765c5ef687e04731d Mon Sep 17 00:00:00 2001 From: rvanasa Date: Tue, 21 Nov 2023 13:59:35 -0700 Subject: [PATCH 15/27] Simplify HTTP outcall testing logic --- Cargo.lock | 1 - Cargo.toml | 4 - src/constants.rs | 1 + src/http.rs | 208 ++--------------------------------------------- tests/mock.rs | 98 ++++++++++++++++++++++ tests/tests.rs | 72 ++++++++++------ 6 files changed, 154 insertions(+), 230 deletions(-) create mode 100644 tests/mock.rs diff --git a/Cargo.lock b/Cargo.lock index 098bb32e..eea6b720 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1671,7 +1671,6 @@ dependencies = [ "assert_matches", "async-trait", "candid", - "evm_rpc", "futures", "hex", "ic-base-types 0.8.0", diff --git a/Cargo.toml b/Cargo.toml index 071f3ddc..1a7b44e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,9 +15,6 @@ opt-level = 'z' [profile.canister-release] inherits = "release" -[features] -mock = [] - [dependencies] candid = { workspace = true } ic-canister-log = { workspace = true } @@ -42,7 +39,6 @@ hex = "0.4" futures = "0.3" [dev-dependencies] -evm_rpc = { path = ".", features = ["mock"] } ic-ic00-types = { git = "https://github.com/dfinity/ic", rev = "release-2023-09-27_23-01" } ic-base-types = { git = "https://github.com/dfinity/ic", rev = "release-2023-09-27_23-01" } ic-config = { git = "https://github.com/dfinity/ic", rev = "release-2023-09-27_23-01" } diff --git a/src/constants.rs b/src/constants.rs index 132b1d8a..324b8937 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -20,6 +20,7 @@ pub const DEFAULT_ETHEREUM_PROVIDER: EthereumProvider = EthereumProvider::Ankr; pub const DEFAULT_SEPOLIA_PROVIDER: SepoliaProvider = SepoliaProvider::PublicNode; pub const CONTENT_TYPE_HEADER: &str = "Content-Type"; +pub const CONTENT_TYPE_VALUE: &str = "application/json"; pub const ETH_MAINNET_CHAIN_ID: u64 = 1; pub const ETH_SEPOLIA_CHAIN_ID: u64 = 11155111; diff --git a/src/http.rs b/src/http.rs index a546991e..1ebec546 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,4 +1,3 @@ -use candid::Decode; use cketh_common::eth_rpc::{HttpOutcallError, ProviderError, RpcError, ValidationError}; use ic_canister_log::log; use ic_cdk::api::{ @@ -66,7 +65,7 @@ pub async fn do_http_request( inc_metric_entry!(host_requests, host.to_string()); let mut request_headers = vec![HttpHeader { name: CONTENT_TYPE_HEADER.to_string(), - value: "application/json".to_string(), + value: CONTENT_TYPE_VALUE.to_string(), }]; request_headers.extend(api.headers); let request = CanisterHttpRequestArgument { @@ -118,204 +117,9 @@ pub async fn perform_http_request( request: CanisterHttpRequestArgument, #[allow(unused_variables)] cycles: u128, ) -> CallResult { - #[cfg(any(feature = "mock", test))] - { - if let Some(mock) = mock_http::MOCK_OUTCALL.with(|mock| mock.borrow_mut().take()) { - mock.assert_matches(&request); - let mut response = mock.response; - if let Some(transform) = request.transform { - let method = transform.function.0.method; - response = match method.as_str() { - "__transform_json_rpc" => do_transform_http_request( - Decode!(&transform.context, TransformArgs).unwrap(), - ), - _ => panic!("Unsupported transform: {}", method), - } - } - Ok(response) - } else { - panic!("No mock configured for HTTP request") - } - } - #[cfg(not(any(feature = "mock", test)))] - { - Ok( - ic_cdk::api::management_canister::http_request::http_request(request, cycles) - .await? - .0, - ) - } -} - -#[cfg(any(feature = "mock", test))] -pub use mock_http::*; - -#[cfg(any(feature = "mock", test))] -pub mod mock_http { - use std::cell::RefCell; - - use ic_cdk::api::management_canister::http_request::{ - CanisterHttpRequestArgument, HttpHeader, HttpMethod, HttpResponse, - }; - thread_local! { - pub static MOCK_OUTCALL: RefCell> = RefCell::new(None); - } - - pub struct MockOutcallBody(pub Vec); - - impl From for MockOutcallBody { - fn from(string: String) -> Self { - string.as_bytes().to_vec().into() - } - } - impl<'a> From<&'a str> for MockOutcallBody { - fn from(string: &'a str) -> Self { - string.to_string().into() - } - } - impl From> for MockOutcallBody { - fn from(bytes: Vec) -> Self { - MockOutcallBody(bytes) - } - } - - pub struct MockOutcallBuilder(MockOutcall); - - impl MockOutcallBuilder { - pub fn new(status: u16, body: impl Into) -> Self { - Self(MockOutcall { - method: None, - url: None, - request_headers: None, - request_body: None, - response: HttpResponse { - status: status.into(), - headers: vec![], - body: body.into().0, - }, - }) - } - - pub fn method(mut self, method: HttpMethod) -> Self { - self.0.method = Some(method); - self - } - - pub fn expect_url(mut self, url: impl ToString) -> Self { - self.0.url = Some(url.to_string()); - self - } - - pub fn expect_headers(mut self, headers: Vec) -> Self { - self.0.request_headers = Some(headers); - self - } - - pub fn expect_body(mut self, body: impl Into) -> Self { - self.0.request_body = Some(body.into().0); - self - } - - pub fn build(self) -> MockOutcall { - self.0 - } - } - - #[derive(Clone, Debug)] - pub struct MockOutcall { - pub method: Option, - pub url: Option, - pub request_headers: Option>, - pub request_body: Option>, - pub response: HttpResponse, - } - - impl MockOutcall { - pub fn mock_once(self) { - mock_http_request(self) - } - - pub fn assert_matches(&self, request: &CanisterHttpRequestArgument) { - if let Some(ref url) = self.url { - assert_eq!(url, &request.url); - } - if let Some(ref headers) = self.request_headers { - assert_eq!(headers, &request.headers); - } - if let Some(ref body) = self.request_body { - assert_eq!(body, &request.body.as_deref().unwrap_or_default()); - } - } - } - - impl From for MockOutcall { - fn from(response: HttpResponse) -> Self { - Self { - method: None, - url: None, - request_headers: None, - request_body: None, - response, - } - } - } - - pub fn assert_no_mock_http_request() { - assert!( - MOCK_OUTCALL.with(|mock| mock.borrow().is_none()), - "Previous mock HTTPS outcall was not used" - ) - } - - fn mock_http_request(mock: MockOutcall) { - assert_no_mock_http_request(); - MOCK_OUTCALL.with(|current_mock| { - let mut current_mock = current_mock.borrow_mut(); - *current_mock = Some(mock) - }) - } -} - -#[cfg(test)] -mod test { - use assert_matches::assert_matches; - use cketh_common::eth_rpc_client::providers::RpcApi; - use futures::executor::block_on; - use ic_cdk::api::management_canister::http_request::HttpHeader; - - use crate::*; - - #[test] - fn test_do_http_request() { - let principal = Principal::anonymous(); - do_authorize(principal, Auth::Rpc); - do_authorize(principal, Auth::FreeRpc); - - let payload = r#"{"id":1,"jsonrpc":"2.0","method":"eth_gasPrice","params":null}"#; - let expected_result = r#"{"id":1,"jsonrpc":"2.0","result":"0x00112233"}"#; - let url = "https://cloudflare-eth.com"; - let headers = vec![HttpHeader { - name: CONTENT_TYPE_HEADER.to_string(), - value: "application/json".to_string(), - }]; - MockOutcallBuilder::new(200, expected_result) - .expect_url(url) - .expect_body(payload) - .expect_headers(headers.clone()) - .build() - .mock_once(); - - assert_matches!( - block_on(do_http_request( - principal, - ResolvedSource::Api(RpcApi { - url: url.to_string(), - headers - }), - payload, - 1000 - )), - Ok(_) - ); - } + Ok( + ic_cdk::api::management_canister::http_request::http_request(request, cycles) + .await? + .0, + ) } diff --git a/tests/mock.rs b/tests/mock.rs new file mode 100644 index 00000000..1e726db7 --- /dev/null +++ b/tests/mock.rs @@ -0,0 +1,98 @@ +use ic_cdk::api::management_canister::http_request::{ + CanisterHttpRequestArgument, HttpHeader, HttpMethod, HttpResponse, +}; + +pub struct MockOutcallBody(pub Vec); + +impl From for MockOutcallBody { + fn from(string: String) -> Self { + string.as_bytes().to_vec().into() + } +} +impl<'a> From<&'a str> for MockOutcallBody { + fn from(string: &'a str) -> Self { + string.to_string().into() + } +} +impl From> for MockOutcallBody { + fn from(bytes: Vec) -> Self { + MockOutcallBody(bytes) + } +} + +pub struct MockOutcallBuilder(MockOutcall); + +impl MockOutcallBuilder { + pub fn new(status: u16, body: impl Into) -> Self { + Self(MockOutcall { + method: None, + url: None, + request_headers: None, + request_body: None, + response: HttpResponse { + status: status.into(), + headers: vec![], + body: body.into().0, + }, + }) + } + + pub fn expect_method(mut self, method: HttpMethod) -> Self { + self.0.method = Some(method); + self + } + + pub fn expect_url(mut self, url: impl ToString) -> Self { + self.0.url = Some(url.to_string()); + self + } + + pub fn expect_headers(mut self, headers: Vec) -> Self { + self.0.request_headers = Some(headers); + self + } + + pub fn expect_body(mut self, body: impl Into) -> Self { + self.0.request_body = Some(body.into().0); + self + } + + pub fn build(self) -> MockOutcall { + self.0 + } +} + +#[derive(Clone, Debug)] +pub struct MockOutcall { + pub method: Option, + pub url: Option, + pub request_headers: Option>, + pub request_body: Option>, + pub response: HttpResponse, +} + +impl MockOutcall { + pub fn assert_matches(&self, request: &CanisterHttpRequestArgument) { + if let Some(ref url) = self.url { + assert_eq!(url, &request.url); + } + if let Some(ref headers) = self.request_headers { + assert_eq!(headers, &request.headers); + } + if let Some(ref body) = self.request_body { + assert_eq!(body, &request.body.as_deref().unwrap_or_default()); + } + } +} + +impl From for MockOutcall { + fn from(response: HttpResponse) -> Self { + Self { + method: None, + url: None, + request_headers: None, + request_body: None, + response, + } + } +} diff --git a/tests/tests.rs b/tests/tests.rs index 034729f5..8e078927 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1,9 +1,12 @@ +mod mock; + use std::{marker::PhantomData, rc::Rc, time::Duration}; use candid::{CandidType, Decode, Encode, Nat}; use ic_base_types::{CanisterId, PrincipalId}; use ic_cdk::api::management_canister::http_request::{ - HttpResponse as OutCallHttpResponse, TransformArgs, + CanisterHttpRequestArgument, HttpHeader, HttpMethod, HttpResponse as OutCallHttpResponse, + TransformArgs, TransformContext, TransformFunc, }; use ic_ic00_types::CanisterSettingsArgsBuilder; use ic_state_machine_tests::{ @@ -15,6 +18,7 @@ use ic_test_utilities_load_wasm::load_wasm; use serde::de::DeserializeOwned; use evm_rpc::*; +use mock::*; const DEFAULT_CALLER_TEST_ID: u64 = 10352385; const DEFAULT_CONTROLLER_TEST_ID: u64 = 10352386; @@ -218,23 +222,30 @@ impl CallFlow { let contexts = self.setup.env.canister_http_request_contexts(); let (id, context) = contexts.first_key_value().expect("no pending HTTP request"); - // mock.assert_matches(&CanisterHttpRequestArgument { - // url: context.url, - // max_response_bytes: context.max_response_bytes.map(|n|n.), - // method: match context.http_method { - // HttpMethod::GET => CanisterHttpMethod::GET, - // }, - // headers: context - // .headers - // .iter() - // .map(|h| HttpHeader { - // name: h.name, - // value: h.value, - // }) - // .collect(), - // body: context.body, - // transform: context.transform, - // }); + mock.assert_matches(&CanisterHttpRequestArgument { + url: context.url.clone(), + max_response_bytes: context.max_response_bytes.map(|n| n.get()), + // Convert HTTP method type by name + method: serde_json::from_str( + &serde_json::to_string(&context.http_method) + .unwrap() + .to_lowercase(), + ) + .unwrap(), + headers: context + .headers + .iter() + .map(|h| HttpHeader { + name: h.name.clone(), + value: h.value.clone(), + }) + .collect(), + body: context.body.clone(), + transform: context.transform.clone().map(|t| TransformContext { + context: t.context, + function: TransformFunc::new(self.setup.canister_id.get().0, t.method_name), + }), + }); let mut response = OutCallHttpResponse { status: mock.response.status.into(), headers: mock.response.headers, @@ -295,7 +306,7 @@ impl CallFlow { } #[test] -fn should_register_providers() { +fn register_provider() { let setup = EvmRpcSetup::new(); setup.authorize_caller(Auth::RegisterProvider); @@ -361,18 +372,33 @@ fn should_register_providers() { } #[test] -fn should_allow_free_rpc() { +fn free_rpc_auth() { let setup = EvmRpcSetup::new(); setup.authorize_caller(Auth::FreeRpc); + let url = "https://cloudflare-eth.com"; + let payload = r#"{"id":1,"jsonrpc":"2.0","method":"eth_gasPrice","params":null}"#; let expected_result = r#"{"id":1,"jsonrpc":"2.0","result":"0x00112233"}"#; let result = setup .request( - Source::Provider(0), - r#"{"id":1,"jsonrpc":"2.0","method":"eth_gasPrice","params":null}"#, + Source::Custom { + url: url.to_string(), + headers: None, + }, + payload, 1000, ) - .mock_http(MockOutcallBuilder::new(200, expected_result).build()) + .mock_http( + MockOutcallBuilder::new(200, expected_result) + .expect_url(url.to_string()) + .expect_method(HttpMethod::GET) + .expect_body(payload) + .expect_headers(vec![HttpHeader { + name: CONTENT_TYPE_HEADER.to_string(), + value: CONTENT_TYPE_VALUE.to_string(), + }]) + .build(), + ) .wait() .expect("request()"); assert_eq!(result, expected_result); From 6ab1cdd392c0f935388c8f96fe0a2cd9547817d6 Mon Sep 17 00:00:00 2001 From: rvanasa Date: Tue, 21 Nov 2023 14:05:49 -0700 Subject: [PATCH 16/27] Remove unused Cargo dependency --- Cargo.lock | 1 - Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eea6b720..2cd6e3e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1671,7 +1671,6 @@ dependencies = [ "assert_matches", "async-trait", "candid", - "futures", "hex", "ic-base-types 0.8.0", "ic-canister-log 0.2.0 (git+https://github.com/dfinity/ic?rev=release-2023-09-27_23-01)", diff --git a/Cargo.toml b/Cargo.toml index 1a7b44e7..c5144a28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,6 @@ serde_json = "1.0" url = "2.4" async-trait = "0.1" hex = "0.4" -futures = "0.3" [dev-dependencies] ic-ic00-types = { git = "https://github.com/dfinity/ic", rev = "release-2023-09-27_23-01" } From f8671b95b21e67d900a5af48540c4f5dce910823 Mon Sep 17 00:00:00 2001 From: rvanasa Date: Tue, 21 Nov 2023 14:11:06 -0700 Subject: [PATCH 17/27] Reformat --- tests/tests.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/tests.rs b/tests/tests.rs index 8e078927..d725aa67 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -399,7 +399,6 @@ fn free_rpc_auth() { }]) .build(), ) - .wait() - .expect("request()"); - assert_eq!(result, expected_result); + .wait(); + assert_eq!(result, Ok(expected_result.to_string())); } From 280a274e7f8dc915f98b81cff7abf4a869a97ebd Mon Sep 17 00:00:00 2001 From: rvanasa Date: Tue, 21 Nov 2023 14:12:21 -0700 Subject: [PATCH 18/27] Fix linter warnings --- tests/tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/tests.rs b/tests/tests.rs index d725aa67..0a35f074 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -247,7 +247,7 @@ impl CallFlow { }), }); let mut response = OutCallHttpResponse { - status: mock.response.status.into(), + status: mock.response.status, headers: mock.response.headers, body: mock.response.body, }; @@ -283,7 +283,7 @@ impl CallFlow { .collect(), body: response.body, }; - let payload = PayloadBuilder::new().http_response(id.clone(), &http_response); + let payload = PayloadBuilder::new().http_response(*id, &http_response); self.setup.env.execute_payload(payload); self From 56fe2fb86def151723d032dd41796a373a049134 Mon Sep 17 00:00:00 2001 From: rvanasa Date: Tue, 21 Nov 2023 14:21:17 -0700 Subject: [PATCH 19/27] Rename local variable --- tests/tests.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/tests.rs b/tests/tests.rs index 0a35f074..b01d8fa2 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -63,7 +63,7 @@ impl EvmRpcSetup { ); let controller = PrincipalId::new_user_test_id(DEFAULT_CONTROLLER_TEST_ID); - let evm_rpc_id = env.create_canister_with_cycles( + let canister_id = env.create_canister_with_cycles( None, Cycles::new(INITIAL_CYCLES), Some( @@ -72,7 +72,7 @@ impl EvmRpcSetup { .build(), ), ); - env.install_existing_canister(evm_rpc_id, evm_rpc_wasm(), Encode!(&()).unwrap()) + env.install_existing_canister(canister_id, evm_rpc_wasm(), Encode!(&()).unwrap()) .unwrap(); let caller = PrincipalId::new_user_test_id(DEFAULT_CALLER_TEST_ID); @@ -81,7 +81,7 @@ impl EvmRpcSetup { env, caller, controller, - canister_id: evm_rpc_id, + canister_id, } } From e8081162417d0a86e6e11a54beb83809fa9f60c4 Mon Sep 17 00:00:00 2001 From: rvanasa Date: Tue, 21 Nov 2023 14:31:58 -0700 Subject: [PATCH 20/27] Simplify --- src/candid_rpc.rs | 6 +++++- src/http.rs | 24 +++++------------------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/candid_rpc.rs b/src/candid_rpc.rs index 5026933b..a9bb5e71 100644 --- a/src/candid_rpc.rs +++ b/src/candid_rpc.rs @@ -40,7 +40,11 @@ impl RpcTransport for CanisterTransport { request: CanisterHttpRequestArgument, cost: u128, ) -> CallResult { - perform_http_request(request, cost).await + Ok( + ic_cdk::api::management_canister::http_request::http_request(request, cost) + .await? + .0, + ) } } diff --git a/src/http.rs b/src/http.rs index 1ebec546..025fc906 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,11 +1,8 @@ use cketh_common::eth_rpc::{HttpOutcallError, ProviderError, RpcError, ValidationError}; use ic_canister_log::log; -use ic_cdk::api::{ - call::CallResult, - management_canister::http_request::{ - CanisterHttpRequestArgument, HttpHeader, HttpMethod, HttpResponse, TransformArgs, - TransformContext, - }, +use ic_cdk::api::management_canister::http_request::{ + CanisterHttpRequestArgument, HttpHeader, HttpMethod, HttpResponse, TransformArgs, + TransformContext, }; use num_traits::ToPrimitive; @@ -79,8 +76,8 @@ pub async fn do_http_request( vec![], )), }; - match perform_http_request(request, cost).await { - Ok(response) => Ok(response), + match ic_cdk::api::management_canister::http_request::http_request(request, cost).await { + Ok((response,)) => Ok(response), Err((code, message)) => { inc_metric!(request_err_http); Err(HttpOutcallError::IcError { code, message }.into()) @@ -112,14 +109,3 @@ pub fn get_http_response_body(response: HttpResponse) -> Result CallResult { - Ok( - ic_cdk::api::management_canister::http_request::http_request(request, cycles) - .await? - .0, - ) -} From c8142a99b0918ce9175ab632f94df09440148c90 Mon Sep 17 00:00:00 2001 From: rvanasa Date: Wed, 22 Nov 2023 11:25:59 -0700 Subject: [PATCH 21/27] Update unit test names --- tests/tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/tests.rs b/tests/tests.rs index b01d8fa2..c683aa8e 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -306,7 +306,7 @@ impl CallFlow { } #[test] -fn register_provider() { +fn should_register_provider() { let setup = EvmRpcSetup::new(); setup.authorize_caller(Auth::RegisterProvider); @@ -372,7 +372,7 @@ fn register_provider() { } #[test] -fn free_rpc_auth() { +fn should_query_gas_price_from_free_rpc_provider() { let setup = EvmRpcSetup::new(); setup.authorize_caller(Auth::FreeRpc); From 1fcd52e2102e502f5d5c9fd62e985f994ba20a4a Mon Sep 17 00:00:00 2001 From: Ryan Vandersmith Date: Wed, 22 Nov 2023 11:57:34 -0700 Subject: [PATCH 22/27] Update src/http.rs Co-authored-by: Thomas Locher --- src/http.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/http.rs b/src/http.rs index 025fc906..537ecd3d 100644 --- a/src/http.rs +++ b/src/http.rs @@ -89,7 +89,7 @@ pub fn do_transform_http_request(args: TransformArgs) -> HttpResponse { HttpResponse { status: args.response.status, body: canonicalize_json(&args.response.body).unwrap_or(args.response.body), - // Strip headers as they contain the Date which is not necessarily the same + // Strip headers as they contain the date which is not necessarily the same // and will prevent consensus on the result. headers: Vec::::new(), } From 66268727df9a8593352f4db0faa4448c3da9037a Mon Sep 17 00:00:00 2001 From: rvanasa Date: Wed, 22 Nov 2023 15:10:53 -0700 Subject: [PATCH 23/27] Test each individual part of mock HTTPS outcalls --- Cargo.lock | 52 ++++++++++++------------- tests/mock.rs | 40 ++++++++++++++++--- tests/tests.rs | 104 +++++++++++++++++++++++++++++++++++-------------- 3 files changed, 135 insertions(+), 61 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dc9ac484..c049644e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1785,9 +1785,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -2349,7 +2349,7 @@ dependencies = [ [[package]] name = "ic-base-types" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#fc43f0f42bb1606713cd89cf4917b1554d129f1d" dependencies = [ "base32", "byte-unit", @@ -2392,7 +2392,7 @@ dependencies = [ [[package]] name = "ic-btc-types-internal" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#fc43f0f42bb1606713cd89cf4917b1554d129f1d" dependencies = [ "candid", "ic-btc-interface", @@ -2510,7 +2510,7 @@ dependencies = [ [[package]] name = "ic-canisters-http-types" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#fc43f0f42bb1606713cd89cf4917b1554d129f1d" dependencies = [ "candid", "serde", @@ -2642,7 +2642,7 @@ dependencies = [ [[package]] name = "ic-cketh-minter" version = "0.1.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#fc43f0f42bb1606713cd89cf4917b1554d129f1d" dependencies = [ "askama", "async-trait", @@ -2777,7 +2777,7 @@ dependencies = [ [[package]] name = "ic-crypto-ecdsa-secp256k1" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#fc43f0f42bb1606713cd89cf4917b1554d129f1d" dependencies = [ "k256", "lazy_static", @@ -3096,7 +3096,7 @@ dependencies = [ [[package]] name = "ic-crypto-internal-sha2" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#fc43f0f42bb1606713cd89cf4917b1554d129f1d" dependencies = [ "sha2 0.10.8", ] @@ -3262,7 +3262,7 @@ dependencies = [ [[package]] name = "ic-crypto-sha2" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#fc43f0f42bb1606713cd89cf4917b1554d129f1d" dependencies = [ "ic-crypto-internal-sha2 0.9.0", ] @@ -3270,7 +3270,7 @@ dependencies = [ [[package]] name = "ic-crypto-sha3" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#fc43f0f42bb1606713cd89cf4917b1554d129f1d" dependencies = [ "sha3 0.9.1", ] @@ -3484,7 +3484,7 @@ dependencies = [ [[package]] name = "ic-error-types" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#fc43f0f42bb1606713cd89cf4917b1554d129f1d" dependencies = [ "ic-utils 0.9.0", "serde", @@ -3587,7 +3587,7 @@ dependencies = [ [[package]] name = "ic-ic00-types" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#fc43f0f42bb1606713cd89cf4917b1554d129f1d" dependencies = [ "candid", "ic-base-types 0.9.0", @@ -3877,7 +3877,7 @@ dependencies = [ [[package]] name = "ic-protobuf" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#fc43f0f42bb1606713cd89cf4917b1554d129f1d" dependencies = [ "bincode", "candid", @@ -4199,7 +4199,7 @@ dependencies = [ [[package]] name = "ic-sys" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#fc43f0f42bb1606713cd89cf4917b1554d129f1d" dependencies = [ "hex", "ic-crypto-sha2 0.9.0", @@ -4341,7 +4341,7 @@ dependencies = [ [[package]] name = "ic-utils" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#fc43f0f42bb1606713cd89cf4917b1554d129f1d" dependencies = [ "cvt", "hex", @@ -4358,7 +4358,7 @@ dependencies = [ [[package]] name = "ic-utils-ensure" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#fc43f0f42bb1606713cd89cf4917b1554d129f1d" [[package]] name = "ic-utils-lru-cache" @@ -4436,7 +4436,7 @@ dependencies = [ [[package]] name = "icrc-ledger-client" version = "0.1.2" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#fc43f0f42bb1606713cd89cf4917b1554d129f1d" dependencies = [ "async-trait", "candid", @@ -4447,7 +4447,7 @@ dependencies = [ [[package]] name = "icrc-ledger-client-cdk" version = "0.1.2" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#fc43f0f42bb1606713cd89cf4917b1554d129f1d" dependencies = [ "async-trait", "candid", @@ -4473,7 +4473,7 @@ dependencies = [ [[package]] name = "icrc-ledger-types" version = "0.1.4" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#fc43f0f42bb1606713cd89cf4917b1554d129f1d" dependencies = [ "base32", "candid", @@ -4494,9 +4494,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -5478,9 +5478,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" @@ -5550,7 +5550,7 @@ dependencies = [ [[package]] name = "phantom_newtype" version = "0.9.0" -source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#313e959fad7b1790ccdb8b8595e1a6b2959a611f" +source = "git+https://github.com/rvanasa/ic?branch=evm-rpc-canister#fc43f0f42bb1606713cd89cf4917b1554d129f1d" dependencies = [ "candid", "serde", @@ -7624,9 +7624,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", diff --git a/tests/mock.rs b/tests/mock.rs index 1e726db7..a8431f1d 100644 --- a/tests/mock.rs +++ b/tests/mock.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use ic_cdk::api::management_canister::http_request::{ CanisterHttpRequestArgument, HttpHeader, HttpMethod, HttpResponse, }; @@ -20,6 +22,7 @@ impl From> for MockOutcallBody { } } +#[derive(Clone, Debug)] pub struct MockOutcallBuilder(MockOutcall); impl MockOutcallBuilder { @@ -37,31 +40,50 @@ impl MockOutcallBuilder { }) } - pub fn expect_method(mut self, method: HttpMethod) -> Self { + pub fn with_method(mut self, method: HttpMethod) -> Self { self.0.method = Some(method); self } - pub fn expect_url(mut self, url: impl ToString) -> Self { + pub fn with_url(mut self, url: impl ToString) -> Self { self.0.url = Some(url.to_string()); self } - pub fn expect_headers(mut self, headers: Vec) -> Self { - self.0.request_headers = Some(headers); + pub fn with_request_headers(mut self, headers: Vec<(impl ToString, impl ToString)>) -> Self { + self.0.request_headers = Some( + headers + .into_iter() + .map(|(name, value)| HttpHeader { + name: name.to_string(), + value: value.to_string(), + }) + .collect(), + ); self } - pub fn expect_body(mut self, body: impl Into) -> Self { + pub fn with_request_body(mut self, body: impl Into) -> Self { self.0.request_body = Some(body.into().0); self } + pub fn with_response_header(mut self, name: String, value: String) -> Self { + self.0.response.headers.push(HttpHeader { name, value }); + self + } + pub fn build(self) -> MockOutcall { self.0 } } +impl From for MockOutcall { + fn from(builder: MockOutcallBuilder) -> Self { + builder.build() + } +} + #[derive(Clone, Debug)] pub struct MockOutcall { pub method: Option, @@ -76,8 +98,14 @@ impl MockOutcall { if let Some(ref url) = self.url { assert_eq!(url, &request.url); } + if let Some(ref method) = self.method { + assert_eq!(method, &request.method); + } if let Some(ref headers) = self.request_headers { - assert_eq!(headers, &request.headers); + assert_eq!( + headers.iter().collect::>(), + request.headers.iter().collect::>() + ); } if let Some(ref body) = self.request_body { assert_eq!(body, &request.body.as_deref().unwrap_or_default()); diff --git a/tests/tests.rs b/tests/tests.rs index c683aa8e..33ad76e8 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -2,6 +2,7 @@ mod mock; use std::{marker::PhantomData, rc::Rc, time::Duration}; +use assert_matches::assert_matches; use candid::{CandidType, Decode, Encode, Nat}; use ic_base_types::{CanisterId, PrincipalId}; use ic_cdk::api::management_canister::http_request::{ @@ -13,7 +14,6 @@ use ic_state_machine_tests::{ CanisterHttpResponsePayload, Cycles, IngressState, IngressStatus, MessageId, PayloadBuilder, StateMachine, StateMachineBuilder, WasmResult, }; - use ic_test_utilities_load_wasm::load_wasm; use serde::de::DeserializeOwned; @@ -27,6 +27,11 @@ const INITIAL_CYCLES: u128 = 100_000_000_000_000_000; const MAX_TICKS: usize = 10; +const GAS_PRICE_URL: &str = "https://cloudflare-eth.com"; +const GAS_PRICE_PAYLOAD: &str = r#"{"id":1,"jsonrpc":"2.0","method":"eth_gasPrice","params":null}"#; +const GAS_PRICE_RESPONSE: &str = r#"{"id":1,"jsonrpc":"2.0","result":"0x00112233"}"#; +const GAS_PRICE_RESPONSE_BYTES: u64 = 1000; + fn evm_rpc_wasm() -> Vec { load_wasm(std::env::var("CARGO_MANIFEST_DIR").unwrap(), "evm_rpc", &[]) } @@ -212,7 +217,8 @@ impl CallFlow { } } - pub fn mock_http(self, mock: MockOutcall) -> Self { + pub fn mock_http(self, mock: impl Into) -> Self { + let mock = mock.into(); assert_eq!(self.setup.env.canister_http_request_contexts().len(), 0); self.setup.tick_until_http_request(); match self.setup.env.ingress_status(&self.message_id) { @@ -371,34 +377,74 @@ fn should_register_provider() { ) } -#[test] -fn should_query_gas_price_from_free_rpc_provider() { +fn setup_gas_price_request(builder_fn: impl Fn(MockOutcallBuilder) -> MockOutcallBuilder) { let setup = EvmRpcSetup::new(); setup.authorize_caller(Auth::FreeRpc); - let url = "https://cloudflare-eth.com"; - let payload = r#"{"id":1,"jsonrpc":"2.0","method":"eth_gasPrice","params":null}"#; - let expected_result = r#"{"id":1,"jsonrpc":"2.0","result":"0x00112233"}"#; - let result = setup - .request( - Source::Custom { - url: url.to_string(), - headers: None, - }, - payload, - 1000, - ) - .mock_http( - MockOutcallBuilder::new(200, expected_result) - .expect_url(url.to_string()) - .expect_method(HttpMethod::GET) - .expect_body(payload) - .expect_headers(vec![HttpHeader { - name: CONTENT_TYPE_HEADER.to_string(), - value: CONTENT_TYPE_VALUE.to_string(), - }]) - .build(), - ) - .wait(); - assert_eq!(result, Ok(expected_result.to_string())); + assert_matches!( + setup + .request( + Source::Custom { + url: GAS_PRICE_URL.to_string(), + headers: Some(vec![HttpHeader { + name: "Custom".to_string(), + value: "Value".to_string(), + }]), + }, + GAS_PRICE_PAYLOAD, + GAS_PRICE_RESPONSE_BYTES, + ) + .mock_http(builder_fn(MockOutcallBuilder::new(200, GAS_PRICE_RESPONSE))) + .wait(), + Ok(_) + ); +} + +#[test] +fn should_request_succeed() { + setup_gas_price_request(|builder| builder) +} + +#[test] +fn should_request_succeed_with_method() { + setup_gas_price_request(|builder| builder.with_method(HttpMethod::POST)) +} + +#[test] +fn should_request_succeed_with_request_headers() { + setup_gas_price_request(|builder| { + builder.with_request_headers(vec![ + (CONTENT_TYPE_HEADER, CONTENT_TYPE_VALUE), + ("Custom", "Value"), + ]) + }) +} + +#[test] +fn should_request_succeed_with_request_body() { + setup_gas_price_request(|builder| builder.with_request_body(GAS_PRICE_PAYLOAD)) +} + +#[test] +#[should_panic(expected = "assertion failed: `(left == right)`")] +fn should_request_fail_with_url() { + setup_gas_price_request(|builder| builder.with_url("https://not-the-url.com")) +} + +#[test] +#[should_panic(expected = "assertion failed: `(left == right)`")] +fn should_request_fail_with_method() { + setup_gas_price_request(|builder| builder.with_method(HttpMethod::GET)) +} + +#[test] +#[should_panic(expected = "assertion failed: `(left == right)`")] +fn should_request_fail_with_request_headers() { + setup_gas_price_request(|builder| builder.with_request_headers(vec![("Custom", "NotValue")])) +} + +#[test] +#[should_panic(expected = "assertion failed: `(left == right)`")] +fn should_request_fail_with_request_body() { + setup_gas_price_request(|builder| builder.with_request_body(r#"{"different":"body"}"#)) } From bad4f785d86d779967b5f23b9f82c12ef1127cbc Mon Sep 17 00:00:00 2001 From: rvanasa Date: Wed, 22 Nov 2023 15:24:51 -0700 Subject: [PATCH 24/27] Refactor --- tests/tests.rs | 51 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/tests/tests.rs b/tests/tests.rs index 33ad76e8..1f92fa20 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -377,7 +377,7 @@ fn should_register_provider() { ) } -fn setup_gas_price_request(builder_fn: impl Fn(MockOutcallBuilder) -> MockOutcallBuilder) { +fn gas_price_request(builder_fn: impl Fn(MockOutcallBuilder) -> MockOutcallBuilder) { let setup = EvmRpcSetup::new(); setup.authorize_caller(Auth::FreeRpc); @@ -402,17 +402,17 @@ fn setup_gas_price_request(builder_fn: impl Fn(MockOutcallBuilder) -> MockOutcal #[test] fn should_request_succeed() { - setup_gas_price_request(|builder| builder) + gas_price_request(|builder| builder) } #[test] fn should_request_succeed_with_method() { - setup_gas_price_request(|builder| builder.with_method(HttpMethod::POST)) + gas_price_request(|builder| builder.with_method(HttpMethod::POST)) } #[test] fn should_request_succeed_with_request_headers() { - setup_gas_price_request(|builder| { + gas_price_request(|builder| { builder.with_request_headers(vec![ (CONTENT_TYPE_HEADER, CONTENT_TYPE_VALUE), ("Custom", "Value"), @@ -422,29 +422,58 @@ fn should_request_succeed_with_request_headers() { #[test] fn should_request_succeed_with_request_body() { - setup_gas_price_request(|builder| builder.with_request_body(GAS_PRICE_PAYLOAD)) + gas_price_request(|builder| builder.with_request_body(GAS_PRICE_PAYLOAD)) } #[test] -#[should_panic(expected = "assertion failed: `(left == right)`")] +#[should_panic( + expected = "assertion failed: `(left == right)`" +)] fn should_request_fail_with_url() { - setup_gas_price_request(|builder| builder.with_url("https://not-the-url.com")) + gas_price_request(|builder| builder.with_url("https://not-the-url.com")) } #[test] #[should_panic(expected = "assertion failed: `(left == right)`")] fn should_request_fail_with_method() { - setup_gas_price_request(|builder| builder.with_method(HttpMethod::GET)) + gas_price_request(|builder| builder.with_method(HttpMethod::GET)) } #[test] -#[should_panic(expected = "assertion failed: `(left == right)`")] +#[should_panic( + expected = "assertion failed: `(left == right)`" +)] fn should_request_fail_with_request_headers() { - setup_gas_price_request(|builder| builder.with_request_headers(vec![("Custom", "NotValue")])) + gas_price_request(|builder| builder.with_request_headers(vec![("Custom", "NotValue")])) } #[test] #[should_panic(expected = "assertion failed: `(left == right)`")] fn should_request_fail_with_request_body() { - setup_gas_price_request(|builder| builder.with_request_body(r#"{"different":"body"}"#)) + gas_price_request(|builder| builder.with_request_body(r#"{"different":"body"}"#)) +} + +#[test] +fn should_gas_price_request_succeed_from_free_rpc_caller() { + let setup = EvmRpcSetup::new(); + setup.authorize_caller(Auth::FreeRpc); + + let result = setup + .request( + Source::Custom { + url: GAS_PRICE_URL.to_string(), + headers: None, + }, + GAS_PRICE_PAYLOAD, + GAS_PRICE_RESPONSE_BYTES, + ) + .mock_http( + MockOutcallBuilder::new(200, GAS_PRICE_RESPONSE) + .with_url(GAS_PRICE_URL.to_string()) + .with_method(HttpMethod::POST) + .with_request_body(GAS_PRICE_PAYLOAD) + .with_request_headers(vec![(CONTENT_TYPE_HEADER, CONTENT_TYPE_VALUE)]), + ) + .wait(); + assert_eq!(result, Ok(GAS_PRICE_RESPONSE.to_string())); } From 529351f56e55bca1c1924e6bdc53b0ee02a6e79d Mon Sep 17 00:00:00 2001 From: rvanasa Date: Wed, 22 Nov 2023 15:28:06 -0700 Subject: [PATCH 25/27] Add 'should_request_succeed_with_url' test --- tests/tests.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/tests.rs b/tests/tests.rs index 1f92fa20..5564bc60 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -405,6 +405,11 @@ fn should_request_succeed() { gas_price_request(|builder| builder) } +#[test] +fn should_request_succeed_with_url() { + gas_price_request(|builder| builder.with_url(GAS_PRICE_URL)) +} + #[test] fn should_request_succeed_with_method() { gas_price_request(|builder| builder.with_method(HttpMethod::POST)) @@ -426,9 +431,7 @@ fn should_request_succeed_with_request_body() { } #[test] -#[should_panic( - expected = "assertion failed: `(left == right)`" -)] +#[should_panic(expected = "assertion failed: `(left == right)`")] fn should_request_fail_with_url() { gas_price_request(|builder| builder.with_url("https://not-the-url.com")) } @@ -440,9 +443,7 @@ fn should_request_fail_with_method() { } #[test] -#[should_panic( - expected = "assertion failed: `(left == right)`" -)] +#[should_panic(expected = "assertion failed: `(left == right)`")] fn should_request_fail_with_request_headers() { gas_price_request(|builder| builder.with_request_headers(vec![("Custom", "NotValue")])) } From b2de71993eea2991feee6d0e58829b17138f862a Mon Sep 17 00:00:00 2001 From: rvanasa Date: Wed, 22 Nov 2023 15:38:01 -0700 Subject: [PATCH 26/27] Test JSON canonicalization --- tests/tests.rs | 113 +++++++++++++++++++++++++++++-------------------- 1 file changed, 66 insertions(+), 47 deletions(-) diff --git a/tests/tests.rs b/tests/tests.rs index 5564bc60..8befc70c 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -27,10 +27,10 @@ const INITIAL_CYCLES: u128 = 100_000_000_000_000_000; const MAX_TICKS: usize = 10; -const GAS_PRICE_URL: &str = "https://cloudflare-eth.com"; -const GAS_PRICE_PAYLOAD: &str = r#"{"id":1,"jsonrpc":"2.0","method":"eth_gasPrice","params":null}"#; -const GAS_PRICE_RESPONSE: &str = r#"{"id":1,"jsonrpc":"2.0","result":"0x00112233"}"#; -const GAS_PRICE_RESPONSE_BYTES: u64 = 1000; +const MOCK_REQUEST_URL: &str = "https://cloudflare-eth.com"; +const MOCK_REQUEST_PAYLOAD: &str = r#"{"id":1,"jsonrpc":"2.0","method":"eth_gasPrice"}"#; +const MOCK_REQUEST_RESPONSE: &str = r#"{"id":1,"jsonrpc":"2.0","result":"0x00112233"}"#; +const MOCK_REQUEST_RESPONSE_BYTES: u64 = 1000; fn evm_rpc_wasm() -> Vec { load_wasm(std::env::var("CARGO_MANIFEST_DIR").unwrap(), "evm_rpc", &[]) @@ -377,7 +377,7 @@ fn should_register_provider() { ) } -fn gas_price_request(builder_fn: impl Fn(MockOutcallBuilder) -> MockOutcallBuilder) { +fn mock_request(builder_fn: impl Fn(MockOutcallBuilder) -> MockOutcallBuilder) { let setup = EvmRpcSetup::new(); setup.authorize_caller(Auth::FreeRpc); @@ -385,39 +385,42 @@ fn gas_price_request(builder_fn: impl Fn(MockOutcallBuilder) -> MockOutcallBuild setup .request( Source::Custom { - url: GAS_PRICE_URL.to_string(), + url: MOCK_REQUEST_URL.to_string(), headers: Some(vec![HttpHeader { name: "Custom".to_string(), value: "Value".to_string(), }]), }, - GAS_PRICE_PAYLOAD, - GAS_PRICE_RESPONSE_BYTES, + MOCK_REQUEST_PAYLOAD, + MOCK_REQUEST_RESPONSE_BYTES, ) - .mock_http(builder_fn(MockOutcallBuilder::new(200, GAS_PRICE_RESPONSE))) + .mock_http(builder_fn(MockOutcallBuilder::new( + 200, + MOCK_REQUEST_RESPONSE + ))) .wait(), Ok(_) ); } #[test] -fn should_request_succeed() { - gas_price_request(|builder| builder) +fn mock_request_should_succeed() { + mock_request(|builder| builder) } #[test] -fn should_request_succeed_with_url() { - gas_price_request(|builder| builder.with_url(GAS_PRICE_URL)) +fn mock_request_should_succeed_with_url() { + mock_request(|builder| builder.with_url(MOCK_REQUEST_URL)) } #[test] -fn should_request_succeed_with_method() { - gas_price_request(|builder| builder.with_method(HttpMethod::POST)) +fn mock_request_should_succeed_with_method() { + mock_request(|builder| builder.with_method(HttpMethod::POST)) } #[test] -fn should_request_succeed_with_request_headers() { - gas_price_request(|builder| { +fn mock_request_should_succeed_with_request_headers() { + mock_request(|builder| { builder.with_request_headers(vec![ (CONTENT_TYPE_HEADER, CONTENT_TYPE_VALUE), ("Custom", "Value"), @@ -426,55 +429,71 @@ fn should_request_succeed_with_request_headers() { } #[test] -fn should_request_succeed_with_request_body() { - gas_price_request(|builder| builder.with_request_body(GAS_PRICE_PAYLOAD)) +fn mock_request_should_succeed_with_request_body() { + mock_request(|builder| builder.with_request_body(MOCK_REQUEST_PAYLOAD)) +} + +#[test] +fn mock_request_should_succeed_with_all() { + mock_request(|builder| { + builder + .with_url(MOCK_REQUEST_URL) + .with_method(HttpMethod::POST) + .with_request_headers(vec![ + (CONTENT_TYPE_HEADER, CONTENT_TYPE_VALUE), + ("Custom", "Value"), + ]) + .with_request_body(MOCK_REQUEST_PAYLOAD) + }) } #[test] #[should_panic(expected = "assertion failed: `(left == right)`")] -fn should_request_fail_with_url() { - gas_price_request(|builder| builder.with_url("https://not-the-url.com")) +fn mock_request_should_fail_with_url() { + mock_request(|builder| builder.with_url("https://not-the-url.com")) } #[test] #[should_panic(expected = "assertion failed: `(left == right)`")] -fn should_request_fail_with_method() { - gas_price_request(|builder| builder.with_method(HttpMethod::GET)) +fn mock_request_should_fail_with_method() { + mock_request(|builder| builder.with_method(HttpMethod::GET)) } #[test] #[should_panic(expected = "assertion failed: `(left == right)`")] -fn should_request_fail_with_request_headers() { - gas_price_request(|builder| builder.with_request_headers(vec![("Custom", "NotValue")])) +fn mock_request_should_fail_with_request_headers() { + mock_request(|builder| builder.with_request_headers(vec![("Custom", "NotValue")])) } #[test] #[should_panic(expected = "assertion failed: `(left == right)`")] -fn should_request_fail_with_request_body() { - gas_price_request(|builder| builder.with_request_body(r#"{"different":"body"}"#)) +fn mock_request_should_fail_with_request_body() { + mock_request(|builder| builder.with_request_body(r#"{"different":"body"}"#)) } #[test] -fn should_gas_price_request_succeed_from_free_rpc_caller() { +fn should_canonicalize_json_response() { let setup = EvmRpcSetup::new(); setup.authorize_caller(Auth::FreeRpc); - - let result = setup - .request( - Source::Custom { - url: GAS_PRICE_URL.to_string(), - headers: None, - }, - GAS_PRICE_PAYLOAD, - GAS_PRICE_RESPONSE_BYTES, - ) - .mock_http( - MockOutcallBuilder::new(200, GAS_PRICE_RESPONSE) - .with_url(GAS_PRICE_URL.to_string()) - .with_method(HttpMethod::POST) - .with_request_body(GAS_PRICE_PAYLOAD) - .with_request_headers(vec![(CONTENT_TYPE_HEADER, CONTENT_TYPE_VALUE)]), - ) - .wait(); - assert_eq!(result, Ok(GAS_PRICE_RESPONSE.to_string())); + let responses = [ + r#"{"id":1,"jsonrpc":"2.0","result":"0x00112233"}"#, + r#"{"result":"0x00112233","id":1,"jsonrpc":"2.0"}"#, + r#"{"result":"0x00112233","jsonrpc":"2.0","id":1}"#, + ] + .into_iter() + .map(|response| { + setup + .request( + Source::Custom { + url: MOCK_REQUEST_URL.to_string(), + headers: None, + }, + MOCK_REQUEST_PAYLOAD, + MOCK_REQUEST_RESPONSE_BYTES, + ) + .mock_http(MockOutcallBuilder::new(200, response)) + .wait() + }) + .collect::>(); + assert!(responses.windows(2).all(|w| w[0] == w[1])); } From c83f855206cee6d24a1eeb35c33b36eaeddd09bc Mon Sep 17 00:00:00 2001 From: rvanasa Date: Wed, 22 Nov 2023 15:46:11 -0700 Subject: [PATCH 27/27] Adjust HTTPS outcall transform source code --- src/http.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/http.rs b/src/http.rs index 537ecd3d..bf15ad39 100644 --- a/src/http.rs +++ b/src/http.rs @@ -89,9 +89,8 @@ pub fn do_transform_http_request(args: TransformArgs) -> HttpResponse { HttpResponse { status: args.response.status, body: canonicalize_json(&args.response.body).unwrap_or(args.response.body), - // Strip headers as they contain the date which is not necessarily the same - // and will prevent consensus on the result. - headers: Vec::::new(), + // Remove headers (which may contain a timestamp) for consensus + headers: vec![], } }