From 513a08a75cf9c774bdb8027f90ae399350f149dc Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 23 Apr 2026 12:57:32 +0200 Subject: [PATCH] feat: queue discovered deposit signatures for processing When `getSignaturesForAddress` returns signatures for a monitored account, enqueue them in a stable BTreeMap instead of discarding them. Co-Authored-By: Claude Sonnet 4.6 --- minter/src/deposit/automatic/mod.rs | 11 ++++- minter/src/deposit/automatic/tests.rs | 60 +++++++++++++++++++++++++++ minter/src/storage/mod.rs | 34 ++++++++++++++- 3 files changed, 102 insertions(+), 3 deletions(-) diff --git a/minter/src/deposit/automatic/mod.rs b/minter/src/deposit/automatic/mod.rs index 970b894b..3502c270 100644 --- a/minter/src/deposit/automatic/mod.rs +++ b/minter/src/deposit/automatic/mod.rs @@ -8,6 +8,7 @@ use crate::{ SchnorrPublicKey, TaskType, audit::process_event, event::EventType, mutate_state, read_state, }, + storage::with_discovered_signatures_mut, }; use canlog::log; use cksol_types::UpdateBalanceError; @@ -123,8 +124,14 @@ async fn poll_account( "Failed to get signatures for address {deposit_address}: {e}" ); } - Ok(_signatures) => { - // TODO(DEFI-2780): Process discovered deposit signatures. + Ok(signatures) => { + with_discovered_signatures_mut(|queue| { + for sig_entry in &signatures { + let sig_bytes: [u8; 64] = + solana_signature::Signature::from(sig_entry.signature.clone()).into(); + queue.insert(sig_bytes, account); + } + }); } } diff --git a/minter/src/deposit/automatic/tests.rs b/minter/src/deposit/automatic/tests.rs index 618759df..a1f0cb92 100644 --- a/minter/src/deposit/automatic/tests.rs +++ b/minter/src/deposit/automatic/tests.rs @@ -2,6 +2,7 @@ use super::*; use crate::{ constants::MAX_CONCURRENT_RPC_CALLS, state::{event::EventType, read_state}, + storage::{reset_discovered_signatures, with_discovered_signatures}, test_fixtures::{ EventsAssert, account, events::start_monitoring_account, init_schnorr_master_key, init_state, runtime::TestCanisterRuntime, @@ -21,6 +22,17 @@ fn start_monitoring_max_number_of_accounts() { } } +fn confirmed_tx_status(signature_bytes: [u8; 64]) -> ConfirmedTransactionStatusWithSignature { + ConfirmedTransactionStatusWithSignature { + signature: solana_signature::Signature::from(signature_bytes).into(), + slot: 12345, + err: None, + memo: None, + block_time: None, + confirmation_status: None, + } +} + mod update_balance { use super::*; @@ -131,8 +143,56 @@ mod poll_monitored_addresses { } } + #[tokio::test] + async fn should_enqueue_discovered_signatures() { + setup(); + + let account = account(0); + start_monitoring_account(account); + + let sig1: [u8; 64] = [1u8; 64]; + let sig2: [u8; 64] = [2u8; 64]; + let runtime = TestCanisterRuntime::new() + .with_increasing_time() + .add_stub_response(SignaturesResult::Consistent(Ok(vec![ + confirmed_tx_status(sig1), + confirmed_tx_status(sig2), + ]))); + + poll_monitored_addresses(runtime).await; + + with_discovered_signatures(|queue| { + assert_eq!(queue.get(&sig1), Some(account)); + assert_eq!(queue.get(&sig2), Some(account)); + assert_eq!(queue.len(), 2); + }); + } + + #[tokio::test] + async fn should_not_enqueue_signatures_on_rpc_failure() { + setup(); + + let account = account(0); + start_monitoring_account(account); + + let runtime = TestCanisterRuntime::new() + .with_increasing_time() + .add_stub_response(SignaturesResult::Consistent(Err( + sol_rpc_types::RpcError::ProviderError( + sol_rpc_types::ProviderError::InvalidRpcConfig("test".to_string()), + ), + ))); + + poll_monitored_addresses(runtime).await; + + with_discovered_signatures(|queue| { + assert_eq!(queue.len(), 0); + }); + } + fn setup() { init_state(); init_schnorr_master_key(); + reset_discovered_signatures(); } } diff --git a/minter/src/storage/mod.rs b/minter/src/storage/mod.rs index d59d3093..7527fd5f 100644 --- a/minter/src/storage/mod.rs +++ b/minter/src/storage/mod.rs @@ -3,16 +3,21 @@ use crate::{ state::event::{Event, EventType}, }; use ic_stable_structures::{ - DefaultMemoryImpl, StableLog, + DefaultMemoryImpl, StableBTreeMap, StableLog, memory_manager::{MemoryId, MemoryManager, VirtualMemory}, }; +use icrc_ledger_types::icrc1::account::Account; use std::cell::RefCell; const EVENT_LOG_INDEX_MEMORY_ID: MemoryId = MemoryId::new(0); const EVENT_LOG_DATA_MEMORY_ID: MemoryId = MemoryId::new(1); +const DISCOVERED_SIGNATURES_MEMORY_ID: MemoryId = MemoryId::new(2); type VMem = VirtualMemory; type EventLog = StableLog; +/// Maps signature bytes to the account that owns the deposit. +/// Using signature as key since Solana transaction signatures are globally unique. +type DiscoveredSignatures = StableBTreeMap<[u8; 64], Account, VMem>; thread_local! { static MEMORY_MANAGER: RefCell> = RefCell::new( @@ -30,6 +35,10 @@ thread_local! { ) ); + /// Queue of discovered deposit transaction signatures awaiting processing. + static DISCOVERED_SIGNATURES: RefCell = MEMORY_MANAGER + .with(|m| RefCell::new(StableBTreeMap::init(m.borrow().get(DISCOVERED_SIGNATURES_MEMORY_ID)))); + static UNSTABLE_METRICS: RefCell = const { RefCell::new(Metrics::new()) }; } @@ -84,6 +93,20 @@ where EVENTS.with(|events| f(Box::new(events.borrow().iter()))) } +pub fn with_discovered_signatures_mut(f: F) -> R +where + F: FnOnce(&mut DiscoveredSignatures) -> R, +{ + DISCOVERED_SIGNATURES.with(|q| f(&mut q.borrow_mut())) +} + +pub fn with_discovered_signatures(f: F) -> R +where + F: FnOnce(&DiscoveredSignatures) -> R, +{ + DISCOVERED_SIGNATURES.with(|q| f(&q.borrow())) +} + #[cfg(any(test, feature = "canbench-rs"))] pub(crate) fn reset_events() { MEMORY_MANAGER.with(|m| { @@ -95,3 +118,12 @@ pub(crate) fn reset_events() { }); }); } + +#[cfg(any(test, feature = "canbench-rs"))] +pub(crate) fn reset_discovered_signatures() { + MEMORY_MANAGER.with(|m| { + DISCOVERED_SIGNATURES.with(|q| { + *q.borrow_mut() = StableBTreeMap::new(m.borrow().get(DISCOVERED_SIGNATURES_MEMORY_ID)); + }); + }); +}