Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5f853e6
test(cketh): build the live balance-scan harness on the shared fixture
gregorydemay Aug 13, 2026
5c6bda6
refactor(cketh): thread the install sender through the shared installers
gregorydemay Aug 13, 2026
c2053c6
fix(cketh): restore ECDSA_KEY_NAME as a single shared constant
gregorydemay Aug 13, 2026
f683e8c
fix(cketh): correct the documented cause of the live-mode crash
gregorydemay Aug 13, 2026
7e84561
refactor(cketh): rename EvmRpcBackend to EthereumBackend
gregorydemay Aug 13, 2026
04d586b
docs(cketh): explain why Mocked implies Finalized/LAST_SCRAPED_BLOCK_…
gregorydemay Aug 13, 2026
206bb5c
docs(cketh): scope CkEthCanisters' doc to how each fixture actually u…
gregorydemay Aug 13, 2026
d14bc70
docs(cketh): tighten the cycles-headroom comment on the live harness
gregorydemay Aug 13, 2026
33d34d1
refactor(cketh): build the live balance-scan harness on CkEthSetup it…
gregorydemay Aug 13, 2026
651d03a
refactor(cketh): collapse build_mocked/build_live into one build sequ…
gregorydemay Aug 13, 2026
ad8ce46
fix(cketh): restore the cycles-headroom rationale as a named constant
gregorydemay Aug 13, 2026
5d619c3
docs(cketh): lead the ledger conditional with the reason, not the obs…
gregorydemay Aug 13, 2026
a114ffd
refactor(cketh): reuse CkEthSetup::add_ckerc20_token in register_supp…
gregorydemay Aug 13, 2026
501c22f
refactor(cketh): apply the rate-limiting config to both new_env modes
gregorydemay Aug 13, 2026
d000a55
refactor(cketh): name new_env's modes with an EnvMode enum
gregorydemay Aug 13, 2026
78aa809
fix(cketh): assert with_env and with_live_mode are never combined
gregorydemay Aug 13, 2026
986fd38
docs(cketh): fix CkEthCanisters' doc referencing the removed build_mo…
gregorydemay Aug 13, 2026
336bc52
refactor(cketh): derive the fixture's live mode from EthereumBackend
gregorydemay Aug 13, 2026
7ebe033
refactor(cketh): hold the anvil node itself in EthereumBackend::Anvil
gregorydemay Aug 13, 2026
75f3d8e
refactor(cketh): inline pocket_ic_builder into its one caller, new_env
gregorydemay Aug 13, 2026
0a4ea72
refactor(cketh): let the fixture build the PocketIC instance it needs
gregorydemay Aug 13, 2026
8a44389
refactor(cketh): create and install the canisters the same way for ev…
gregorydemay Aug 13, 2026
a87674b
refactor(cketh): build the live balance-scan harness on CkErc20Setup
gregorydemay Aug 13, 2026
96d3b0b
Merge origin/ic_DEFI-2262_8_live-scan-setup
gregorydemay Aug 13, 2026
7a5682b
fix(cketh): build the live fixture on a non-live instance, go live last
gregorydemay Aug 14, 2026
8aff162
refactor(cketh): delete CkEthSetupBuilder, fold it into CkEthSetup::new
gregorydemay Aug 14, 2026
3cb7de4
Merge master into ic_DEFI-2262_8_live-scan-setup
gregorydemay Aug 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 127 additions & 50 deletions rs/ethereum/cketh/test_utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::flow::{
use crate::mock::JsonRpcMethod;
use assert_matches::assert_matches;
use candid::{Decode, Encode, Nat, Principal};
use evm_rpc_types::{InstallArgs, OverrideProvider, RegexSubstitution};
use ic_base_types::PrincipalId;
use ic_cketh_minter::endpoints::events::{Event, EventPayload, GetEventsResult};
use ic_cketh_minter::endpoints::{
Expand Down Expand Up @@ -144,42 +145,18 @@ impl PocketIcHttpQuery for &CkEthSetup {

impl CkEthSetup {
pub fn new(env: Arc<PocketIc>) -> Self {
// Create minter canister first to match canister ID and Ethereum address hardcoded in tests.
let minter_id = env.create_canister();
env.add_cycles(minter_id, u128::MAX);
let ledger_id = env.create_canister();
env.add_cycles(ledger_id, u128::MAX);
let evm_rpc_id = env.create_canister();
env.add_cycles(evm_rpc_id, u128::MAX);

env.install_canister(
ledger_id,
ledger_wasm(),
Encode!(&LedgerArgument::Init(
LedgerInitArgsBuilder::with_symbol_and_name("ckETH", "ckETH")
.with_minting_account(minter_id)
.with_transfer_fee(CKETH_TRANSFER_FEE)
.with_max_memo_length(80)
.with_decimals(18)
.with_feature_flags(ic_icrc1_ledger::FeatureFlags {
icrc2: true,
icrc152: false
})
.build(),
))
.unwrap(),
None,
);
install_evm_rpc(&env, evm_rpc_id);
let minter_id = install_minter(&env, ledger_id, minter_id, evm_rpc_id);
let canisters = create_cketh_canisters(&env);
install_ledger(&env, &canisters);
install_evm_rpc(&env, &canisters, &EthereumBackend::Mocked);
install_minter(&env, &canisters, &EthereumBackend::Mocked);

let caller = PrincipalId::new_user_test_id(DEFAULT_PRINCIPAL_ID);
Self {
env,
caller,
ledger_id,
minter_id,
evm_rpc_id,
ledger_id: canisters.ledger_id,
minter_id: canisters.minter_id,
evm_rpc_id: canisters.evm_rpc_id,
support_subaccount: false,
}
}
Expand Down Expand Up @@ -752,15 +729,21 @@ pub fn format_ethereum_address_to_eip_55(address: &str) -> String {
}

pub fn new_pocket_ic() -> PocketIc {
PocketIcBuilder::new()
.with_fiduciary_subnet()
pocket_ic_builder()
.with_icp_config(IcpConfig {
canister_execution_rate_limiting: Some(IcpConfigFlag::Disabled),
..Default::default()
})
.build()
}

/// A [`PocketIcBuilder`] with the fiduciary subnet every ckETH fixture needs for the secp256k1
/// `key_1` used by the minter; callers add anything further (e.g. an NNS subnet for
/// [`PocketIc::make_live`]) before `build()`.
fn pocket_ic_builder() -> PocketIcBuilder {
Comment thread
gregorydemay marked this conversation as resolved.
Outdated
PocketIcBuilder::new().with_fiduciary_subnet()
}

fn ledger_wasm() -> Vec<u8> {
let path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
.parent()
Expand Down Expand Up @@ -791,36 +774,130 @@ fn evm_rpc_wasm() -> Vec<u8> {
)
}

fn install_minter(
env: &PocketIc,
ledger_id: Principal,
/// The minter, its ckETH ledger and the EVM RPC canister it calls out to, plus the sender to
/// install/upgrade them as. [`CkEthSetup::new`] creates all 3 (via [`create_cketh_canisters`],
/// minter first) and installs all 3; the live harness in [`live_scan`] creates all 3 itself
/// (ledger first, under its own non-anonymous controller) and installs only the minter and the EVM
/// RPC canister, assembling this same struct to hand to [`install_minter`]/[`install_evm_rpc`].
struct CkEthCanisters {
minter_id: Principal,
ledger_id: Principal,
evm_rpc_id: Principal,
) -> Principal {
controller: Option<Principal>,
}

fn create_cketh_canisters(env: &PocketIc) -> CkEthCanisters {
// Create minter canister first to match canister ID and Ethereum address hardcoded in tests.
let minter_id = env.create_canister();
env.add_cycles(minter_id, u128::MAX);
let ledger_id = env.create_canister();
env.add_cycles(ledger_id, u128::MAX);
let evm_rpc_id = env.create_canister();
env.add_cycles(evm_rpc_id, u128::MAX);
CkEthCanisters {
minter_id,
ledger_id,
evm_rpc_id,
controller: None,
}
}

fn install_ledger(env: &PocketIc, canisters: &CkEthCanisters) {
env.install_canister(
canisters.ledger_id,
ledger_wasm(),
Encode!(&LedgerArgument::Init(
LedgerInitArgsBuilder::with_symbol_and_name("ckETH", "ckETH")
.with_minting_account(canisters.minter_id)
.with_transfer_fee(CKETH_TRANSFER_FEE)
.with_max_memo_length(80)
.with_decimals(18)
.with_feature_flags(ic_icrc1_ledger::FeatureFlags {
icrc2: true,
icrc152: false
})
.build(),
))
.unwrap(),
canisters.controller,
);
}

/// The Ethereum chain under test: which JSON-RPC endpoint the EVM RPC canister's outcalls reach,
/// and the corresponding minter init/upgrade assumptions about that chain's state (the block
/// height to track, and where its log-scraping cursor starts).
enum EthereumBackend<'a> {
/// Canned JSON-RPC mocks pinned to a historical mainnet snapshot.
Mocked,
/// A live anvil node reached over HTTP at `url`: a fresh chain with no finalized blocks yet.
Anvil(&'a str),
}

impl EthereumBackend<'_> {
fn install_args(&self) -> InstallArgs {
InstallArgs {
override_provider: match self {
EthereumBackend::Mocked => None,
EthereumBackend::Anvil(url) => Some(OverrideProvider {
override_url: Some(RegexSubstitution {
pattern: ".*".into(),
replacement: url.to_string(),
}),
}),
},
..Default::default()
}
}

fn ethereum_block_height(&self) -> CandidBlockTag {
match self {
// The mocked responses replay a historical mainnet snapshot, long since finalized.
EthereumBackend::Mocked => CandidBlockTag::Finalized,
// A fresh anvil chain has no finalized blocks, so track its "latest" head instead.
EthereumBackend::Anvil(_) => CandidBlockTag::Latest,
}
}

fn last_scraped_block_number(&self) -> Nat {
match self {
// The block the mocked JSON-RPC responses are canned to scrape logs from onward.
EthereumBackend::Mocked => LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL.into(),
EthereumBackend::Anvil(_) => 0_u8.into(),
}
}
Comment thread
gregorydemay marked this conversation as resolved.
}

/// PocketIC's fiduciary subnet holds the secp256k1 test key under this name, the key the minter
/// derives deposit addresses from.
const ECDSA_KEY_NAME: &str = "key_1";

fn install_minter(env: &PocketIc, canisters: &CkEthCanisters, backend: &EthereumBackend) {
let args = MinterInitArgs {
ecdsa_key_name: "key_1".parse().unwrap(),
ecdsa_key_name: ECDSA_KEY_NAME.to_string(),
ethereum_network: EthereumNetwork::Mainnet,
ledger_id,
ledger_id: canisters.ledger_id,
next_transaction_nonce: 0_u8.into(),
ethereum_block_height: CandidBlockTag::Finalized,
ethereum_block_height: backend.ethereum_block_height(),
ethereum_contract_address: Some(ETH_HELPER_CONTRACT_ADDRESS.to_string()),
minimum_withdrawal_amount: CKETH_MINIMUM_WITHDRAWAL_AMOUNT.into(),
last_scraped_block_number: LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL.into(),
evm_rpc_id: Some(evm_rpc_id),
last_scraped_block_number: backend.last_scraped_block_number(),
evm_rpc_id: Some(canisters.evm_rpc_id),
};
let minter_arg = MinterArg::InitArg(args);
env.install_canister(
minter_id,
canisters.minter_id,
minter_wasm(),
Encode!(&minter_arg).unwrap(),
None,
Encode!(&MinterArg::InitArg(args)).unwrap(),
canisters.controller,
);
minter_id
}

fn install_evm_rpc(env: &PocketIc, evm_rpc_id: Principal) {
let args = evm_rpc_types::InstallArgs::default();
env.install_canister(evm_rpc_id, evm_rpc_wasm(), Encode!(&args).unwrap(), None);
fn install_evm_rpc(env: &PocketIc, canisters: &CkEthCanisters, backend: &EthereumBackend) {
env.install_canister(
canisters.evm_rpc_id,
evm_rpc_wasm(),
Encode!(&backend.install_args()).unwrap(),
canisters.controller,
);
}

fn fail_as_timed_out(env: &PocketIc, request: &CanisterHttpRequest) {
Expand Down
Loading
Loading