diff --git a/integration_tests/src/fixtures.rs b/integration_tests/src/fixtures.rs index b3bf6ba2..fd2cd147 100644 --- a/integration_tests/src/fixtures.rs +++ b/integration_tests/src/fixtures.rs @@ -206,8 +206,8 @@ impl MockBuilder { ) } - /// Mock for `getSignaturesForAddress` returning the given list of signature objects. - pub fn get_signatures_for_address(self, signatures: Vec) -> Self { + /// Mock for `getSignaturesForAddress` returning the given list of `(signature, slot)` pairs. + pub fn get_signatures_for_address(self, signatures: Vec<(&str, u64)>) -> Self { self.expect( get_signatures_for_address_request(), get_signatures_for_address_response(signatures), @@ -380,10 +380,23 @@ fn get_signatures_for_address_request() -> JsonRpcRequestMatcher { JsonRpcRequestMatcher::with_method("getSignaturesForAddress") } -fn get_signatures_for_address_response(signatures: Vec) -> JsonRpcResponse { +fn get_signatures_for_address_response(signatures: Vec<(&str, u64)>) -> JsonRpcResponse { + let entries: Vec = signatures + .into_iter() + .map(|(signature, slot)| { + json!({ + "signature": signature, + "slot": slot, + "err": null, + "memo": null, + "blockTime": null, + "confirmationStatus": null + }) + }) + .collect(); JsonRpcResponse::from(json!({ "jsonrpc": "2.0", - "result": signatures, + "result": entries, "id": 1 })) } diff --git a/integration_tests/tests/tests.rs b/integration_tests/tests/tests.rs index 93c1f850..a8226d8f 100644 --- a/integration_tests/tests/tests.rs +++ b/integration_tests/tests/tests.rs @@ -5,9 +5,8 @@ use cksol_int_tests::{ CkSolMinter, Setup, SetupBuilder, fixtures::{ DEFAULT_CALLER_ACCOUNT, DEFAULT_CALLER_DEPOSIT_ADDRESS, DEPOSIT_AMOUNT, - EXPECTED_MINT_AMOUNT, MockBuilder, SharedMockHttpOutcalls, - default_get_deposit_address_args, default_process_deposit_args, - default_update_balance_args, deposit_transaction_signature, + EXPECTED_MINT_AMOUNT, MockBuilder, SharedMockHttpOutcalls, default_process_deposit_args, + default_update_balance_args, deposit_signature_status_json, deposit_transaction_signature, }, }; use cksol_types::{ @@ -17,7 +16,7 @@ use cksol_types::{ }; use cksol_types_internal::{ UpgradeArgs, - event::{EventType, TransactionPurpose}, + event::{DepositSource, EventType, TransactionPurpose}, log::Priority, }; use ic_pocket_canister_runtime::{JsonRpcResponse, MockHttpOutcalls}; @@ -32,6 +31,7 @@ const FINALIZE_TRANSACTIONS_DELAY: Duration = Duration::from_mins(2); const RESUBMIT_TRANSACTIONS_DELAY: Duration = Duration::from_mins(3); const DEPOSIT_CONSOLIDATION_DELAY: Duration = Duration::from_mins(10); const POLL_MONITORED_ADDRESSES_DELAY: Duration = Duration::from_mins(1); +const PROCESS_PENDING_SIGNATURES_DELAY: Duration = Duration::from_secs(5); /// Deposits funds into the minter via `process_deposit`, consolidates them, /// and finalizes the consolidation so the minter's internal balance is credited. @@ -1295,47 +1295,44 @@ mod automated_deposit_flow_tests { use super::*; #[tokio::test] - async fn should_poll_monitored_address() { + async fn should_accept_automatic_deposit() { let setup = SetupBuilder::new().build().await; let minter = setup.minter(); - // Initialize the minter public key and register the account for monitoring. - assert_eq!( - minter - .get_deposit_address(default_get_deposit_address_args()) - .await - .to_string(), - DEFAULT_CALLER_DEPOSIT_ADDRESS - ); + // Register the account for monitoring. minter .update_balance(default_update_balance_args()) .await .expect("update_balance should succeed"); - minter.assert_that_events().await.satisfy(|events| { - check!(events.iter().any(|e| { - e == &EventType::StartedMonitoringAccount { - account: DEFAULT_CALLER_ACCOUNT, - } - })); - }); - - // Advance time: the minter should poll getSignaturesForAddress once, then remove the account. + // Poll phase: minter calls getSignaturesForAddress and discovers the deposit. setup.advance_time(POLL_MONITORED_ADDRESSES_DELAY).await; setup .execute_http_mocks( MockBuilder::with_start_id(0) - .get_signatures_for_address(vec![]) + .get_signatures_for_address(vec![deposit_signature_status_json()]) + .build(), + ) + .await; + + // Process phase: minter calls getTransaction and accepts the deposit. + setup.advance_time(PROCESS_PENDING_SIGNATURES_DELAY).await; + setup + .execute_http_mocks( + MockBuilder::with_start_id(4) + .get_deposit_transaction() .build(), ) .await; minter.assert_that_events().await.satisfy(|events| { - check!(events.iter().any(|e| { - e == &EventType::StoppedMonitoringAccount { - account: DEFAULT_CALLER_ACCOUNT, + check!(events.iter().any(|e| matches!( + e, + EventType::AcceptedDeposit { + source: DepositSource::Automatic, + .. } - })); + ))); }); setup.drop().await; diff --git a/minter/src/consolidate/tests.rs b/minter/src/consolidate/tests.rs index cb52de76..6fd249a9 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/automatic/mod.rs b/minter/src/deposit/automatic/mod.rs index 32ee80fd..24e4faef 100644 --- a/minter/src/deposit/automatic/mod.rs +++ b/minter/src/deposit/automatic/mod.rs @@ -1,12 +1,15 @@ use crate::{ address::{account_address, lazy_get_schnorr_master_key}, constants::MAX_CONCURRENT_RPC_CALLS, + deposit::fetch_and_validate_deposit, guard::TimerGuard, rpc::get_signatures_for_address, runtime::CanisterRuntime, state::{ - SchnorrPublicKey, TaskType, audit::process_event, event::EventType, mutate_state, - read_state, + SchnorrPublicKey, TaskType, + audit::process_event, + event::{DepositId, DepositSource, EventType}, + mutate_state, read_state, }, }; use canlog::log; @@ -38,6 +41,9 @@ pub const POLL_MONITORED_ADDRESSES_DELAY: Duration = Duration::from_mins(1); /// Maximum number of `getTransaction` calls to make per polled account. pub const MAX_TRANSACTIONS_PER_ACCOUNT: usize = 10; +/// How often the minter processes the pending-signatures queue. +pub const PROCESS_PENDING_SIGNATURES_DELAY: Duration = Duration::from_secs(5); + /// Registers the given account for automated deposit monitoring. /// /// Returns `Ok(())` if the account was registered (or was already being monitored). @@ -160,6 +166,114 @@ async fn poll_account( }); } +/// Processes pending deposit signatures using a round-robin, capacity-filling strategy. +/// +/// Each pass takes one signature per account (fair round-robin by `Account` key order). If +/// capacity remains after a full pass, another pass begins — so up to `MAX_CONCURRENT_RPC_CALLS` +/// signatures are dispatched in parallel each call. For each signature, calls `getTransaction` +/// and emits [`EventType::AcceptedDeposit`] with `source: Automatic` if valid. Invalid or +/// already-processed signatures are silently discarded. Reschedules itself immediately if +/// signatures remain after the capacity is exhausted. +pub async fn process_pending_signatures(runtime: R) { + let _guard = match TimerGuard::new(TaskType::ProcessPendingSignatures) { + Ok(guard) => guard, + Err(_) => return, + }; + + // Round-robin across accounts, refilling capacity with additional passes until exhausted. + let to_process: Vec<(Account, Signature)> = PENDING_SIGNATURES.with(|pending| { + let mut pending = pending.borrow_mut(); + + // Interleave queues round-robin by iterating column-by-column: round 0 yields + // one item from each account, then round 1, and so on. `take` stops early + // without advancing past the capacity limit. + let max_round = pending.values().map(VecDeque::len).max().unwrap_or(0); + let to_process: Vec<(Account, Signature)> = (0..max_round) + .flat_map(|round| { + pending.iter().filter_map(move |(&account, queue)| { + queue.get(round).map(|&sig| (account, sig)) + }) + }) + .take(MAX_CONCURRENT_RPC_CALLS) + .collect(); + + // Drain the taken items from each account's queue. + let mut counts: BTreeMap = BTreeMap::new(); + for &(account, _) in &to_process { + *counts.entry(account).or_default() += 1; + } + for (account, count) in counts { + if let Some(queue) = pending.get_mut(&account) { + queue.drain(..count); + } + } + pending.retain(|_, queue| !queue.is_empty()); + to_process + }); + + if to_process.is_empty() { + return; + } + + let more_to_process = PENDING_SIGNATURES.with(|p| !p.borrow().is_empty()); + let reschedule = scopeguard::guard(runtime.clone(), |runtime| { + runtime.set_timer(Duration::ZERO, process_pending_signatures); + }); + + let fee = read_state(|s| s.automated_deposit_fee()); + + futures::future::join_all( + to_process + .into_iter() + .map(|(account, signature)| process_signature(&runtime, account, signature, fee)), + ) + .await; + + if !more_to_process { + scopeguard::ScopeGuard::into_inner(reschedule); + } +} + +async fn process_signature( + runtime: &R, + account: Account, + signature: Signature, + fee: u64, +) { + // Skip signatures that were already accepted or minted (e.g. via manual deposit). + let deposit_id = DepositId { account, signature }; + if read_state(|s| s.deposit_status(&deposit_id)).is_some() { + return; + } + + match fetch_and_validate_deposit(runtime, account, signature, fee).await { + Ok((deposit_id, deposit_amount, amount_to_mint)) => { + mutate_state(|state| { + process_event( + state, + EventType::AcceptedDeposit { + deposit_id, + deposit_amount, + amount_to_mint, + source: DepositSource::Automatic, + }, + runtime, + ) + }); + log!( + Priority::Info, + "Accepted automatic deposit {deposit_id:?}: {deposit_amount} lamports deposited, minting {amount_to_mint} lamports" + ); + } + Err(e) => { + log!( + Priority::Info, + "Discarding automatic deposit signature {signature}: {e}" + ); + } + } +} + #[cfg(any(test, feature = "canbench-rs"))] pub fn pending_signatures_for(account: &Account) -> Vec { PENDING_SIGNATURES.with(|p| { diff --git a/minter/src/deposit/automatic/tests.rs b/minter/src/deposit/automatic/tests.rs index a9b2024f..429fbb87 100644 --- a/minter/src/deposit/automatic/tests.rs +++ b/minter/src/deposit/automatic/tests.rs @@ -1,15 +1,23 @@ use super::*; use crate::{ constants::MAX_CONCURRENT_RPC_CALLS, - state::{event::EventType, read_state}, + state::{ + event::{DepositId, DepositSource, EventType}, + read_state, + }, test_fixtures::{ - EventsAssert, account, events::start_monitoring_account, init_schnorr_master_key, - init_state, runtime::TestCanisterRuntime, signature, + AUTOMATED_DEPOSIT_FEE, EventsAssert, account, + deposit::{ + DEPOSIT_AMOUNT, DEPOSITOR_ACCOUNT, legacy_deposit_transaction, + legacy_deposit_transaction_signature, + }, + events::start_monitoring_account, + init_schnorr_master_key, init_state, + runtime::TestCanisterRuntime, + signature, }, }; -use sol_rpc_types::{ConfirmedTransactionStatusWithSignature, MultiRpcResult, TransactionError}; - -type SignaturesResult = MultiRpcResult>; +use sol_rpc_types::{ConfirmedTransactionStatusWithSignature, RpcError, TransactionError}; fn confirmed_tx(signature: Signature) -> ConfirmedTransactionStatusWithSignature { ConfirmedTransactionStatusWithSignature { @@ -43,6 +51,13 @@ fn start_monitoring_max_number_of_accounts() { } } +/// Pushes signatures into the pending queue for the given account. +fn queue_pending_signatures(account: Account, sigs: impl IntoIterator) { + PENDING_SIGNATURES.with(|p| { + p.borrow_mut().entry(account).or_default().extend(sigs); + }); +} + mod update_balance { use super::*; @@ -127,7 +142,7 @@ mod poll_monitored_addresses { // Round 1: polls MAX_CONCURRENT_RPC_CALLS accounts, 1 remains → reschedule. let mut runtime = TestCanisterRuntime::new().with_increasing_time(); for _ in 0..MAX_CONCURRENT_RPC_CALLS { - runtime = runtime.add_stub_response(SignaturesResult::Consistent(Ok(vec![]))); + runtime = runtime.add_get_signatures_for_address_response(vec![]); } poll_monitored_addresses(runtime.clone()).await; @@ -137,7 +152,7 @@ mod poll_monitored_addresses { // Round 2: polls the remaining 1 account → no reschedule, queue empty. let runtime = TestCanisterRuntime::new() .with_increasing_time() - .add_stub_response(SignaturesResult::Consistent(Ok(vec![]))); + .add_get_signatures_for_address_response(vec![]); poll_monitored_addresses(runtime.clone()).await; assert_eq!(monitored_accounts_count(), 0); @@ -165,10 +180,7 @@ mod poll_monitored_addresses { let s2 = signature(2); let runtime = TestCanisterRuntime::new() .with_increasing_time() - .add_stub_response(SignaturesResult::Consistent(Ok(vec![ - confirmed_tx(s1), - confirmed_tx(s2), - ]))); + .add_get_signatures_for_address_response(vec![confirmed_tx(s1), confirmed_tx(s2)]); poll_monitored_addresses(runtime).await; @@ -187,10 +199,7 @@ mod poll_monitored_addresses { let s_fail = signature(2); let runtime = TestCanisterRuntime::new() .with_increasing_time() - .add_stub_response(SignaturesResult::Consistent(Ok(vec![ - confirmed_tx(s_ok), - failed_tx(s_fail), - ]))); + .add_get_signatures_for_address_response(vec![confirmed_tx(s_ok), failed_tx(s_fail)]); poll_monitored_addresses(runtime).await; @@ -207,9 +216,9 @@ mod poll_monitored_addresses { let runtime = TestCanisterRuntime::new() .with_increasing_time() - .add_stub_response(SignaturesResult::Consistent(Err( - sol_rpc_types::RpcError::ValidationError("RPC error".to_string()), - ))); + .add_get_signatures_for_address_error(RpcError::ValidationError( + "RPC error".to_string(), + )); poll_monitored_addresses(runtime).await; @@ -221,3 +230,152 @@ mod poll_monitored_addresses { init_schnorr_master_key(); } } + +mod process_pending_signatures_tests { + use super::*; + + #[tokio::test] + async fn should_accept_valid_deposit() { + setup(); + reset_pending_signatures(); + + let sig = legacy_deposit_transaction_signature(); + queue_pending_signatures(DEPOSITOR_ACCOUNT, [sig]); + + let runtime = TestCanisterRuntime::new() + .with_increasing_time() + .add_get_transaction_response(legacy_deposit_transaction()); + + process_pending_signatures(runtime).await; + + assert!( + pending_signatures_for(&DEPOSITOR_ACCOUNT).is_empty(), + "signature should have been consumed" + ); + + let deposit_id = DepositId { + account: DEPOSITOR_ACCOUNT, + signature: sig, + }; + EventsAssert::from_recorded() + .expect_event_eq(EventType::AcceptedDeposit { + deposit_id, + deposit_amount: DEPOSIT_AMOUNT, + amount_to_mint: DEPOSIT_AMOUNT - AUTOMATED_DEPOSIT_FEE, + source: DepositSource::Automatic, + }) + .assert_no_more_events(); + } + + #[tokio::test] + async fn should_discard_invalid_deposit() { + setup(); + reset_pending_signatures(); + + let sig = legacy_deposit_transaction_signature(); + queue_pending_signatures(DEPOSITOR_ACCOUNT, [sig]); + + // getTransaction returns None (tx not found) + let runtime = TestCanisterRuntime::new() + .with_increasing_time() + .add_get_transaction_not_found(); + + process_pending_signatures(runtime).await; + + assert!(pending_signatures_for(&DEPOSITOR_ACCOUNT).is_empty()); + EventsAssert::assert_no_events_recorded(); + } + + #[tokio::test] + async fn should_skip_already_processed_deposit() { + setup(); + reset_pending_signatures(); + + let sig = legacy_deposit_transaction_signature(); + + // Pre-populate state as if this deposit was already accepted manually. + crate::test_fixtures::events::accept_deposit( + DepositId { + account: DEPOSITOR_ACCOUNT, + signature: sig, + }, + DEPOSIT_AMOUNT, + ); + + queue_pending_signatures(DEPOSITOR_ACCOUNT, [sig]); + + // No getTransaction stub — if called it would panic. + let runtime = TestCanisterRuntime::new().with_increasing_time(); + + process_pending_signatures(runtime).await; + + use crate::test_fixtures::MANUAL_DEPOSIT_FEE; + // No new AcceptedDeposit (Automatic) event should have been emitted. + EventsAssert::from_recorded() + .expect_event_eq(EventType::AcceptedDeposit { + deposit_id: DepositId { + account: DEPOSITOR_ACCOUNT, + signature: sig, + }, + deposit_amount: DEPOSIT_AMOUNT, + amount_to_mint: DEPOSIT_AMOUNT - MANUAL_DEPOSIT_FEE, + source: DepositSource::Manual, + }) + .assert_no_more_events(); + } + + #[tokio::test] + async fn should_process_multiple_signatures_per_account_when_capacity_allows() { + setup(); + reset_pending_signatures(); + + // Two accounts with two signatures each — all four fit within MAX_CONCURRENT_RPC_CALLS. + let acc1 = DEPOSITOR_ACCOUNT; + let acc2 = account(2); + let sigs: Vec<_> = (1..=4).map(signature).collect(); + + queue_pending_signatures(acc1, [sigs[0], sigs[1]]); + queue_pending_signatures(acc2, [sigs[2], sigs[3]]); + + // All four getTransaction calls return None (invalid deposit — just discard). + let runtime = TestCanisterRuntime::new() + .with_increasing_time() + .add_n_get_transaction_not_found(4); + + process_pending_signatures(runtime.clone()).await; + + // All consumed in one call thanks to multi-pass round-robin; no reschedule. + assert!(pending_signatures_for(&acc1).is_empty()); + assert!(pending_signatures_for(&acc2).is_empty()); + assert_eq!(runtime.set_timer_call_count(), 0); + EventsAssert::assert_no_events_recorded(); + } + + #[tokio::test] + async fn should_reschedule_when_capacity_exhausted() { + setup(); + reset_pending_signatures(); + + // MAX_CONCURRENT_RPC_CALLS + 1 signatures → only MAX fit in one round. + let sigs: Vec<_> = (0..=MAX_CONCURRENT_RPC_CALLS).map(signature).collect(); + queue_pending_signatures(DEPOSITOR_ACCOUNT, sigs.iter().copied()); + + let runtime = TestCanisterRuntime::new() + .with_increasing_time() + .add_n_get_transaction_not_found(MAX_CONCURRENT_RPC_CALLS); + + process_pending_signatures(runtime.clone()).await; + + // Last signature still pending → reschedule. + assert_eq!( + pending_signatures_for(&DEPOSITOR_ACCOUNT), + vec![sigs[MAX_CONCURRENT_RPC_CALLS]] + ); + assert_eq!(runtime.set_timer_call_count(), 1); + } + + fn setup() { + init_state(); + init_schnorr_master_key(); + } +} diff --git a/minter/src/deposit/manual/mod.rs b/minter/src/deposit/manual/mod.rs index 179f23e6..43fca09c 100644 --- a/minter/src/deposit/manual/mod.rs +++ b/minter/src/deposit/manual/mod.rs @@ -33,6 +33,7 @@ pub async fn process_deposit( let Deposit { deposit_amount, amount_to_mint, + source: _, } = match read_state(|state| state.deposit_status(&deposit_id)) { None => try_accept_deposit(&runtime, account, signature).await?, Some(DepositStatus::Processing { @@ -42,6 +43,7 @@ pub async fn process_deposit( }) => Deposit { deposit_amount, amount_to_mint, + source: DepositSource::Manual, }, // Deposit is already fully processed, nothing more to do Some(status @ (DepositStatus::Quarantined(_) | DepositStatus::Minted { .. })) => { @@ -112,5 +114,6 @@ async fn try_accept_deposit( Ok(Deposit { deposit_amount, amount_to_mint, + source: DepositSource::Manual, }) } diff --git a/minter/src/deposit/manual/tests.rs b/minter/src/deposit/manual/tests.rs index a1a0c6c6..07e93b7d 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; @@ -271,8 +265,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, @@ -371,7 +365,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], @@ -437,35 +431,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/main.rs b/minter/src/main.rs index 0203a3c6..3f6b96f6 100644 --- a/minter/src/main.rs +++ b/minter/src/main.rs @@ -3,7 +3,10 @@ use canlog::{Log, Sort}; use cksol_minter::{ address::lazy_get_schnorr_master_key, consolidate::{DEPOSIT_CONSOLIDATION_DELAY, consolidate_deposits}, - deposit::automatic::{POLL_MONITORED_ADDRESSES_DELAY, poll_monitored_addresses}, + deposit::automatic::{ + POLL_MONITORED_ADDRESSES_DELAY, PROCESS_PENDING_SIGNATURES_DELAY, poll_monitored_addresses, + process_pending_signatures, + }, monitor::{ FINALIZE_TRANSACTIONS_DELAY, RESUBMIT_TRANSACTIONS_DELAY, finalize_transactions, resubmit_transactions, @@ -377,6 +380,9 @@ fn setup_timers() { ic_cdk_timers::set_timer_interval(POLL_MONITORED_ADDRESSES_DELAY, async || { poll_monitored_addresses(IcCanisterRuntime::new()).await; }); + ic_cdk_timers::set_timer_interval(PROCESS_PENDING_SIGNATURES_DELAY, async || { + process_pending_signatures(IcCanisterRuntime::new()).await; + }); } fn main() {} 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/state/audit.rs b/minter/src/state/audit.rs index 75abceb9..18ca4af8 100644 --- a/minter/src/state/audit.rs +++ b/minter/src/state/audit.rs @@ -31,9 +31,9 @@ fn apply_state_transition(state: &mut State, payload: &EventType, timestamp: u64 deposit_id, deposit_amount, amount_to_mint, - source: _, + source, } => { - state.process_accepted_deposit(deposit_id, deposit_amount, amount_to_mint); + state.process_accepted_deposit(deposit_id, deposit_amount, amount_to_mint, *source); } EventType::QuarantinedDeposit(deposit_id) => state.process_quarantined_deposit(deposit_id), EventType::Minted { diff --git a/minter/src/state/mod.rs b/minter/src/state/mod.rs index c265d456..2a71192f 100644 --- a/minter/src/state/mod.rs +++ b/minter/src/state/mod.rs @@ -2,7 +2,9 @@ use crate::{ constants::{FEE_PER_SIGNATURE, RENT_EXEMPTION_THRESHOLD}, ledger::client::LedgerClient, numeric::{LedgerBurnIndex, LedgerMintIndex}, - state::event::{DepositId, TransactionPurpose, VersionedMessage, WithdrawalRequest}, + state::event::{ + DepositId, DepositSource, TransactionPurpose, VersionedMessage, WithdrawalRequest, + }, utils::{ insertion_ordered_map::InsertionOrderedMap, insertion_ordered_set::InsertionOrderedSet, }, @@ -271,6 +273,7 @@ impl State { if let Some(Deposit { deposit_amount, amount_to_mint, + .. }) = self.accepted_deposits.get(deposit_id) { return Some(DepositStatus::Processing { @@ -414,6 +417,7 @@ impl State { deposit_id: &DepositId, deposit_amount: &Lamport, amount_to_mint: &Lamport, + source: DepositSource, ) { assert!( !self.quarantined_deposits.contains_key(deposit_id), @@ -429,6 +433,7 @@ impl State { Deposit { deposit_amount: *deposit_amount, amount_to_mint: *amount_to_mint, + source, } ), None, @@ -827,6 +832,7 @@ pub struct SchnorrPublicKey { pub struct Deposit { pub deposit_amount: Lamport, pub amount_to_mint: Lamport, + pub source: DepositSource, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -843,6 +849,7 @@ pub enum TaskType { ResubmitTransactions, WithdrawalProcessing, PollMonitoredAddresses, + ProcessPendingSignatures, } /// Details about a consolidation transaction, capturing the individual 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;