From 578f8fab54c51d054b153455a1b5f830afb17729 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Fri, 20 Mar 2026 16:58:14 +0100 Subject: [PATCH 1/3] test: add deterministic deposit consolidation integration tests --- integration_tests/src/fixtures.rs | 70 ++++++++++++++++++++++ integration_tests/src/lib.rs | 15 +++++ integration_tests/tests/tests.rs | 99 ++++++++++++++++++++++++++----- 3 files changed, 169 insertions(+), 15 deletions(-) diff --git a/integration_tests/src/fixtures.rs b/integration_tests/src/fixtures.rs index b14671dc..8e7e346e 100644 --- a/integration_tests/src/fixtures.rs +++ b/integration_tests/src/fixtures.rs @@ -3,6 +3,7 @@ use async_trait::async_trait; use cksol_types::{GetDepositAddressArgs, Signature, UpdateBalanceArgs}; use ic_pocket_canister_runtime::{ ExecuteHttpOutcallMocks, JsonRpcRequestMatcher, JsonRpcResponse, MockHttpOutcalls, + MockHttpOutcallsBuilder, }; use icrc_ledger_types::{ icrc::generic_value::{ICRC3Value, Value}, @@ -156,3 +157,72 @@ pub fn get_memo(block: ICRC3Value) -> Vec { let memo_blob = memo.clone().as_blob().expect("memo should be a blob"); memo_blob.into_vec() } + +/// Creates HTTP mocks for `getTransaction` RPC calls. +pub fn get_transaction_http_mocks(response: impl Fn() -> JsonRpcResponse) -> MockHttpOutcalls { + MockHttpOutcallsBuilder::new() + .given(get_deposit_transaction_request().with_id(0)) + .respond_with(response().with_id(0)) + .given(get_deposit_transaction_request().with_id(1)) + .respond_with(response().with_id(1)) + .given(get_deposit_transaction_request().with_id(2)) + .respond_with(response().with_id(2)) + .given(get_deposit_transaction_request().with_id(3)) + .respond_with(response().with_id(3)) + .build() +} + +/// JSON-RPC request matcher for `getSlot`. +pub fn get_slot_request() -> JsonRpcRequestMatcher { + JsonRpcRequestMatcher::with_method("getSlot") +} + +/// JSON-RPC response for `getSlot`. +pub fn get_slot_response(slot: u64) -> JsonRpcResponse { + JsonRpcResponse::from(json!({ + "jsonrpc": "2.0", + "result": slot, + "id": 1 + })) +} + +/// JSON-RPC request matcher for `getBlock`. +pub fn get_block_request(slot: u64) -> JsonRpcRequestMatcher { + JsonRpcRequestMatcher::with_method("getBlock").with_params(json!([ + slot, + { + "transactionDetails": "none", + "rewards": false, + "maxSupportedTransactionVersion": 0 + } + ])) +} + +/// JSON-RPC response for `getBlock`. +pub fn get_block_response(blockhash: &str) -> JsonRpcResponse { + JsonRpcResponse::from(json!({ + "jsonrpc": "2.0", + "result": { + "blockhash": blockhash, + "previousBlockhash": "CzBVNFJkh7WkQDfJUiDjLc7kPrJd8kR2yiCvwBUhSe7Y", + "parentSlot": 449819444, + "blockTime": 1700000000_i64, + "blockHeight": 449819444 + }, + "id": 1 + })) +} + +/// JSON-RPC request matcher for `sendTransaction`. +pub fn send_transaction_request() -> JsonRpcRequestMatcher { + JsonRpcRequestMatcher::with_method("sendTransaction") +} + +/// JSON-RPC response for `sendTransaction`. +pub fn send_transaction_response(signature: &str) -> JsonRpcResponse { + JsonRpcResponse::from(json!({ + "jsonrpc": "2.0", + "result": signature, + "id": 1 + })) +} diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index e7533575..33e67ce7 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -20,6 +20,9 @@ use icrc_ledger_types::{ icrc3::blocks::{GetBlocksRequest, GetBlocksResult, ICRC3GenericBlock}, }; use num_traits::cast::ToPrimitive; +pub use pocket_ic::common::rest::{ + CanisterHttpReply, CanisterHttpRequest, CanisterHttpResponse, MockCanisterHttpResponse, +}; use pocket_ic::{PocketIcBuilder, RejectResponse, nonblocking::PocketIc}; use serde::de::DeserializeOwned; use sol_rpc_client::SolRpcClient; @@ -297,6 +300,18 @@ impl Setup { self.env.as_ref().unwrap().advance_time(duration).await } + pub async fn execute_http_mocks(&self, mut mocks: impl ExecuteHttpOutcallMocks) { + const MAX_ITERATIONS: usize = 20; + let env = self.env.as_ref().unwrap(); + + for _ in 0..MAX_ITERATIONS { + self.tick().await; + self.advance_time(Duration::from_nanos(1)).await; + + mocks.execute_http_outcall_mocks(env).await; + } + } + pub async fn drop(self) { let mut setup = self; if let Some(env) = setup.env.take() { diff --git a/integration_tests/tests/tests.rs b/integration_tests/tests/tests.rs index e6686867..9ddded2a 100644 --- a/integration_tests/tests/tests.rs +++ b/integration_tests/tests/tests.rs @@ -6,8 +6,9 @@ use cksol_int_tests::{ fixtures::{ DEFAULT_CALLER_ACCOUNT, DEFAULT_CALLER_DEPOSIT_ADDRESS, DEPOSIT_AMOUNT, EXPECTED_MINT_AMOUNT, SharedMockHttpOutcalls, default_update_balance_args, - deposit_transaction_signature, get_deposit_transaction_request, - get_deposit_transaction_response, + deposit_transaction_signature, get_block_request, get_block_response, + get_deposit_transaction_response, get_slot_request, get_slot_response, + get_transaction_http_mocks, send_transaction_request, send_transaction_response, }, }; use cksol_types::{ @@ -19,6 +20,7 @@ use ic_pocket_canister_runtime::{JsonRpcResponse, MockHttpOutcalls, MockHttpOutc use icrc_ledger_types::icrc1::account::Subaccount; use serde_json::json; use sol_rpc_types::{CommitmentLevel, ConsensusStrategy, GetTransactionEncoding, RpcConfig}; +use std::time::Duration; use tokio::join; mod get_deposit_address_tests { @@ -767,19 +769,6 @@ mod update_balance_tests { setup.drop().await; } - fn get_transaction_http_mocks(response: impl Fn() -> JsonRpcResponse) -> MockHttpOutcalls { - MockHttpOutcallsBuilder::new() - .given(get_deposit_transaction_request().with_id(0)) - .respond_with(response().with_id(0)) - .given(get_deposit_transaction_request().with_id(1)) - .respond_with(response().with_id(1)) - .given(get_deposit_transaction_request().with_id(2)) - .respond_with(response().with_id(2)) - .given(get_deposit_transaction_request().with_id(3)) - .respond_with(response().with_id(3)) - .build() - } - async fn get_transaction_cycles_cost(setup: &Setup) -> u128 { setup .sol_rpc() @@ -840,3 +829,83 @@ mod anonymous_caller_tests { setup.drop().await; } } + +mod consolidation_tests { + use super::*; + + const DEPOSIT_CONSOLIDATION_DELAY: Duration = Duration::from_secs(600); + + #[tokio::test] + async fn should_consolidate_deposits_after_timer() { + let setup = SetupBuilder::new().with_proxy_canister().build().await; + + let result = setup + .minter() + .with_http_mocks(get_transaction_http_mocks(get_deposit_transaction_response)) + .update_balance(default_update_balance_args()) + .await; + assert_matches!(result, Ok(DepositStatus::Minted { .. })); + + // Advance time past the consolidation delay to trigger the timer + setup.advance_time(DEPOSIT_CONSOLIDATION_DELAY).await; + setup + .execute_http_mocks(http_mocks_for_deposit_consolidation()) + .await; + + // Verify consolidation events were recorded + let events_after = setup.minter().get_all_events().await; + assert!( + events_after + .iter() + .any(|e| matches!(e.payload, EventType::ConsolidatedDeposits { .. })), + "Expected ConsolidatedDeposits event. Events: {events_after:?}" + ); + assert!( + events_after + .iter() + .any(|e| matches!(e.payload, EventType::SubmittedTransaction { .. })), + "Expected SubmittedTransaction event. Events: {events_after:?}" + ); + + // Verify the consolidated deposits match the deposit amount + for event in &events_after { + if let EventType::ConsolidatedDeposits { deposits } = &event.payload { + let total: Lamport = deposits.iter().map(|(_, amount)| amount).sum(); + assert_eq!( + total, DEPOSIT_AMOUNT, + "Consolidated amount should match the deposit amount" + ); + } + } + + setup.drop().await; + } + + // Returns the required HTTP outcall mocks for executing the deposit concolidation task + fn http_mocks_for_deposit_consolidation() -> MockHttpOutcalls { + const SLOT: u64 = 100_000_000; + const BLOCKHASH: &str = "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZAMdL4VZHirAn"; + const TX_SIGNATURE: &str = "5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW"; + + let mut mocks = MockHttpOutcallsBuilder::new(); + // getSlot requests (IDs 4-7) + for id in 4..8 { + mocks = mocks + .given(get_slot_request().with_id(id)) + .respond_with(get_slot_response(SLOT).with_id(id)); + } + // getBlock requests (IDs 8-11) + for id in 8..12 { + mocks = mocks + .given(get_block_request(SLOT).with_id(id)) + .respond_with(get_block_response(BLOCKHASH).with_id(id)); + } + // sendTransaction requests (IDs 12-15) + for id in 12..16 { + mocks = mocks + .given(send_transaction_request().with_id(id)) + .respond_with(send_transaction_response(TX_SIGNATURE).with_id(id)); + } + mocks.build() + } +} From 2a9dd49d3b67d7b43678e5a0f53c6cb22d2a090f Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Mon, 23 Mar 2026 13:07:56 +0100 Subject: [PATCH 2/3] Fix mocks --- integration_tests/tests/tests.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/integration_tests/tests/tests.rs b/integration_tests/tests/tests.rs index 9ddded2a..10045902 100644 --- a/integration_tests/tests/tests.rs +++ b/integration_tests/tests/tests.rs @@ -888,20 +888,26 @@ mod consolidation_tests { const TX_SIGNATURE: &str = "5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW"; let mut mocks = MockHttpOutcallsBuilder::new(); - // getSlot requests (IDs 4-7) + // getSlot requests for estimate_recent_blockhash (IDs 4-7) for id in 4..8 { mocks = mocks .given(get_slot_request().with_id(id)) .respond_with(get_slot_response(SLOT).with_id(id)); } - // getBlock requests (IDs 8-11) + // getBlock requests for estimate_recent_blockhash (IDs 8-11) for id in 8..12 { mocks = mocks .given(get_block_request(SLOT).with_id(id)) .respond_with(get_block_response(BLOCKHASH).with_id(id)); } - // sendTransaction requests (IDs 12-15) + // getSlot requests for get_slot (IDs 12-15) for id in 12..16 { + mocks = mocks + .given(get_slot_request().with_id(id)) + .respond_with(get_slot_response(SLOT).with_id(id)); + } + // sendTransaction requests (IDs 16-19) + for id in 16..20 { mocks = mocks .given(send_transaction_request().with_id(id)) .respond_with(send_transaction_response(TX_SIGNATURE).with_id(id)); From c30b102f5f5e763ffcd0a09dacfdff33ed50fdb8 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Mon, 23 Mar 2026 14:23:10 +0100 Subject: [PATCH 3/3] Fix typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- integration_tests/tests/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_tests/tests/tests.rs b/integration_tests/tests/tests.rs index 10045902..55b5dbc4 100644 --- a/integration_tests/tests/tests.rs +++ b/integration_tests/tests/tests.rs @@ -881,7 +881,7 @@ mod consolidation_tests { setup.drop().await; } - // Returns the required HTTP outcall mocks for executing the deposit concolidation task + // Returns the required HTTP outcall mocks for executing the deposit consolidation task fn http_mocks_for_deposit_consolidation() -> MockHttpOutcalls { const SLOT: u64 = 100_000_000; const BLOCKHASH: &str = "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZAMdL4VZHirAn";