-use crate::ConfirmationBlockTime;
use bitcoin::{OutPoint, TxOut, Txid};
use crate::{Anchor, COINBASE_MATURITY};
///
/// The generic `A` should be a [`Anchor`] implementation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, core::hash::Hash)]
+#[cfg_attr(
+ feature = "serde",
+ derive(serde::Deserialize, serde::Serialize),
+ serde(bound(
+ deserialize = "A: Ord + serde::Deserialize<'de>",
+ serialize = "A: Ord + serde::Serialize",
+ ))
+)]
pub enum ChainPosition<A> {
/// The chain data is seen as confirmed, and in anchored by `A`.
Confirmed(A),
}
}
-/// Block height and timestamp at which a transaction is confirmed.
-#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
-#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
-pub enum ConfirmationTime {
- /// The transaction is confirmed
- Confirmed {
- /// Confirmation height.
- height: u32,
- /// Confirmation time in unix seconds.
- time: u64,
- },
- /// The transaction is unconfirmed
- Unconfirmed {
- /// The last-seen timestamp in unix seconds.
- last_seen: u64,
- },
-}
-
-impl ConfirmationTime {
- /// Construct an unconfirmed variant using the given `last_seen` time in unix seconds.
- pub fn unconfirmed(last_seen: u64) -> Self {
- Self::Unconfirmed { last_seen }
- }
-
- /// Returns whether [`ConfirmationTime`] is the confirmed variant.
- pub fn is_confirmed(&self) -> bool {
- matches!(self, Self::Confirmed { .. })
- }
-}
-
-impl From<ChainPosition<ConfirmationBlockTime>> for ConfirmationTime {
- fn from(observed_as: ChainPosition<ConfirmationBlockTime>) -> Self {
- match observed_as {
- ChainPosition::Confirmed(a) => Self::Confirmed {
- height: a.block_id.height,
- time: a.confirmation_time,
- },
- ChainPosition::Unconfirmed(last_seen) => Self::Unconfirmed { last_seen },
- }
- }
-}
-
/// A `TxOut` with as much data as we can retrieve about it
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct FullTxOut<A> {
#[cfg(test)]
mod test {
+ use bdk_core::ConfirmationBlockTime;
+
use crate::BlockId;
use super::*;
// licenses.
use alloc::boxed::Box;
+use chain::{ChainPosition, ConfirmationBlockTime};
use core::convert::AsRef;
-use bdk_chain::ConfirmationTime;
use bitcoin::transaction::{OutPoint, Sequence, TxOut};
use bitcoin::{psbt, Weight};
pub is_spent: bool,
/// The derivation index for the script pubkey in the wallet
pub derivation_index: u32,
- /// The confirmation time for transaction containing this utxo
- pub confirmation_time: ConfirmationTime,
+ /// The position of the output in the blockchain.
+ pub chain_position: ChainPosition<ConfirmationBlockTime>,
}
/// A [`Utxo`] with its `satisfaction_weight`.
// For utxo that doesn't exist in DB, they will have lowest priority to be selected
let utxos = {
optional_utxos.sort_unstable_by_key(|wu| match &wu.utxo {
- Utxo::Local(local) => Some(local.confirmation_time),
+ Utxo::Local(local) => Some(local.chain_position),
Utxo::Foreign { .. } => None,
});
#[cfg(test)]
mod test {
use assert_matches::assert_matches;
+ use bitcoin::hashes::Hash;
+ use chain::{BlockId, ChainPosition, ConfirmationBlockTime};
use core::str::FromStr;
use rand::rngs::StdRng;
- use bdk_chain::ConfirmationTime;
- use bitcoin::{Amount, ScriptBuf, TxIn, TxOut};
+ use bitcoin::{Amount, BlockHash, ScriptBuf, TxIn, TxOut};
use super::*;
use crate::types::*;
const FEE_AMOUNT: u64 = 50;
- fn utxo(value: u64, index: u32, confirmation_time: ConfirmationTime) -> WeightedUtxo {
+ fn unconfirmed_utxo(value: u64, index: u32, last_seen: u64) -> WeightedUtxo {
+ utxo(value, index, ChainPosition::Unconfirmed(last_seen))
+ }
+
+ fn confirmed_utxo(
+ value: u64,
+ index: u32,
+ confirmation_height: u32,
+ confirmation_time: u64,
+ ) -> WeightedUtxo {
+ utxo(
+ value,
+ index,
+ ChainPosition::Confirmed(ConfirmationBlockTime {
+ block_id: chain::BlockId {
+ height: confirmation_height,
+ hash: bitcoin::BlockHash::all_zeros(),
+ },
+ confirmation_time,
+ }),
+ )
+ }
+
+ fn utxo(
+ value: u64,
+ index: u32,
+ chain_position: ChainPosition<ConfirmationBlockTime>,
+ ) -> WeightedUtxo {
assert!(index < 10);
let outpoint = OutPoint::from_str(&format!(
"000000000000000000000000000000000000000000000000000000000000000{}:0",
keychain: KeychainKind::External,
is_spent: false,
derivation_index: 42,
- confirmation_time,
+ chain_position,
}),
}
}
fn get_test_utxos() -> Vec<WeightedUtxo> {
vec![
- utxo(100_000, 0, ConfirmationTime::Unconfirmed { last_seen: 0 }),
- utxo(
- FEE_AMOUNT - 40,
- 1,
- ConfirmationTime::Unconfirmed { last_seen: 0 },
- ),
- utxo(200_000, 2, ConfirmationTime::Unconfirmed { last_seen: 0 }),
+ unconfirmed_utxo(100_000, 0, 0),
+ unconfirmed_utxo(FEE_AMOUNT - 40, 1, 0),
+ unconfirmed_utxo(200_000, 2, 0),
]
}
fn get_oldest_first_test_utxos() -> Vec<WeightedUtxo> {
// ensure utxos are from different tx
- let utxo1 = utxo(
- 120_000,
- 1,
- ConfirmationTime::Confirmed {
- height: 1,
- time: 1231006505,
- },
- );
- let utxo2 = utxo(
- 80_000,
- 2,
- ConfirmationTime::Confirmed {
- height: 2,
- time: 1231006505,
- },
- );
- let utxo3 = utxo(
- 300_000,
- 3,
- ConfirmationTime::Confirmed {
- height: 3,
- time: 1231006505,
- },
- );
+ let utxo1 = confirmed_utxo(120_000, 1, 1, 1231006505);
+ let utxo2 = confirmed_utxo(80_000, 2, 2, 1231006505);
+ let utxo3 = confirmed_utxo(300_000, 3, 3, 1231006505);
vec![utxo1, utxo2, utxo3]
}
keychain: KeychainKind::External,
is_spent: false,
derivation_index: rng.next_u32(),
- confirmation_time: if rng.gen_bool(0.5) {
- ConfirmationTime::Confirmed {
- height: rng.next_u32(),
- time: rng.next_u64(),
- }
+ chain_position: if rng.gen_bool(0.5) {
+ ChainPosition::Confirmed(ConfirmationBlockTime {
+ block_id: chain::BlockId {
+ height: rng.next_u32(),
+ hash: BlockHash::all_zeros(),
+ },
+ confirmation_time: rng.next_u64(),
+ })
} else {
- ConfirmationTime::Unconfirmed { last_seen: 0 }
+ ChainPosition::Unconfirmed(0)
},
}),
});
keychain: KeychainKind::External,
is_spent: false,
derivation_index: 42,
- confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 },
+ chain_position: ChainPosition::Unconfirmed(0),
}),
})
.collect()
optional.push(utxo(
500_000,
3,
- ConfirmationTime::Unconfirmed { last_seen: 0 },
+ ChainPosition::<ConfirmationBlockTime>::Unconfirmed(0),
));
// Defensive assertions, for sanity and in case someone changes the test utxos vector.
keychain: KeychainKind::External,
is_spent: false,
derivation_index: 0,
- confirmation_time: ConfirmationTime::Confirmed {
- height: 12345,
- time: 12345,
- },
+ chain_position: ChainPosition::Confirmed(ConfirmationBlockTime {
+ block_id: BlockId {
+ height: 12345,
+ hash: BlockHash::all_zeros(),
+ },
+ confirmation_time: 12345,
+ }),
}),
}
}
SyncResult,
},
tx_graph::{CanonicalTx, TxGraph, TxNode, TxUpdate},
- BlockId, ChainPosition, ConfirmationBlockTime, ConfirmationTime, DescriptorExt, FullTxOut,
- Indexed, IndexedTxGraph, Merge,
+ BlockId, ChainPosition, ConfirmationBlockTime, DescriptorExt, FullTxOut, Indexed,
+ IndexedTxGraph, Merge,
};
use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
use bitcoin::{
.ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?;
let txout = &prev_tx.output[txin.previous_output.vout as usize];
- let confirmation_time: ConfirmationTime = graph
+ let chain_position = graph
.get_chain_position(&self.chain, chain_tip, txin.previous_output.txid)
.ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?
- .cloned()
- .into();
+ .cloned();
let weighted_utxo = match txout_index.index_of_spk(txout.script_pubkey.clone()) {
Some(&(keychain, derivation_index)) => {
keychain,
is_spent: true,
derivation_index,
- confirmation_time,
+ chain_position,
}),
satisfaction_weight,
}
Some(tx) => tx,
None => return false,
};
- let confirmation_time: ConfirmationTime = match self
- .indexed_graph
- .graph()
- .get_chain_position(&self.chain, chain_tip, txid)
- {
- Some(chain_position) => chain_position.cloned().into(),
+ let chain_position = match self.indexed_graph.graph().get_chain_position(
+ &self.chain,
+ chain_tip,
+ txid,
+ ) {
+ Some(chain_position) => chain_position.cloned(),
None => return false,
};
// Whether the UTXO is mature and, if needed, confirmed
let mut spendable = true;
- if must_only_use_confirmed_tx && !confirmation_time.is_confirmed() {
+ if must_only_use_confirmed_tx && !chain_position.is_confirmed() {
return false;
}
if tx.is_coinbase() {
debug_assert!(
- confirmation_time.is_confirmed(),
+ chain_position.is_confirmed(),
"coinbase must always be confirmed"
);
if let Some(current_height) = current_height {
- match confirmation_time {
- ConfirmationTime::Confirmed { height, .. } => {
+ match chain_position {
+ ChainPosition::Confirmed(a) => {
// https://github.com/bitcoin/bitcoin/blob/c5e67be03bb06a5d7885c55db1f016fbf2333fe3/src/validation.cpp#L373-L375
- spendable &=
- (current_height.saturating_sub(height)) >= COINBASE_MATURITY;
+ spendable &= (current_height.saturating_sub(a.block_id.height))
+ >= COINBASE_MATURITY;
}
- ConfirmationTime::Unconfirmed { .. } => spendable = false,
+ ChainPosition::Unconfirmed { .. } => spendable = false,
}
}
}
outpoint: full_txo.outpoint,
txout: full_txo.txout,
is_spent: full_txo.spent_by.is_some(),
- confirmation_time: full_txo.chain_position.into(),
+ chain_position: full_txo.chain_position,
keychain,
derivation_index,
}
};
}
- use bdk_chain::ConfirmationTime;
use bitcoin::consensus::deserialize;
use bitcoin::hex::FromHex;
use bitcoin::TxOut;
txout: TxOut::NULL,
keychain: KeychainKind::External,
is_spent: false,
- confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 },
+ chain_position: chain::ChainPosition::Unconfirmed(0),
derivation_index: 0,
},
LocalOutput {
txout: TxOut::NULL,
keychain: KeychainKind::Internal,
is_spent: false,
- confirmation_time: ConfirmationTime::Confirmed {
- height: 32,
- time: 42,
- },
+ chain_position: chain::ChainPosition::Confirmed(chain::ConfirmationBlockTime {
+ block_id: chain::BlockId {
+ height: 32,
+ hash: bitcoin::BlockHash::all_zeros(),
+ },
+ confirmation_time: 42,
+ }),
derivation_index: 1,
},
]
#![allow(unused)]
-use bdk_chain::{tx_graph, BlockId, ConfirmationBlockTime, ConfirmationTime, TxGraph};
+use bdk_chain::{tx_graph, BlockId, ChainPosition, ConfirmationBlockTime, TxGraph};
use bdk_wallet::{CreateParams, KeychainKind, LocalOutput, Update, Wallet};
use bitcoin::{
hashes::Hash, transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, Transaction,
insert_anchor_from_conf(
&mut wallet,
tx0.compute_txid(),
- ConfirmationTime::Confirmed {
- height: 1_000,
- time: 100,
- },
+ ChainPosition::Confirmed(ConfirmationBlockTime {
+ block_id: BlockId {
+ height: 1_000,
+ hash: BlockHash::all_zeros(),
+ },
+ confirmation_time: 100,
+ }),
);
wallet.insert_tx(tx1.clone());
insert_anchor_from_conf(
&mut wallet,
tx1.compute_txid(),
- ConfirmationTime::Confirmed {
- height: 2_000,
- time: 200,
- },
+ ChainPosition::Confirmed(ConfirmationBlockTime {
+ block_id: BlockId {
+ height: 2_000,
+ hash: BlockHash::all_zeros(),
+ },
+ confirmation_time: 200,
+ }),
);
(wallet, tx1.compute_txid())
/// Simulates confirming a tx with `txid` at the specified `position` by inserting an anchor
/// at the lowest height in local chain that is greater or equal to `position`'s height,
/// assuming the confirmation time matches `ConfirmationTime::Confirmed`.
-pub fn insert_anchor_from_conf(wallet: &mut Wallet, txid: Txid, position: ConfirmationTime) {
- if let ConfirmationTime::Confirmed { height, time } = position {
- // anchor tx to checkpoint with lowest height that is >= position's height
- let anchor = wallet
- .local_chain()
- .range(height..)
- .last()
- .map(|anchor_cp| ConfirmationBlockTime {
- block_id: anchor_cp.block_id(),
- confirmation_time: time,
- })
- .expect("confirmation height cannot be greater than tip");
-
+pub fn insert_anchor_from_conf(
+ wallet: &mut Wallet,
+ txid: Txid,
+ position: ChainPosition<ConfirmationBlockTime>,
+) {
+ if let ChainPosition::Confirmed(anchor) = position {
wallet
.apply_update(Update {
tx_update: tx_graph::TxUpdate {
use anyhow::Context;
use assert_matches::assert_matches;
use bdk_chain::{tx_graph, COINBASE_MATURITY};
-use bdk_chain::{BlockId, ConfirmationTime};
+use bdk_chain::{BlockId, ChainPosition, ConfirmationBlockTime};
use bdk_wallet::coin_selection::{self, LargestFirstCoinSelection};
use bdk_wallet::descriptor::{calc_checksum, DescriptorError, IntoWalletDescriptor};
use bdk_wallet::error::CreateTxError;
mod common;
use common::*;
-fn receive_output(wallet: &mut Wallet, value: u64, height: ConfirmationTime) -> OutPoint {
+fn receive_output(
+ wallet: &mut Wallet,
+ value: u64,
+ height: ChainPosition<ConfirmationBlockTime>,
+) -> OutPoint {
let addr = wallet.next_unused_address(KeychainKind::External).address;
receive_output_to_address(wallet, addr, value, height)
}
wallet: &mut Wallet,
addr: Address,
value: u64,
- height: ConfirmationTime,
+ height: ChainPosition<ConfirmationBlockTime>,
) -> OutPoint {
let tx = Transaction {
version: transaction::Version::ONE,
wallet.insert_tx(tx);
match height {
- ConfirmationTime::Confirmed { .. } => {
+ ChainPosition::Confirmed { .. } => {
insert_anchor_from_conf(wallet, txid, height);
}
- ConfirmationTime::Unconfirmed { last_seen } => {
+ ChainPosition::Unconfirmed(last_seen) => {
insert_seen_at(wallet, txid, last_seen);
}
}
let latest_cp = wallet.latest_checkpoint();
let height = latest_cp.height();
let anchor = if height == 0 {
- ConfirmationTime::Unconfirmed { last_seen: 0 }
+ ChainPosition::Unconfirmed(0)
} else {
- ConfirmationTime::Confirmed { height, time: 0 }
+ ChainPosition::Confirmed(ConfirmationBlockTime {
+ block_id: latest_cp.block_id(),
+ confirmation_time: 0,
+ })
};
receive_output(wallet, value, anchor)
}
};
let txid = small_output_tx.compute_txid();
wallet.insert_tx(small_output_tx);
- insert_anchor_from_conf(
- &mut wallet,
- txid,
- ConfirmationTime::Confirmed {
- height: 2000,
- time: 200,
- },
- );
+ let chain_position = ChainPosition::Confirmed(ConfirmationBlockTime {
+ block_id: wallet.latest_checkpoint().get(2000).unwrap().block_id(),
+ confirmation_time: 200,
+ });
+ insert_anchor_from_conf(&mut wallet, txid, chain_position);
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
};
let txid = small_output_tx.compute_txid();
wallet.insert_tx(small_output_tx.clone());
- insert_anchor_from_conf(
- &mut wallet,
- txid,
- ConfirmationTime::Confirmed {
- height: 2000,
- time: 200,
- },
- );
+ let chain_position = ChainPosition::Confirmed(ConfirmationBlockTime {
+ block_id: wallet.latest_checkpoint().get(2000).unwrap().block_id(),
+ confirmation_time: 200,
+ });
+ insert_anchor_from_conf(&mut wallet, txid, chain_position);
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.create_wallet_no_persist()
.unwrap();
// fund wallet
- receive_output(&mut wallet, amount, ConfirmationTime::unconfirmed(0));
+ receive_output(&mut wallet, amount, ChainPosition::Unconfirmed(0));
// create tx
let mut builder = wallet.build_tx();
builder.add_recipient(recipient.clone(), Amount::from_sat(test.to_send));
let txid = tx.compute_txid();
wallet.insert_tx(tx);
- insert_anchor_from_conf(
- &mut wallet,
- txid,
- ConfirmationTime::Confirmed {
- height: 42,
- time: 42_000,
- },
- );
+
+ let chain_position = ChainPosition::Confirmed(ConfirmationBlockTime {
+ block_id: wallet.latest_checkpoint().get(42).unwrap().block_id(),
+ confirmation_time: 42_000,
+ });
+ insert_anchor_from_conf(&mut wallet, txid, chain_position);
wallet.build_fee_bump(txid).unwrap().finish().unwrap();
}
}],
};
let txid = tx.compute_txid();
- let tip = wallet.latest_checkpoint().height();
wallet.insert_tx(tx.clone());
- insert_anchor_from_conf(
- &mut wallet,
- txid,
- ConfirmationTime::Confirmed {
- height: tip,
- time: 42_000,
- },
- );
+ let chain_position = ChainPosition::Confirmed(ConfirmationBlockTime {
+ block_id: wallet.latest_checkpoint().block_id(),
+ confirmation_time: 42_000,
+ });
+ insert_anchor_from_conf(&mut wallet, txid, chain_position);
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
value: Amount::from_sat(25_000),
}],
};
- let position: ConfirmationTime = wallet
+ let position: ChainPosition<ConfirmationBlockTime> = wallet
.transactions()
.last()
.unwrap()
.chain_position
- .cloned()
- .into();
+ .cloned();
wallet.insert_tx(init_tx.clone());
insert_anchor_from_conf(&mut wallet, init_tx.compute_txid(), position);
}],
};
let txid = init_tx.compute_txid();
- let pos: ConfirmationTime = wallet
+ let pos: ChainPosition<ConfirmationBlockTime> = wallet
.transactions()
.last()
.unwrap()
.chain_position
- .cloned()
- .into();
+ .cloned();
wallet.insert_tx(init_tx);
insert_anchor_from_conf(&mut wallet, txid, pos);
let psbt = builder.finish().unwrap();
// Now we receive one transaction with 0 confirmations. We won't be able to use that for
// fee bumping, as it's still unconfirmed!
- receive_output(
- &mut wallet,
- 25_000,
- ConfirmationTime::Unconfirmed { last_seen: 0 },
- );
+ receive_output(&mut wallet, 25_000, ChainPosition::Unconfirmed(0));
let mut tx = psbt.extract_tx().expect("failed to extract tx");
let txid = tx.compute_txid();
for txin in &mut tx.input {
.assume_checked();
// We receive a tx with 0 confirmations, which will be used as an input
// in the drain tx.
- receive_output(&mut wallet, 25_000, ConfirmationTime::unconfirmed(0));
+ receive_output(&mut wallet, 25_000, ChainPosition::Unconfirmed(0));
let mut builder = wallet.build_tx();
builder.drain_wallet().drain_to(addr.script_pubkey());
let psbt = builder.finish().unwrap();
.unwrap();
let confirmation_height = 5;
- wallet
- .insert_checkpoint(BlockId {
- height: confirmation_height,
- hash: BlockHash::all_zeros(),
- })
- .unwrap();
+ let confirmation_block_id = BlockId {
+ height: confirmation_height,
+ hash: BlockHash::all_zeros(),
+ };
+ wallet.insert_checkpoint(confirmation_block_id).unwrap();
let coinbase_tx = Transaction {
version: transaction::Version::ONE,
lock_time: absolute::LockTime::ZERO,
};
let txid = coinbase_tx.compute_txid();
wallet.insert_tx(coinbase_tx);
- insert_anchor_from_conf(
- &mut wallet,
- txid,
- ConfirmationTime::Confirmed {
- height: confirmation_height,
- time: 30_000,
- },
- );
+ let chain_position = ChainPosition::Confirmed(ConfirmationBlockTime {
+ block_id: confirmation_block_id,
+ confirmation_time: 30_000,
+ });
+ insert_anchor_from_conf(&mut wallet, txid, chain_position);
let not_yet_mature_time = confirmation_height + COINBASE_MATURITY - 1;
let maturity_time = confirmation_height + COINBASE_MATURITY;
.last()
.unwrap()
.address;
- let _outpoint = receive_output_to_address(
- &mut wallet,
- addr,
- 8000,
- ConfirmationTime::Confirmed {
- height: 2000,
- time: 0,
+ let chain_position = ChainPosition::Confirmed(ConfirmationBlockTime {
+ block_id: BlockId {
+ height: 8000,
+ hash: BlockHash::all_zeros(),
},
- );
+ confirmation_time: 0,
+ });
+ let _outpoint = receive_output_to_address(&mut wallet, addr, 8000, chain_position);
assert_eq!(wallet.balance().confirmed, Amount::from_sat(58000));
}
.unwrap();
assert_eq!(wallet.keychains().count(), 1);
let amt = Amount::from_sat(5_000);
- receive_output(
- &mut wallet,
- 2 * amt.to_sat(),
- ConfirmationTime::Unconfirmed { last_seen: 2 },
- );
+ receive_output(&mut wallet, 2 * amt.to_sat(), ChainPosition::Unconfirmed(2));
// create spend tx that produces a change output
let addr = Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")
.unwrap()
#[test]
fn test_transactions_sort_by() {
let (mut wallet, _txid) = get_funded_wallet_wpkh();
- receive_output(&mut wallet, 25_000, ConfirmationTime::unconfirmed(0));
+ receive_output(&mut wallet, 25_000, ChainPosition::Unconfirmed(0));
// sort by chain position, unconfirmed then confirmed by descending block height
let sorted_txs: Vec<WalletTx> =