Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add canister init args #112

Merged
merged 13 commits into from
Jan 3, 2024
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ jobs:
run: npm ci

- name: Deploy EVM RPC
run: dfx deploy evm_rpc
run: dfx deploy evm_rpc --argument "(record {nodesInSubnet = 13})"

- name: Deploy EVM RPC fiduciary canister
run: dfx deploy evm_rpc_fiduciary --argument "(record {nodesInSubnet = 28})"

- name: Generate language bindings
run: npm run generate
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ edition = "2021"
[profile.release]
debug = false
lto = true
strip = true
Copy link
Collaborator Author

@rvanasa rvanasa Jan 3, 2024

Choose a reason for hiding this comment

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

(Quick fix for getting below the 2 MB Wasm size limit)

opt-level = 's'

# Required by `ic-test-utilities-load-wasm`
Expand Down
7 changes: 4 additions & 3 deletions candid/evm_rpc.did
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ type HttpOutcallError = variant {
parsingError : opt text;
};
};
type InitArgs = record {
nodesInSubnet : nat32;
};
type JsonRpcError = record { code : int64; message : text };
type JsonRpcSource = variant {
Custom : record { url : text; headers : opt vec HttpHeader };
Expand Down Expand Up @@ -199,7 +202,7 @@ type ValidationError = variant {
UrlParseError : text;
InvalidHex : text;
};
service : {
service : (InitArgs) -> {
authorize : (principal, Auth) -> ();
deauthorize : (principal, Auth) -> ();
eth_feeHistory : (RpcSource, FeeHistoryArgs) -> (MultiFeeHistoryResult);
Expand All @@ -212,13 +215,11 @@ service : {
eth_sendRawTransaction : (RpcSource, text) -> (MultiSendRawTransactionResult);
getAccumulatedCycleCount : (nat64) -> (nat) query;
getAuthorized : (Auth) -> (vec text) query;
getNodesInSubnet : () -> (nat32) query;
getOpenRpcAccess : () -> (bool) query;
getProviders : () -> (vec ProviderView) query;
registerProvider : (RegisterProviderArgs) -> (nat64);
request : (JsonRpcSource, text, nat64) -> (RequestResult);
requestCost : (JsonRpcSource, text, nat64) -> (RequestCostResult) query;
setNodesInSubnet : (nat32) -> ();
setOpenRpcAccess : (bool) -> ();
unregisterProvider : (nat64) -> (bool);
updateProvider : (UpdateProviderArgs) -> ();
Expand Down
4 changes: 2 additions & 2 deletions dfx.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
"package": "evm_rpc"
},
"e2e_rust": {
"dependencies": ["evm_rpc"],
"dependencies": ["evm_rpc", "evm_rpc_fiduciary"],
"candid": "e2e/rust/e2e_rust.did",
"type": "rust",
"package": "e2e"
},
"e2e_motoko": {
"dependencies": ["evm_rpc"],
"dependencies": ["evm_rpc", "evm_rpc_fiduciary"],
"type": "motoko",
"main": "e2e/motoko/Main.mo"
}
Expand Down
268 changes: 148 additions & 120 deletions e2e/motoko/Main.mo
Original file line number Diff line number Diff line change
@@ -1,149 +1,177 @@
import EvmRpcCanister "canister:evm_rpc";
import EvmRpcFidicuaryCanister "canister:evm_rpc_fiduciary";

import Principal "mo:base/Principal";
import Evm "mo:evm";
import Blob "mo:base/Blob";
import Debug "mo:base/Debug";
import Cycles "mo:base/ExperimentalCycles";
import Principal "mo:base/Principal";
import Text "mo:base/Text";
import Blob "mo:base/Blob";
import Cycles "mo:base/ExperimentalCycles"
import Evm "mo:evm";

shared ({ caller = installer }) actor class Main() {
let mainnet = Evm.Rpc(
#Canister EvmRpcCanister,
#Service {
hostname = "cloudflare-eth.com";
network = ? #EthMainnet;
},
);

public shared ({ caller }) func test() : async () {
assert caller == installer;

let source = #Service {
hostname = "cloudflare-eth.com";
chainId = ?(1 : Nat64); // Ethereum mainnet
};
let json = "{\"jsonrpc\":\"2.0\",\"method\":\"eth_gasPrice\",\"params\":null,\"id\":1}";
let maxResponseBytes : Nat64 = 1000;
let canisterDetails = [
// (`canister module`, `debug name`, `nodes in subnet`, `expected cycles for JSON-RPC call`)
(EvmRpcCanister, "default", 13, 521_498_000),
(EvmRpcFidicuaryCanister, "fiduciary", 28, 1_123_226_461),
Comment on lines +17 to +18
Copy link
Contributor

Choose a reason for hiding this comment

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

It is not clear what these numbers mean (readers may be able to guess what 13 and 28 mean but not the other numbers).
How about either adding a comment or replacing the plain numbers with meaningfully named constants?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added a comment to clarify. There's also a line directly after this which shows the variable names for each tuple entry, although I see how it could be easy to miss.

];
for ((canister, name, nodesInSubnet, expectedCycles) in canisterDetails.vals()) {
Debug.print("Testing " # name # " canister...");

// `requestCost()`
let cyclesResult = await EvmRpcCanister.requestCost(source, json, maxResponseBytes);
let cycles = switch cyclesResult {
case (#Ok cycles) { cycles };
case (#Err err) {
Debug.trap("unexpected error for `request_cost`: " # (debug_show err));
let mainnet = Evm.Rpc(
#Canister canister,
#Service {
hostname = "cloudflare-eth.com";
network = ? #EthMainnet;
},
);

let source = #Service {
hostname = "cloudflare-eth.com";
chainId = ?(1 : Nat64); // Ethereum mainnet
};
};
let json = "{\"jsonrpc\":\"2.0\",\"method\":\"eth_gasPrice\",\"params\":null,\"id\":1}";
let maxResponseBytes : Nat64 = 1000;

// `request()` without cycles
let resultWithoutCycles = await EvmRpcCanister.request(source, json, maxResponseBytes);
assert switch resultWithoutCycles {
case (#Err(#ProviderError(#TooFewCycles { expected }))) expected == cycles;
case _ false;
};
// `requestCost()`
let cyclesResult = await canister.requestCost(source, json, maxResponseBytes);
let cycles = switch cyclesResult {
case (#Ok cycles) { cycles };
case (#Err err) {
Debug.trap("unexpected error for `request_cost`: " # (debug_show err));
};
};
if (cycles != expectedCycles) {
Debug.trap("unexpected number of cycles for " # name # " canister: " # debug_show cycles # " (expected " # debug_show expectedCycles # ")");
};

// `request()` without cycles
let resultWithoutCycles = await canister.request(source, json, maxResponseBytes);
assert switch resultWithoutCycles {
case (#Err(#ProviderError(#TooFewCycles { expected }))) expected == cycles;
case _ false;
};

// `request()` with cycles
let result = await mainnet.request("eth_gasPrice", #Array([]), 1000);
label validate {
switch result {
case (#ok(#Object fields)) {
for ((key, val) in fields.vals()) {
switch (key, val) {
case ("result", #String val) {
assert Text.startsWith(val, #text "0x");
break validate;
// `request()` with cycles
let result = await mainnet.request("eth_gasPrice", #Array([]), 1000);
label validate {
switch result {
case (#ok(#Object fields)) {
for ((key, val) in fields.vals()) {
switch (key, val) {
case ("result", #String val) {
assert Text.startsWith(val, #text "0x");
break validate;
};
case _ {};
};
case _ {};
};
};
case _ {};
};
case _ {};
Debug.trap(debug_show result);
};
Debug.trap(debug_show result);
};

// Candid-RPC methods
type RpcResult<T> = { #Ok : T; #Err : EvmRpcCanister.RpcError };
type MultiRpcResult<T> = {
#Consistent : RpcResult<T>;
#Inconsistent : [(EvmRpcCanister.RpcService, RpcResult<T>)];
};
// `request()` without sufficient cycles
let resultWithoutEnoughCycles = await canister.request(source, json, maxResponseBytes);
Cycles.add(cycles - 1);
assert switch resultWithoutEnoughCycles {
case (#Err(#ProviderError(#TooFewCycles { expected }))) expected == cycles;
case _ false;
};

func assertOk<T>(method : Text, result : MultiRpcResult<T>) {
switch result {
case (#Consistent(#Ok _)) {};
case (#Consistent(#Err err)) {
Debug.trap("received error for " # method # ": " # debug_show err);
};
case (#Inconsistent(results)) {
for ((service, result) in results.vals()) {
switch result {
case (#Ok(_)) {};
case (#Err(err)) {
Debug.trap("received error in inconsistent results for " # method # ": " # debug_show err);
// Candid-RPC methods
type RpcResult<T> = { #Ok : T; #Err : canister.RpcError };
type MultiRpcResult<T> = {
#Consistent : RpcResult<T>;
#Inconsistent : [(canister.RpcService, RpcResult<T>)];
};

func assertOk<T>(method : Text, result : MultiRpcResult<T>) {
switch result {
case (#Consistent(#Ok _)) {};
case (#Consistent(#Err err)) {
Debug.trap("received error for " # method # ": " # debug_show err);
};
case (#Inconsistent(results)) {
for ((service, result) in results.vals()) {
switch result {
case (#Ok(_)) {};
case (#Err(err)) {
Debug.trap("received error in inconsistent results for " # method # ": " # debug_show err);
};
};
};
};
};
};
};

let candidRpcCycles = 1_000_000_000_000;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is there so much code in red and identical-looking code in green below..?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is due to a change in indentation (modified the E2E test to cover both the standard and fiduciary canisters).

let ethMainnetSource = #EthMainnet(?[#Ankr, #BlockPi, #Cloudflare, #PublicNode]);
let candidRpcCycles = 1_000_000_000_000;
let ethMainnetSource = #EthMainnet(?[#Ankr, #BlockPi, #Cloudflare, #PublicNode]);

Cycles.add(candidRpcCycles);
assertOk(
"eth_getLogs",
await EvmRpcCanister.eth_getLogs(
ethMainnetSource,
{
addresses = ["0xdAC17F958D2ee523a2206206994597C13D831ec7"];
fromBlock = null;
toBlock = null;
topics = null;
},
),
);
Cycles.add(candidRpcCycles);
assertOk(
"eth_getBlockByNumber",
await EvmRpcCanister.eth_getBlockByNumber(ethMainnetSource, #Latest),
);
Cycles.add(candidRpcCycles);
assertOk(
"eth_getTransactionReceipt",
await EvmRpcCanister.eth_getTransactionReceipt(ethMainnetSource, "0xdd5d4b18923d7aae953c7996d791118102e889bea37b48a651157a4890e4746f"),
);
Cycles.add(candidRpcCycles);
assertOk(
"eth_getTransactionCount",
await EvmRpcCanister.eth_getTransactionCount(
ethMainnetSource,
{
address = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
block = #Latest;
},
),
);
Cycles.add(candidRpcCycles);
assertOk(
"eth_feeHistory",
await EvmRpcCanister.eth_feeHistory(
ethMainnetSource,
{
blockCount = 3;
newestBlock = #Latest;
rewardPercentiles = null;
},
),
);
Cycles.add(candidRpcCycles);
assertOk(
"eth_sendRawTransaction",
await EvmRpcCanister.eth_sendRawTransaction(
ethMainnetSource,
"0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83",
),
);
switch (await canister.eth_getBlockByNumber(ethMainnetSource, #Latest)) {
case (#Consistent(#Err(#ProviderError(#TooFewCycles _)))) {};
case result {
Debug.trap("received unexpected result: " # debug_show result);
};
};

Cycles.add(candidRpcCycles);
assertOk(
"eth_getLogs",
await canister.eth_getLogs(
ethMainnetSource,
{
addresses = ["0xdAC17F958D2ee523a2206206994597C13D831ec7"];
fromBlock = null;
toBlock = null;
topics = null;
},
),
);
Cycles.add(candidRpcCycles);
assertOk(
"eth_getBlockByNumber",
await canister.eth_getBlockByNumber(ethMainnetSource, #Latest),
);
Cycles.add(candidRpcCycles);
assertOk(
"eth_getTransactionReceipt",
await canister.eth_getTransactionReceipt(ethMainnetSource, "0xdd5d4b18923d7aae953c7996d791118102e889bea37b48a651157a4890e4746f"),
);
Cycles.add(candidRpcCycles);
assertOk(
"eth_getTransactionCount",
await canister.eth_getTransactionCount(
ethMainnetSource,
{
address = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
block = #Latest;
},
),
);
Cycles.add(candidRpcCycles);
assertOk(
"eth_feeHistory",
await canister.eth_feeHistory(
ethMainnetSource,
{
blockCount = 3;
newestBlock = #Latest;
rewardPercentiles = null;
},
),
);
Cycles.add(candidRpcCycles);
assertOk(
"eth_sendRawTransaction",
await canister.eth_sendRawTransaction(
ethMainnetSource,
"0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83",
),
);
};
};
};
5 changes: 4 additions & 1 deletion scripts/local
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ CANISTER_ID=evm_rpc
PRINCIPAL=$(dfx identity get-principal)


dfx deploy $CANISTER_ID --mode reinstall -y
dfx canister create --all
dfx deploy evm_rpc --argument "(record {nodesInSubnet = 13})" --mode reinstall -y
dfx deploy evm_rpc_fiduciary --argument "(record {nodesInSubnet = 28})" --mode reinstall -y

dfx canister call $CANISTER_ID authorize "(principal \"$PRINCIPAL\", variant { RegisterProvider })"

dfx canister call $CANISTER_ID registerProvider '(record {hostname = "cloudflare-eth.com"; credentialPath = "/v1/mainnet"; chainId = 1; cyclesPerCall = 1000; cyclesPerMessageByte = 100})'
Expand Down
Loading