Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions dash/src/test_utils/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ use std::ops::Range;
use crate::{Address, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid, Witness};

impl Transaction {
/// Creates a transaction with no inputs or outputs.
pub fn empty() -> Transaction {
Transaction {
version: 1,
lock_time: 0,
input: Vec::new(),
output: Vec::new(),
special_transaction_payload: None,
}
}

pub fn dummy(
address: &Address,
inputs_ids_range: Range<u8>,
Expand Down
8 changes: 6 additions & 2 deletions key-wallet-ffi/include/key_wallet_ffi.h
Original file line number Diff line number Diff line change
Expand Up @@ -177,14 +177,18 @@ typedef enum {
Transaction is in the mempool (unconfirmed)
*/
MEMPOOL = 0,
/*
Transaction is in the mempool with an InstantSend lock
*/
INSTANT_SEND = 1,
/*
Transaction is in a block at the given height
*/
IN_BLOCK = 1,
IN_BLOCK = 2,
/*
Transaction is in a chain-locked block at the given height
*/
IN_CHAIN_LOCKED_BLOCK = 2,
IN_CHAIN_LOCKED_BLOCK = 3,
} FFITransactionContext;

/*
Expand Down
1 change: 1 addition & 0 deletions key-wallet-ffi/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ pub unsafe extern "C" fn wallet_check_transaction(
},
}
}
FFITransactionContext::InstantSend => TransactionContext::InstantSend,
};

// Create a ManagedWalletInfo from the wallet
Expand Down
6 changes: 4 additions & 2 deletions key-wallet-ffi/src/transaction_checking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ pub unsafe extern "C" fn managed_wallet_check_transaction(
},
}
}
FFITransactionContext::InstantSend => TransactionContext::InstantSend,
};

// Check the transaction - wallet is now required
Expand Down Expand Up @@ -615,7 +616,8 @@ mod tests {
fn test_transaction_context_conversion() {
// Test that FFI transaction context values match expectations
assert_eq!(FFITransactionContext::Mempool as u32, 0);
assert_eq!(FFITransactionContext::InBlock as u32, 1);
assert_eq!(FFITransactionContext::InChainLockedBlock as u32, 2);
assert_eq!(FFITransactionContext::InstantSend as u32, 1);
assert_eq!(FFITransactionContext::InBlock as u32, 2);
assert_eq!(FFITransactionContext::InChainLockedBlock as u32, 3);
}
}
27 changes: 22 additions & 5 deletions key-wallet-ffi/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Common types for FFI interface

use key_wallet::transaction_checking::TransactionContext;
use key_wallet::{Network, Wallet};
use std::os::raw::{c_char, c_uint};
use std::sync::Arc;
Expand Down Expand Up @@ -712,10 +713,27 @@ impl FFIWalletAccountCreationOptions {
pub enum FFITransactionContext {
/// Transaction is in the mempool (unconfirmed)
Mempool = 0,
/// Transaction is in the mempool with an InstantSend lock
InstantSend = 1,
/// Transaction is in a block at the given height
InBlock = 1,
InBlock = 2,
/// Transaction is in a chain-locked block at the given height
InChainLockedBlock = 2,
InChainLockedBlock = 3,
}

impl From<TransactionContext> for FFITransactionContext {
fn from(ctx: TransactionContext) -> Self {
match ctx {
TransactionContext::Mempool => FFITransactionContext::Mempool,
TransactionContext::InstantSend => FFITransactionContext::InstantSend,
TransactionContext::InBlock {
..
} => FFITransactionContext::InBlock,
TransactionContext::InChainLockedBlock {
..
} => FFITransactionContext::InChainLockedBlock,
}
}
}

/// FFI-compatible transaction context details
Expand Down Expand Up @@ -764,9 +782,7 @@ impl FFITransactionContextDetails {
}

/// Convert to the native TransactionContext
pub fn to_transaction_context(&self) -> key_wallet::transaction_checking::TransactionContext {
use key_wallet::transaction_checking::TransactionContext;

pub fn to_transaction_context(&self) -> TransactionContext {
match self.context_type {
FFITransactionContext::Mempool => TransactionContext::Mempool,
FFITransactionContext::InBlock => {
Expand Down Expand Up @@ -815,6 +831,7 @@ impl FFITransactionContextDetails {
},
}
}
FFITransactionContext::InstantSend => TransactionContext::InstantSend,
}
}
}
39 changes: 39 additions & 0 deletions key-wallet/src/managed_account/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,8 @@ impl ManagedCoreAccount {
tx.is_coin_base(),
);
utxo.is_confirmed = context.confirmed();
utxo.is_instantlocked =
matches!(context, TransactionContext::InstantSend);
self.utxos.insert(outpoint, utxo);
}
}
Expand All @@ -374,6 +376,30 @@ impl ManagedCoreAccount {
}
}

/// Re-process an existing transaction with updated context (e.g., mempool→block confirmation)
/// and potentially new address matches from gap limit rescans.
pub(crate) fn confirm_transaction(
&mut self,
tx: &Transaction,
account_match: &AccountMatch,
context: TransactionContext,
) -> bool {
let mut changed = false;
if let Some(tx_record) = self.transactions.get_mut(&tx.txid()) {
if !tx_record.is_confirmed() {
if let (Some(height), Some(hash)) = (context.block_height(), context.block_hash()) {
tx_record.mark_confirmed(height, hash);
changed = true;
}
if let Some(ts) = context.timestamp() {
tx_record.timestamp = ts as u64;
}
}
}
self.update_utxos(tx, account_match, context);
changed
}

/// Record a new transaction and update UTXOs for spendable account types
pub(crate) fn record_transaction(
&mut self,
Expand All @@ -399,6 +425,19 @@ impl ManagedCoreAccount {
self.update_utxos(tx, account_match, context);
}

/// Mark all UTXOs belonging to a transaction as InstantSend-locked.
/// Returns `true` if any UTXO was newly marked.
pub(crate) fn mark_utxos_instant_send(&mut self, txid: &Txid) -> bool {
let mut any_changed = false;
for utxo in self.utxos.values_mut() {
if utxo.outpoint.txid == *txid && !utxo.is_instantlocked {
utxo.is_instantlocked = true;
any_changed = true;
}
}
any_changed
}

/// Update the account balance
pub fn update_balance(&mut self, synced_height: u32) {
let mut spendable = 0;
Expand Down
23 changes: 6 additions & 17 deletions key-wallet/src/managed_account/transaction_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,20 +137,9 @@ mod tests {
use super::*;
use dashcore::hashes::Hash;

fn create_test_transaction() -> Transaction {
// Create a minimal test transaction
Transaction {
version: 1,
lock_time: 0,
input: Vec::new(),
output: Vec::new(),
special_transaction_payload: None,
}
}

#[test]
fn test_transaction_record_creation() {
let tx = create_test_transaction();
let tx = Transaction::empty();
let record = TransactionRecord::new(tx.clone(), 1234567890, 50000, true);

assert_eq!(record.txid, tx.txid());
Expand All @@ -162,7 +151,7 @@ mod tests {

#[test]
fn test_confirmations_calculation() {
let tx = create_test_transaction();
let tx = Transaction::empty();
let mut record = TransactionRecord::new(tx, 1234567890, 50000, true);

// Unconfirmed transaction
Expand All @@ -187,7 +176,7 @@ mod tests {

#[test]
fn test_incoming_outgoing() {
let tx = create_test_transaction();
let tx = Transaction::empty();

let incoming = TransactionRecord::new(tx.clone(), 1234567890, 50000, false);
assert!(incoming.is_incoming());
Expand All @@ -202,7 +191,7 @@ mod tests {

#[test]
fn test_confirmed_transaction_creation() {
let tx = create_test_transaction();
let tx = Transaction::empty();
let block_hash = BlockHash::all_zeros();
let record =
TransactionRecord::new_confirmed(tx.clone(), 100, block_hash, 1234567890, 50000, true);
Expand All @@ -214,7 +203,7 @@ mod tests {

#[test]
fn test_mark_unconfirmed() {
let tx = create_test_transaction();
let tx = Transaction::empty();
let block_hash = BlockHash::all_zeros();
let mut record =
TransactionRecord::new_confirmed(tx, 100, block_hash, 1234567890, 50000, true);
Expand All @@ -230,7 +219,7 @@ mod tests {

#[test]
fn test_labels_and_fees() {
let tx = create_test_transaction();
let tx = Transaction::empty();
let mut record = TransactionRecord::new(tx, 1234567890, -50000, true);

assert_eq!(record.fee, None);
Expand Down
2 changes: 2 additions & 0 deletions key-wallet/src/test_utils/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
mod account;
mod utxo;
mod wallet;

pub use wallet::TestWalletContext;
91 changes: 89 additions & 2 deletions key-wallet/src/test_utils/wallet.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,96 @@
use dashcore::Network;
use dashcore::blockdata::transaction::Transaction;
use dashcore::{Address, Network, Txid};

use crate::wallet::ManagedWalletInfo;
use crate::managed_account::transaction_record::TransactionRecord;
use crate::managed_account::ManagedCoreAccount;
use crate::transaction_checking::{
TransactionCheckResult, TransactionContext, WalletTransactionChecker,
};
use crate::utxo::Utxo;
use crate::wallet::initialization::WalletAccountCreationOptions;
use crate::wallet::{ManagedWalletInfo, Wallet};
use crate::ExtendedPubKey;

impl ManagedWalletInfo {
pub fn dummy(id: u8) -> Self {
ManagedWalletInfo::new(Network::Regtest, [id; 32])
}
}

/// Pre-built wallet context for transaction checking tests.
///
/// Provides a testnet wallet with a default BIP44 account, a pre-derived
/// receive address, and the corresponding extended public key.
pub struct TestWalletContext {
pub managed_wallet: ManagedWalletInfo,
pub wallet: Wallet,
pub receive_address: Address,
pub xpub: ExtendedPubKey,
}

impl TestWalletContext {
/// Creates a new random testnet wallet with a BIP44 account and one
/// pre-derived receive address.
pub fn new_random() -> Self {
let wallet = Wallet::new_random(Network::Testnet, WalletAccountCreationOptions::Default)
.expect("Should create wallet");
let mut managed_wallet =
ManagedWalletInfo::from_wallet_with_name(&wallet, "Test".to_string());

let xpub = wallet
.accounts
.standard_bip44_accounts
.get(&0)
.expect("Should have BIP44 account")
.account_xpub;

let receive_address = managed_wallet
.first_bip44_managed_account_mut()
.expect("Should have managed account")
.next_receive_address(Some(&xpub), true)
.expect("Should get address");

Self {
managed_wallet,
wallet,
receive_address,
xpub,
}
}

/// Returns the first BIP44 managed account (immutable).
pub fn bip44_account(&self) -> &ManagedCoreAccount {
self.managed_wallet.first_bip44_managed_account().expect("Should have BIP44 account")
}

/// Returns a transaction record by txid from the first BIP44 account.
pub fn transaction(&self, txid: &Txid) -> &TransactionRecord {
self.bip44_account().transactions.get(txid).expect("Should have transaction")
}

/// Returns the first UTXO from the first BIP44 account.
pub fn first_utxo(&self) -> &Utxo {
self.bip44_account().utxos.values().next().expect("Should have UTXO")
}

/// Runs `check_core_transaction` with `update_state = true`.
pub async fn check_transaction(
&mut self,
tx: &Transaction,
context: TransactionContext,
) -> TransactionCheckResult {
self.managed_wallet.check_core_transaction(tx, context, &mut self.wallet, true).await
}

/// Funds the wallet's receive address via a mempool transaction and
/// asserts it was accepted. Returns the context and the funding transaction.
pub async fn with_mempool_funding(mut self, amount: u64) -> (Self, Transaction) {
let tx = Transaction::dummy(&self.receive_address, 0..1, &[amount]);

let result = self.check_transaction(&tx, TransactionContext::Mempool).await;
assert!(result.is_relevant);
assert!(result.is_new_transaction);

(self, tx)
}
}
2 changes: 1 addition & 1 deletion key-wallet/src/transaction_checking/account_checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ impl ManagedAccountCollection {
) -> TransactionCheckResult {
let mut result = TransactionCheckResult {
is_relevant: false,
is_new_transaction: true,
is_new_transaction: false,
affected_accounts: Vec::new(),
total_received: 0,
total_sent: 0,
Expand Down
Loading
Loading