@@ -568,6 +568,96 @@ mod tests {
568568 assert ! ( managed_account. transactions. contains_key( & coinbase_tx. txid( ) ) ) ;
569569 }
570570
571+ /// Test that spending a wallet-owned UTXO without creating change is detected
572+ #[ test]
573+ fn test_wallet_checker_detects_spend_only_transaction ( ) {
574+ let network = Network :: Testnet ;
575+ let wallet = Wallet :: new_random ( & [ network] , WalletAccountCreationOptions :: Default )
576+ . expect ( "Should create wallet" ) ;
577+
578+ let mut managed_wallet =
579+ ManagedWalletInfo :: from_wallet_with_name ( & wallet, "Test" . to_string ( ) ) ;
580+
581+ // Prepare a managed BIP44 account and derive a receive address
582+ let account_collection = wallet. accounts . get ( & network) . expect ( "Should have accounts" ) ;
583+ let wallet_account =
584+ account_collection. standard_bip44_accounts . get ( & 0 ) . expect ( "Should have BIP44 account" ) ;
585+
586+ let receive_address = managed_wallet
587+ . first_bip44_managed_account_mut ( network)
588+ . expect ( "Should have managed account" )
589+ . next_receive_address ( Some ( & wallet_account. account_xpub ) , true )
590+ . expect ( "Should derive receive address" ) ;
591+
592+ // Fund the wallet with a transaction paying to the receive address
593+ let funding_value = 50_000_000u64 ;
594+ let funding_tx = create_transaction_to_address ( & receive_address, funding_value) ;
595+ let funding_context = TransactionContext :: InBlock {
596+ height : 1 ,
597+ block_hash : Some ( BlockHash :: from_slice ( & [ 2u8 ; 32 ] ) . expect ( "Should create block hash" ) ) ,
598+ timestamp : Some ( 1_650_000_000 ) ,
599+ } ;
600+
601+ let funding_result =
602+ managed_wallet. check_transaction ( & funding_tx, network, funding_context, Some ( & wallet) ) ;
603+ assert ! ( funding_result. is_relevant, "Funding transaction must be relevant" ) ;
604+ assert_eq ! ( funding_result. total_received, funding_value) ;
605+
606+ // Build a spend transaction that sends funds to an external address only
607+ let external_address = Address :: p2pkh (
608+ & dashcore:: PublicKey :: from_slice ( & [ 0x02 ; 33 ] ) . expect ( "Should create pubkey" ) ,
609+ network,
610+ ) ;
611+ let spend_tx = Transaction {
612+ version : 2 ,
613+ lock_time : 0 ,
614+ input : vec ! [ TxIn {
615+ previous_output: OutPoint {
616+ txid: funding_tx. txid( ) ,
617+ vout: 0 ,
618+ } ,
619+ script_sig: ScriptBuf :: new( ) ,
620+ sequence: 0xffffffff ,
621+ witness: dashcore:: Witness :: new( ) ,
622+ } ] ,
623+ output : vec ! [ TxOut {
624+ value: funding_value - 1_000 , // leave a small fee
625+ script_pubkey: external_address. script_pubkey( ) ,
626+ } ] ,
627+ special_transaction_payload : None ,
628+ } ;
629+
630+ let spend_context = TransactionContext :: InBlock {
631+ height : 2 ,
632+ block_hash : Some ( BlockHash :: from_slice ( & [ 3u8 ; 32 ] ) . expect ( "Should create block hash" ) ) ,
633+ timestamp : Some ( 1_650_000_100 ) ,
634+ } ;
635+
636+ let spend_result =
637+ managed_wallet. check_transaction ( & spend_tx, network, spend_context, Some ( & wallet) ) ;
638+
639+ assert ! ( spend_result. is_relevant, "Spend transaction should be detected" ) ;
640+ assert_eq ! ( spend_result. total_received, 0 ) ;
641+ assert_eq ! ( spend_result. total_sent, funding_value) ;
642+
643+ // Ensure the UTXO was removed and the transaction record reflects the spend
644+ let account = managed_wallet
645+ . accounts
646+ . get ( & network)
647+ . expect ( "Should have managed accounts" )
648+ . standard_bip44_accounts
649+ . get ( & 0 )
650+ . expect ( "Should have managed BIP44 account" ) ;
651+
652+ assert ! ( account. utxos. is_empty( ) , "Spent UTXO should be removed" ) ;
653+
654+ let record = account
655+ . transactions
656+ . get ( & spend_tx. txid ( ) )
657+ . expect ( "Spend transaction should be recorded" ) ;
658+ assert_eq ! ( record. net_amount, -( funding_value as i64 ) ) ;
659+ }
660+
571661 /// Test mempool context for timestamp/height handling
572662 #[ test]
573663 fn test_wallet_checker_mempool_context ( ) {
0 commit comments