From 15ab0348c94863f9d28a57e5a285853ff3339b89 Mon Sep 17 00:00:00 2001 From: Matthew Date: Fri, 26 Jun 2026 10:31:02 -0500 Subject: [PATCH] feat!: expose chainposition first and last seen timestamps --- bdk-ffi/src/tests/wallet.rs | 63 ++++++++++++++++++++++++++++++++++++- bdk-ffi/src/types.rs | 22 ++++++++++--- bdk-ffi/src/wallet.rs | 4 +-- 3 files changed, 82 insertions(+), 7 deletions(-) diff --git a/bdk-ffi/src/tests/wallet.rs b/bdk-ffi/src/tests/wallet.rs index 6219fa7b..f5ca5776 100644 --- a/bdk-ffi/src/tests/wallet.rs +++ b/bdk-ffi/src/tests/wallet.rs @@ -1,10 +1,18 @@ -use crate::bitcoin::{Network, NetworkKind}; +use crate::bitcoin::{Network, NetworkKind, Transaction}; use crate::descriptor::Descriptor; use crate::store::Persister; +use crate::types::{ChainPosition, UnconfirmedTx}; use crate::wallet::Wallet; +use bdk_wallet::bitcoin::absolute::LockTime; +use bdk_wallet::bitcoin::transaction::Version; +use bdk_wallet::bitcoin::{ + Amount as BdkAmount, OutPoint as BdkOutPoint, ScriptBuf, Sequence, + Transaction as BdkTransaction, TxIn as BdkTxIn, TxOut as BdkTxOut, Txid as BdkTxid, Witness, +}; use bdk_wallet::KeychainKind; +use std::str::FromStr; use std::sync::Arc; const EXTERNAL_DESCRIPTOR: &str = "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)"; @@ -35,6 +43,33 @@ fn build_wallet() -> Wallet { .unwrap() } +fn unconfirmed_tx_to_wallet(wallet: &Wallet) -> Arc { + let address_info = wallet.reveal_next_address(KeychainKind::External); + let script_pubkey = ScriptBuf::from_bytes(address_info.address.script_pubkey().to_bytes()); + let tx = BdkTransaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: vec![BdkTxIn { + previous_output: BdkOutPoint { + txid: BdkTxid::from_str( + "0101010101010101010101010101010101010101010101010101010101010101", + ) + .unwrap(), + vout: 0, + }, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::default(), + }], + output: vec![BdkTxOut { + value: BdkAmount::from_sat(50_000), + script_pubkey, + }], + }; + + Arc::new(Transaction::from(tx)) +} + #[test] fn test_create_wallet() { let wallet = build_wallet(); @@ -150,3 +185,29 @@ fn test_create_two_path_wallet() { assert_eq!(wallet.derivation_index(KeychainKind::External), Some(0)); assert_eq!(wallet.derivation_index(KeychainKind::Internal), Some(0)); } + +#[test] +fn test_chain_position_preserves_first_and_last_seen() { + let wallet = build_wallet(); + let tx = unconfirmed_tx_to_wallet(&wallet); + + wallet.apply_unconfirmed_txs(vec![UnconfirmedTx { + tx: tx.clone(), + last_seen: 10, + }]); + wallet.apply_unconfirmed_txs(vec![UnconfirmedTx { tx, last_seen: 20 }]); + + let transactions = wallet.transactions(); + + assert_eq!(transactions.len(), 1); + match &transactions[0].chain_position { + ChainPosition::Unconfirmed { + first_seen, + last_seen, + } => { + assert_eq!(*first_seen, Some(10)); + assert_eq!(*last_seen, Some(20)); + } + other => panic!("expected unconfirmed chain position, got {:?}", other), + } +} diff --git a/bdk-ffi/src/types.rs b/bdk-ffi/src/types.rs index 0cfe874c..291b377c 100644 --- a/bdk-ffi/src/types.rs +++ b/bdk-ffi/src/types.rs @@ -77,8 +77,18 @@ pub enum ChainPosition { /// or equal to this child TXID. transitively: Option>, }, - /// The transaction was last seen in the mempool at this timestamp. - Unconfirmed { timestamp: Option }, + /// The chain data is not confirmed. + Unconfirmed { + /// When the chain data was first seen in the mempool. + /// + /// This value will be `None` if the chain data was never seen in the mempool. + first_seen: Option, + /// When the chain data is last seen in the mempool. + /// + /// This value will be `None` if the chain data was never seen in the mempool and only seen + /// in a conflicting chain. + last_seen: Option, + }, } impl From> for ChainPosition { @@ -100,8 +110,12 @@ impl From> for ChainPosition { transitively: transitively.map(|t| Arc::new(Txid(t))), } } - BdkChainPosition::Unconfirmed { last_seen, .. } => ChainPosition::Unconfirmed { - timestamp: last_seen, + BdkChainPosition::Unconfirmed { + first_seen, + last_seen, + } => ChainPosition::Unconfirmed { + first_seen, + last_seen, }, } } diff --git a/bdk-ffi/src/wallet.rs b/bdk-ffi/src/wallet.rs index acd73c6a..f0c0f30e 100644 --- a/bdk-ffi/src/wallet.rs +++ b/bdk-ffi/src/wallet.rs @@ -534,8 +534,8 @@ impl Wallet { /// in the best chain. /// * The [`ChainPosition`] of the transaction in the best chain - whether the transaction is /// confirmed or unconfirmed. If the transaction is confirmed, the anchor which proves the - /// confirmation is provided. If the transaction is unconfirmed, the unix timestamp of when - /// the transaction was last seen in the mempool is provided. + /// confirmation is provided. If the transaction is unconfirmed, the unix timestamps of when + /// the transaction was first and last seen in the mempool are provided. pub fn get_tx(&self, txid: Arc) -> Result, TxidParseError> { Ok(self.get_wallet().get_tx(txid.0).map(|tx| tx.into())) }