From dac18184aa2e402c72bf88c0374d821f48b1a0c7 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Tue, 28 Apr 2026 09:26:23 +0200 Subject: [PATCH] test: add descriptive stub helpers to TestCanisterRuntime Replaces the DepositRuntimeExt trait with direct methods on TestCanisterRuntime, and adds stub helpers for all RPC and ledger calls: getTransaction, getSignaturesForAddress, getSlot, getBlock, sendTransaction, getSignatureStatuses, icrc1_transfer, icrc2_transfer_from. Each helper has a doc comment identifying the RPC call it stubs. Updates all test files to use these methods, eliminating verbose MultiRpcResult::Consistent(...) boilerplate and local type aliases. Co-Authored-By: Claude Sonnet 4.6 --- minter/src/consolidate/tests.rs | 58 +++++--------- minter/src/deposit/manual/tests.rs | 57 +++----------- minter/src/monitor/tests.rs | 112 +++++++++++--------------- minter/src/test_fixtures/runtime.rs | 118 +++++++++++++++++++++++++++- minter/src/withdraw/tests.rs | 67 ++++++---------- 5 files changed, 220 insertions(+), 192 deletions(-) diff --git a/minter/src/consolidate/tests.rs b/minter/src/consolidate/tests.rs index 28a56be9..319098e5 100644 --- a/minter/src/consolidate/tests.rs +++ b/minter/src/consolidate/tests.rs @@ -16,11 +16,7 @@ use crate::{ }, }; use assert_matches::assert_matches; -use sol_rpc_types::{ConfirmedBlock, MultiRpcResult, RpcError, Signature, Slot}; - -type SlotResult = MultiRpcResult; -type BlockResult = MultiRpcResult; -type SendTransactionResult = MultiRpcResult; +use sol_rpc_types::{MultiRpcResult, RpcError, Signature}; #[tokio::test] async fn should_return_early_if_no_deposits_to_consolidate() { @@ -55,11 +51,8 @@ async fn should_return_early_if_fetching_blockhash_fails() { add_funds_to_consolidate(&[(deposit_id(0), 1_000_000_000)]); - let error = SlotResult::Consistent(Err(RpcError::ValidationError("Error".to_string()))); let runtime = TestCanisterRuntime::new() - .add_stub_response(error.clone()) - .add_stub_response(error.clone()) - .add_stub_response(error); + .add_n_get_slot_error(RpcError::ValidationError("Error".to_string()), 3); consolidate_deposits(runtime).await; @@ -81,11 +74,9 @@ async fn should_submit_single_consolidation_request() { let runtime = TestCanisterRuntime::new() .with_increasing_time() // get_recent_slot_and_blockhash calls (get_recent_block internally calls getSlot then getBlock) - .add_stub_response(SlotResult::Consistent(Ok(slot))) - .add_stub_response(BlockResult::Consistent(Ok(confirmed_block()))) - .add_stub_response(SendTransactionResult::Consistent(Ok( - fee_payer_signature.into() - ))) + .add_get_slot_response(slot) + .add_get_block_response(confirmed_block()) + .add_send_transaction_response(fee_payer_signature) .add_signature(fee_payer_signature.into()); consolidate_deposits(runtime).await; @@ -118,10 +109,10 @@ async fn should_record_events_even_if_transaction_submission_fails() { let runtime = TestCanisterRuntime::new() .with_increasing_time() // get_recent_slot_and_blockhash calls - .add_stub_response(SlotResult::Consistent(Ok(slot))) - .add_stub_response(BlockResult::Consistent(Ok(confirmed_block()))) + .add_get_slot_response(slot) + .add_get_block_response(confirmed_block()) // Transaction submission fails - .add_stub_response(SendTransactionResult::Inconsistent(vec![])) + .add_stub_response(MultiRpcResult::::Inconsistent(vec![])) .add_signature(fee_payer_signature.into()); consolidate_deposits(runtime).await; @@ -158,14 +149,10 @@ async fn should_submit_multiple_consolidation_batches() { let mut runtime = TestCanisterRuntime::new() .with_increasing_time() // get_recent_slot_and_blockhash calls - .add_stub_response(SlotResult::Consistent(Ok(slot))) - .add_stub_response(BlockResult::Consistent(Ok(confirmed_block()))) - .add_stub_response(SendTransactionResult::Consistent(Ok( - fee_payer_signature_1.into() - ))) - .add_stub_response(SendTransactionResult::Consistent(Ok( - fee_payer_signature_2.into() - ))); + .add_get_slot_response(slot) + .add_get_block_response(confirmed_block()) + .add_send_transaction_response(fee_payer_signature_1) + .add_send_transaction_response(fee_payer_signature_2); for i in 0..(2 + NUM_DEPOSITS) { runtime = runtime.add_signature(signature(i).into()); @@ -229,11 +216,9 @@ async fn should_consolidate_multiple_deposits_to_same_account_in_single_transfer let slot = 100; let runtime = TestCanisterRuntime::new() .with_increasing_time() - .add_stub_response(SlotResult::Consistent(Ok(slot))) - .add_stub_response(BlockResult::Consistent(Ok(confirmed_block()))) - .add_stub_response(SendTransactionResult::Consistent(Ok( - fee_payer_signature.into() - ))) + .add_get_slot_response(slot) + .add_get_block_response(confirmed_block()) + .add_send_transaction_response(fee_payer_signature) .add_signature(fee_payer_signature.into()); consolidate_deposits(runtime).await; @@ -271,11 +256,10 @@ async fn should_reschedule_until_all_deposits_consolidated() { // Round 1: processes MAX_CONCURRENT_RPC_CALLS batches, 1 deposit remains → reschedule let mut runtime = TestCanisterRuntime::new() .with_increasing_time() - .add_stub_response(SlotResult::Consistent(Ok(slot))) - .add_stub_response(BlockResult::Consistent(Ok(confirmed_block()))); + .add_get_slot_response(slot) + .add_get_block_response(confirmed_block()); for i in 0..MAX_CONCURRENT_RPC_CALLS { - runtime = - runtime.add_stub_response(SendTransactionResult::Consistent(Ok(signature(i).into()))); + runtime = runtime.add_send_transaction_response(signature(i)); } for i in 0..(MAX_CONCURRENT_RPC_CALLS + num_deposits) { runtime = runtime.add_signature(signature(i).into()); @@ -293,9 +277,9 @@ async fn should_reschedule_until_all_deposits_consolidated() { let last_sig = signature(num_deposits); let runtime = TestCanisterRuntime::new() .with_increasing_time() - .add_stub_response(SlotResult::Consistent(Ok(slot))) - .add_stub_response(BlockResult::Consistent(Ok(confirmed_block()))) - .add_stub_response(SendTransactionResult::Consistent(Ok(last_sig.into()))) + .add_get_slot_response(slot) + .add_get_block_response(confirmed_block()) + .add_send_transaction_response(last_sig) .add_signature(last_sig.into()); consolidate_deposits(runtime.clone()).await; diff --git a/minter/src/deposit/manual/tests.rs b/minter/src/deposit/manual/tests.rs index 997523f4..305aaf9c 100644 --- a/minter/src/deposit/manual/tests.rs +++ b/minter/src/deposit/manual/tests.rs @@ -25,14 +25,8 @@ use candid_parser::Principal; use cksol_types::{DepositStatus, InsufficientCyclesError, ProcessDepositError}; use cksol_types_internal::InitArgs; use ic_canister_runtime::IcError; -use icrc_ledger_types::icrc1::{ - account::Account, - transfer::{BlockIndex, TransferError}, -}; -use sol_rpc_types::{EncodedConfirmedTransactionWithStatusMeta, Lamport, MultiRpcResult}; - -type GetTransactionResult = MultiRpcResult>; -type MintResult = Result; +use icrc_ledger_types::icrc1::{account::Account, transfer::TransferError}; +use sol_rpc_types::{EncodedConfirmedTransactionWithStatusMeta, Lamport}; mod process_deposit_tests { use super::*; @@ -89,7 +83,7 @@ mod process_deposit_tests { init_state(); init_schnorr_master_key(); - let runtime = rejected_runtime().add_get_transaction_not_found_response(); + let runtime = rejected_runtime().add_get_transaction_not_found(); let result = process_deposit( runtime, @@ -158,7 +152,7 @@ mod process_deposit_tests { init_schnorr_master_key(); let runtime = runtime(legacy_deposit_transaction()) - .add_mint_response(Err(TransferError::TemporarilyUnavailable)); + .add_icrc1_transfer_response(Err(TransferError::TemporarilyUnavailable)); let result = process_deposit( runtime, @@ -181,7 +175,7 @@ mod process_deposit_tests { // First call: makes JSON-RPC call and attempts to mint let runtime = runtime(legacy_deposit_transaction()) - .add_mint_response(Err(TransferError::TemporarilyUnavailable)); + .add_icrc1_transfer_response(Err(TransferError::TemporarilyUnavailable)); let result = process_deposit( runtime, DEPOSITOR_ACCOUNT, @@ -194,7 +188,7 @@ mod process_deposit_tests { // additional JSON-RPC calls let runtime = TestCanisterRuntime::new() .with_increasing_time() - .add_mint_response(Ok(BLOCK_INDEX.into())); + .add_icrc1_transfer_response(Ok(BLOCK_INDEX.into())); let result = process_deposit( runtime, DEPOSITOR_ACCOUNT, @@ -228,7 +222,7 @@ mod process_deposit_tests { ] { reset_events(); - let runtime = runtime(transaction).add_mint_response(Ok(block_index.into())); + let runtime = runtime(transaction).add_icrc1_transfer_response(Ok(block_index.into())); let result = process_deposit(runtime, DEPOSITOR_ACCOUNT, signature).await; @@ -270,8 +264,8 @@ mod process_deposit_tests { init_schnorr_master_key(); // Successful mint - let runtime = - runtime(legacy_deposit_transaction()).add_mint_response(Ok(BLOCK_INDEX.into())); + let runtime = runtime(legacy_deposit_transaction()) + .add_icrc1_transfer_response(Ok(BLOCK_INDEX.into())); let result = process_deposit( runtime, DEPOSITOR_ACCOUNT, @@ -370,7 +364,7 @@ mod process_deposit_tests { for i in 0..3 { let runtime = runtime(deposit_transaction_to_multiple_accounts()) - .add_mint_response(Ok(BLOCK_INDEXES[i].into())); + .add_icrc1_transfer_response(Ok(BLOCK_INDEXES[i].into())); let result = process_deposit( runtime, ACCOUNTS[i], @@ -435,35 +429,4 @@ mod process_deposit_tests { .add_msg_cycles_refunded(GET_TRANSACTION_CYCLES - RPC_COST) .add_msg_cycles_accept(RPC_COST + consolidation_fee) } - - trait DepositRuntimeExt: Sized { - fn add_get_transaction_response( - self, - tx: impl TryInto, - ) -> Self; - fn add_get_transaction_not_found_response(self) -> Self; - fn add_mint_response(self, result: MintResult) -> Self; - } - - impl DepositRuntimeExt for TestCanisterRuntime { - fn add_get_transaction_response( - self, - response: impl TryInto, - ) -> Self { - self.add_stub_response(GetTransactionResult::Consistent(Ok(Some( - response - .try_into() - .ok() - .expect("failed to convert transaction"), - )))) - } - - fn add_get_transaction_not_found_response(self) -> Self { - self.add_stub_response(GetTransactionResult::Consistent(Ok(None))) - } - - fn add_mint_response(self, result: MintResult) -> Self { - self.add_stub_response(result) - } - } } diff --git a/minter/src/monitor/tests.rs b/minter/src/monitor/tests.rs index b737f605..10ac5634 100644 --- a/minter/src/monitor/tests.rs +++ b/minter/src/monitor/tests.rs @@ -12,15 +12,10 @@ use crate::{ }, }; use sol_rpc_types::{ - ConfirmedBlock, MultiRpcResult, RpcError, Signature, Slot, TransactionConfirmationStatus, - TransactionError, TransactionStatus, + MultiRpcResult, RpcError, Signature, Slot, TransactionConfirmationStatus, TransactionError, + TransactionStatus, }; -type SlotResult = MultiRpcResult; -type BlockResult = MultiRpcResult; -type SendTransactionResult = MultiRpcResult; -type SignatureStatusesResult = MultiRpcResult>>; - const CURRENT_SLOT: Slot = 408_807_102; const RECENT_SLOT: Slot = CURRENT_SLOT - 10; const EXPIRED_SLOT: Slot = CURRENT_SLOT - MAX_BLOCKHASH_AGE - 1; @@ -62,11 +57,8 @@ mod finalization { let events_before = EventsAssert::from_recorded(); - let error = SlotResult::Consistent(Err(RpcError::ValidationError("Error".to_string()))); let runtime = TestCanisterRuntime::new() - .add_stub_response(error.clone()) - .add_stub_response(error.clone()) - .add_stub_response(error); + .add_n_get_slot_error(RpcError::ValidationError("Error".to_string()), 3); finalize_transactions(runtime).await; @@ -86,12 +78,14 @@ mod finalization { // Round 1: finalizes MAX_CONCURRENT_RPC_CALLS batches, 1 transaction unchecked → reschedule let mut runtime = TestCanisterRuntime::new() .with_increasing_time() - .add_stub_response(SlotResult::Consistent(Ok(CURRENT_SLOT))) - .add_stub_response(BlockResult::Consistent(Ok(confirmed_block()))); + .add_get_slot_response(CURRENT_SLOT) + .add_get_block_response(confirmed_block()); for _ in 0..MAX_CONCURRENT_RPC_CALLS { - runtime = runtime.add_stub_response(SignatureStatusesResult::Consistent(Ok( - vec![Some(finalized_status()); MAX_SIGNATURES_PER_STATUS_CHECK], - ))); + runtime = + runtime.add_get_signature_statuses_response(vec![ + Some(finalized_status()); + MAX_SIGNATURES_PER_STATUS_CHECK + ]); } finalize_transactions(runtime.clone()).await; @@ -102,11 +96,9 @@ mod finalization { // Round 2: finalizes the remaining 1 transaction → no reschedule let runtime = TestCanisterRuntime::new() .with_increasing_time() - .add_stub_response(SlotResult::Consistent(Ok(CURRENT_SLOT))) - .add_stub_response(BlockResult::Consistent(Ok(confirmed_block()))) - .add_stub_response(SignatureStatusesResult::Consistent(Ok(vec![Some( - finalized_status(), - )]))); + .add_get_slot_response(CURRENT_SLOT) + .add_get_block_response(confirmed_block()) + .add_get_signature_statuses_response(vec![Some(finalized_status())]); finalize_transactions(runtime.clone()).await; @@ -122,11 +114,9 @@ mod finalization { let runtime = TestCanisterRuntime::new() .with_increasing_time() - .add_stub_response(SlotResult::Consistent(Ok(CURRENT_SLOT))) - .add_stub_response(BlockResult::Consistent(Ok(confirmed_block()))) - .add_stub_response(SignatureStatusesResult::Consistent(Ok(vec![Some( - finalized_status(), - )]))); + .add_get_slot_response(CURRENT_SLOT) + .add_get_block_response(confirmed_block()) + .add_get_signature_statuses_response(vec![Some(finalized_status())]); finalize_transactions(runtime).await; @@ -162,11 +152,9 @@ mod finalization { let runtime = TestCanisterRuntime::new() .with_increasing_time() - .add_stub_response(SlotResult::Consistent(Ok(CURRENT_SLOT))) - .add_stub_response(BlockResult::Consistent(Ok(confirmed_block()))) - .add_stub_response(SignatureStatusesResult::Consistent(Ok(vec![ - status.clone(), - ]))); + .add_get_slot_response(CURRENT_SLOT) + .add_get_block_response(confirmed_block()) + .add_get_signature_statuses_response(vec![status.clone()]); let _ = status; // suppress unused warning @@ -188,16 +176,14 @@ mod finalization { let runtime = TestCanisterRuntime::new() .with_increasing_time() - .add_stub_response(SlotResult::Consistent(Ok(CURRENT_SLOT))) - .add_stub_response(BlockResult::Consistent(Ok(confirmed_block()))) - .add_stub_response(SignatureStatusesResult::Consistent(Ok(vec![Some( - TransactionStatus { - slot: RECENT_SLOT, - status: Err(TransactionError::InsufficientFundsForFee), - err: Some(TransactionError::InsufficientFundsForFee), - confirmation_status: Some(TransactionConfirmationStatus::Finalized), - }, - )]))); + .add_get_slot_response(CURRENT_SLOT) + .add_get_block_response(confirmed_block()) + .add_get_signature_statuses_response(vec![Some(TransactionStatus { + slot: RECENT_SLOT, + status: Err(TransactionError::InsufficientFundsForFee), + err: Some(TransactionError::InsufficientFundsForFee), + confirmation_status: Some(TransactionConfirmationStatus::Finalized), + })]); finalize_transactions(runtime).await; @@ -224,13 +210,13 @@ mod finalization { let runtime = TestCanisterRuntime::new() .with_increasing_time() - .add_stub_response(SlotResult::Consistent(Ok(CURRENT_SLOT))) - .add_stub_response(BlockResult::Consistent(Ok(confirmed_block()))) - .add_stub_response(SignatureStatusesResult::Consistent(Ok(vec![ + .add_get_slot_response(CURRENT_SLOT) + .add_get_block_response(confirmed_block()) + .add_get_signature_statuses_response(vec![ Some(finalized_status()), None, Some(finalized_status()), - ]))); + ]); // sig_b is not_found but RECENT_SLOT is not expired, so no resubmission. finalize_transactions(runtime).await; @@ -323,9 +309,9 @@ mod resubmission { let resubmit_runtime = TestCanisterRuntime::new() .with_increasing_time() - .add_stub_response(SlotResult::Consistent(Ok(RESUBMISSION_SLOT))) - .add_stub_response(BlockResult::Consistent(Ok(confirmed_block()))) - .add_stub_response(SendTransactionResult::Consistent(Ok(new_signature.into()))) + .add_get_slot_response(RESUBMISSION_SLOT) + .add_get_block_response(confirmed_block()) + .add_send_transaction_response(new_signature) .add_signature(new_signature.into()); resubmit_transactions(resubmit_runtime).await; @@ -356,11 +342,9 @@ mod resubmission { let finalize_runtime = TestCanisterRuntime::new() .with_increasing_time() - .add_stub_response(SlotResult::Consistent(Ok(CURRENT_SLOT))) - .add_stub_response(BlockResult::Consistent(Ok(confirmed_block()))) - .add_stub_response(SignatureStatusesResult::Consistent(Err( - RpcError::ValidationError("Error".to_string()), - ))); + .add_get_slot_response(CURRENT_SLOT) + .add_get_block_response(confirmed_block()) + .add_get_signature_statuses_error(RpcError::ValidationError("Error".to_string())); finalize_transactions(finalize_runtime).await; @@ -383,9 +367,9 @@ mod resubmission { let resubmit_runtime = TestCanisterRuntime::new() .with_increasing_time() - .add_stub_response(SlotResult::Consistent(Ok(RESUBMISSION_SLOT))) - .add_stub_response(BlockResult::Consistent(Ok(confirmed_block()))) - .add_stub_response(SendTransactionResult::Inconsistent(vec![])) + .add_get_slot_response(RESUBMISSION_SLOT) + .add_get_block_response(confirmed_block()) + .add_stub_response(MultiRpcResult::::Inconsistent(vec![])) .add_signature(new_signature.into()); resubmit_transactions(resubmit_runtime).await; @@ -414,13 +398,11 @@ mod resubmission { // Round 1: resubmits MAX_CONCURRENT_RPC_CALLS transactions, 1 remain → reschedule let mut runtime = TestCanisterRuntime::new() .with_increasing_time() - .add_stub_response(SlotResult::Consistent(Ok(RESUBMISSION_SLOT))) - .add_stub_response(BlockResult::Consistent(Ok(confirmed_block()))); + .add_get_slot_response(RESUBMISSION_SLOT) + .add_get_block_response(confirmed_block()); for i in 0..MAX_CONCURRENT_RPC_CALLS { runtime = runtime - .add_stub_response(SendTransactionResult::Consistent(Ok( - signature(0xA0 + i).into() - ))) + .add_send_transaction_response(signature(0xA0 + i)) .add_signature(signature(0xA0 + i).into()); } @@ -438,13 +420,11 @@ mod resubmission { // Round 2: resubmits remaining transaction → no reschedule let mut runtime = TestCanisterRuntime::new() .with_increasing_time() - .add_stub_response(SlotResult::Consistent(Ok(RESUBMISSION_SLOT))) - .add_stub_response(BlockResult::Consistent(Ok(confirmed_block()))); + .add_get_slot_response(RESUBMISSION_SLOT) + .add_get_block_response(confirmed_block()); for i in 0..(num_transactions - MAX_CONCURRENT_RPC_CALLS) { runtime = runtime - .add_stub_response(SendTransactionResult::Consistent(Ok( - signature(0xB0 + i).into() - ))) + .add_send_transaction_response(signature(0xB0 + i)) .add_signature(signature(0xB0 + i).into()); } diff --git a/minter/src/test_fixtures/runtime.rs b/minter/src/test_fixtures/runtime.rs index 9cd8e3f5..ef6d27fa 100644 --- a/minter/src/test_fixtures/runtime.rs +++ b/minter/src/test_fixtures/runtime.rs @@ -1,8 +1,15 @@ use super::{signer::MockSchnorrSigner, stubs::Stubs}; use crate::{runtime::CanisterRuntime, signer::SchnorrSigner}; -use candid::{CandidType, Principal}; +use candid::{CandidType, Nat, Principal}; use ic_canister_runtime::{IcError, Runtime, StubRuntime}; use ic_cdk_management_canister::{SchnorrPublicKeyArgs, SchnorrPublicKeyResult, SignCallError}; +use icrc_ledger_types::icrc1::transfer::{BlockIndex, TransferError}; +use icrc_ledger_types::icrc2::transfer_from::TransferFromError; +use sol_rpc_types::{ + ConfirmedBlock, ConfirmedTransactionStatusWithSignature, + EncodedConfirmedTransactionWithStatusMeta, MultiRpcResult, RpcError, + Signature as SolRpcSignature, Slot, TransactionStatus, +}; use std::{ future::Future, sync::{Arc, Mutex}, @@ -90,6 +97,115 @@ impl TestCanisterRuntime { self } + // ── getTransaction ──────────────────────────────────────────────────────── + + /// Stubs the next `getTransaction` JSON-RPC call to return the given transaction. + pub fn add_get_transaction_response( + self, + tx: impl TryInto, + ) -> Self { + self.add_stub_response(MultiRpcResult::< + Option, + >::Consistent(Ok(Some( + tx.try_into().ok().expect("failed to convert transaction"), + )))) + } + + /// Stubs the next `getTransaction` JSON-RPC call to return `None` (transaction not found). + pub fn add_get_transaction_not_found(self) -> Self { + self.add_stub_response(MultiRpcResult::< + Option, + >::Consistent(Ok(None))) + } + + /// Stubs `n` consecutive `getTransaction` calls to return `None`. + pub fn add_n_get_transaction_not_found(self, n: usize) -> Self { + (0..n).fold(self, |rt, _| rt.add_get_transaction_not_found()) + } + + // ── getSignaturesForAddress ─────────────────────────────────────────────── + + /// Stubs the next `getSignaturesForAddress` JSON-RPC call to return the given signatures. + pub fn add_get_signatures_for_address_response( + self, + sigs: Vec, + ) -> Self { + self.add_stub_response( + MultiRpcResult::>::Consistent(Ok(sigs)), + ) + } + + /// Stubs the next `getSignaturesForAddress` JSON-RPC call to return an error. + pub fn add_get_signatures_for_address_error(self, err: RpcError) -> Self { + self.add_stub_response( + MultiRpcResult::>::Consistent(Err(err)), + ) + } + + // ── getSlot ─────────────────────────────────────────────────────────────── + + /// Stubs the next `getSlot` JSON-RPC call to return the given slot. + pub fn add_get_slot_response(self, slot: Slot) -> Self { + self.add_stub_response(MultiRpcResult::::Consistent(Ok(slot))) + } + + /// Stubs the next `getSlot` JSON-RPC call to return an error. + pub fn add_get_slot_error(self, err: RpcError) -> Self { + self.add_stub_response(MultiRpcResult::::Consistent(Err(err))) + } + + /// Stubs `n` consecutive `getSlot` JSON-RPC calls to return the given error. + pub fn add_n_get_slot_error(self, err: RpcError, n: usize) -> Self { + (0..n).fold(self, |rt, _| rt.add_get_slot_error(err.clone())) + } + + // ── getBlock ────────────────────────────────────────────────────────────── + + /// Stubs the next `getBlock` JSON-RPC call to return the given block. + pub fn add_get_block_response(self, block: ConfirmedBlock) -> Self { + self.add_stub_response(MultiRpcResult::::Consistent(Ok(block))) + } + + // ── sendTransaction ─────────────────────────────────────────────────────── + + /// Stubs the next `sendTransaction` JSON-RPC call to return the given signature. + pub fn add_send_transaction_response(self, sig: impl Into) -> Self { + self.add_stub_response(MultiRpcResult::::Consistent( + Ok(sig.into()), + )) + } + + // ── getSignatureStatuses ────────────────────────────────────────────────── + + /// Stubs the next `getSignatureStatuses` JSON-RPC call to return the given statuses. + pub fn add_get_signature_statuses_response( + self, + statuses: Vec>, + ) -> Self { + self.add_stub_response( + MultiRpcResult::>>::Consistent(Ok(statuses)), + ) + } + + /// Stubs the next `getSignatureStatuses` JSON-RPC call to return an error. + pub fn add_get_signature_statuses_error(self, err: RpcError) -> Self { + self.add_stub_response( + MultiRpcResult::>>::Consistent(Err(err)), + ) + } + + // ── Ledger ──────────────────────────────────────────────────────────────── + + /// Stubs the next `icrc1_transfer` ledger call (used to mint ckSOL). + pub fn add_icrc1_transfer_response(self, result: Result) -> Self { + self.add_stub_response(result) + } + + /// Stubs the next `icrc2_transfer_from` ledger call (used to burn ckSOL when withdrawing). + pub fn add_icrc2_transfer_from_response(self, result: Result) -> Self { + self.add_stub_response(result) + } + #[cfg(any(test, not(feature = "canbench-rs")))] pub(crate) fn set_timer_call_count(&self) -> usize { *self.set_timer_call_count.lock().unwrap() diff --git a/minter/src/withdraw/tests.rs b/minter/src/withdraw/tests.rs index 87b2e8af..88899879 100644 --- a/minter/src/withdraw/tests.rs +++ b/minter/src/withdraw/tests.rs @@ -21,7 +21,7 @@ use ic_canister_runtime::IcError; use ic_cdk::call::CallRejected; use ic_cdk_management_canister::SignCallError; use icrc_ledger_types::{icrc1::account::Account, icrc2::transfer_from::TransferFromError}; -use sol_rpc_types::{MultiRpcResult, RpcError, Slot}; +use sol_rpc_types::RpcError; use solana_signature::Signature; const VALID_ADDRESS: &str = "E4MpwNnMWs2XtW5gVrxZvyS7fMq31QD5HvbxmwP45Tz3"; @@ -54,9 +54,8 @@ async fn should_return_error_if_calling_ledger_fails() { async fn should_return_error_if_ledger_unavailable() { init_state(); - let runtime = TestCanisterRuntime::new().add_stub_response(Err::( - TransferFromError::TemporarilyUnavailable, - )); + let runtime = TestCanisterRuntime::new() + .add_icrc2_transfer_from_response(Err(TransferFromError::TemporarilyUnavailable)); let result = withdraw( &runtime, @@ -78,7 +77,7 @@ async fn should_return_error_if_ledger_unavailable() { async fn should_return_error_if_insufficient_allowance() { init_state(); - let runtime = TestCanisterRuntime::new().add_stub_response(Err::( + let runtime = TestCanisterRuntime::new().add_icrc2_transfer_from_response(Err( TransferFromError::InsufficientAllowance { allowance: Nat::from(123u64), }, @@ -102,7 +101,7 @@ async fn should_return_error_if_insufficient_allowance() { async fn should_return_error_if_insufficient_funds() { init_state(); - let runtime = TestCanisterRuntime::new().add_stub_response(Err::( + let runtime = TestCanisterRuntime::new().add_icrc2_transfer_from_response(Err( TransferFromError::InsufficientFunds { balance: Nat::from(123u64), }, @@ -126,7 +125,7 @@ async fn should_return_error_if_insufficient_funds() { async fn should_return_temporarily_unavailable_on_generic_error() { init_state(); - let runtime = TestCanisterRuntime::new().add_stub_response(Err::( + let runtime = TestCanisterRuntime::new().add_icrc2_transfer_from_response(Err( TransferFromError::GenericError { error_code: Nat::from(123u64), message: "msg".to_string(), @@ -154,7 +153,7 @@ async fn should_return_ok_if_burn_succeeds() { init_state(); let runtime = TestCanisterRuntime::new() - .add_stub_response(Ok::(Nat::from(123u64))) + .add_icrc2_transfer_from_response(Ok(Nat::from(123u64))) .with_increasing_time(); let result = withdraw( @@ -236,10 +235,6 @@ async fn should_return_error_if_already_processing() { mod process_pending_withdrawals_tests { use super::*; - type GetSlotResult = MultiRpcResult; - type GetBlockResult = MultiRpcResult; - type SendTransactionResult = MultiRpcResult; - #[tokio::test] async fn should_do_nothing_if_no_pending_withdrawals() { init_state(); @@ -313,10 +308,10 @@ mod process_pending_withdrawals_tests { let events_before = EventsAssert::from_recorded(); let runtime = TestCanisterRuntime::new() - .add_stub_response(GetSlotResult::Consistent(Ok(slot))) - .add_stub_response(GetBlockResult::Consistent(Ok(confirmed_block()))) + .add_get_slot_response(slot) + .add_get_block_response(confirmed_block()) .add_signature(tx_signature.into()) - .add_stub_response(SendTransactionResult::Consistent(Ok(tx_signature.into()))) + .add_send_transaction_response(tx_signature) .with_increasing_time(); process_pending_withdrawals(runtime).await; @@ -343,10 +338,10 @@ mod process_pending_withdrawals_tests { let runtime = TestCanisterRuntime::new() .with_increasing_time() - .add_stub_response(GetSlotResult::Consistent(Ok(slot))) - .add_stub_response(GetBlockResult::Consistent(Ok(confirmed_block()))) + .add_get_slot_response(slot) + .add_get_block_response(confirmed_block()) .add_signature(tx_signature.into()) - .add_stub_response(SendTransactionResult::Consistent(Ok(tx_signature.into()))); + .add_send_transaction_response(tx_signature); process_pending_withdrawals(runtime).await; @@ -364,15 +359,7 @@ mod process_pending_withdrawals_tests { let runtime = TestCanisterRuntime::new() .with_increasing_time() - .add_stub_response(GetSlotResult::Consistent(Err(RpcError::ValidationError( - "slot unavailable".to_string(), - )))) - .add_stub_response(GetSlotResult::Consistent(Err(RpcError::ValidationError( - "slot unavailable".to_string(), - )))) - .add_stub_response(GetSlotResult::Consistent(Err(RpcError::ValidationError( - "slot unavailable".to_string(), - )))); + .add_n_get_slot_error(RpcError::ValidationError("slot unavailable".to_string()), 3); process_pending_withdrawals(runtime).await; @@ -407,8 +394,8 @@ mod process_pending_withdrawals_tests { let runtime = TestCanisterRuntime::new() .with_increasing_time() - .add_stub_response(GetSlotResult::Consistent(Ok(slot))) - .add_stub_response(GetBlockResult::Consistent(Ok(confirmed_block()))) + .add_get_slot_response(slot) + .add_get_block_response(confirmed_block()) .add_schnorr_signing_error(SignCallError::CallFailed( CallRejected::with_rejection(4, "signing service unavailable".to_string()).into(), )); @@ -450,12 +437,12 @@ mod process_pending_withdrawals_tests { let runtime = TestCanisterRuntime::new() .with_increasing_time() - .add_stub_response(GetSlotResult::Consistent(Ok(slot))) - .add_stub_response(GetBlockResult::Consistent(Ok(confirmed_block()))) + .add_get_slot_response(slot) + .add_get_block_response(confirmed_block()) .add_signature(signature(1).into()) - .add_stub_response(SendTransactionResult::Consistent(Ok(signature(1).into()))) + .add_send_transaction_response(signature(1)) .add_signature(signature(2).into()) - .add_stub_response(SendTransactionResult::Consistent(Ok(signature(2).into()))); + .add_send_transaction_response(signature(2)); process_pending_withdrawals(runtime).await; @@ -485,14 +472,12 @@ mod process_pending_withdrawals_tests { // Round 1: processes MAX_CONCURRENT_RPC_CALLS batches, 1 request remains → reschedule let mut runtime = TestCanisterRuntime::new() .with_increasing_time() - .add_stub_response(GetSlotResult::Consistent(Ok(slot))) - .add_stub_response(GetBlockResult::Consistent(Ok(confirmed_block()))); + .add_get_slot_response(slot) + .add_get_block_response(confirmed_block()); for i in 0..MAX_CONCURRENT_RPC_CALLS { runtime = runtime .add_signature(signature(i + 1).into()) - .add_stub_response(SendTransactionResult::Consistent(Ok( - signature(i + 1).into() - ))); + .add_send_transaction_response(signature(i + 1)); } process_pending_withdrawals(runtime.clone()).await; @@ -507,10 +492,10 @@ mod process_pending_withdrawals_tests { let last_sig = signature(num_requests); let runtime = TestCanisterRuntime::new() .with_increasing_time() - .add_stub_response(GetSlotResult::Consistent(Ok(slot))) - .add_stub_response(GetBlockResult::Consistent(Ok(confirmed_block()))) + .add_get_slot_response(slot) + .add_get_block_response(confirmed_block()) .add_signature(last_sig.into()) - .add_stub_response(SendTransactionResult::Consistent(Ok(last_sig.into()))); + .add_send_transaction_response(last_sig); process_pending_withdrawals(runtime.clone()).await;