#[cfg(feature = "std")]
impl std::error::Error for NewOrLoadError {}
-/// An error that may occur when inserting a transaction into [`Wallet`].
-#[derive(Debug)]
-pub enum InsertTxError {
- /// The error variant that occurs when the caller attempts to insert a transaction with a
- /// confirmation height that is greater than the internal chain tip.
- ConfirmationHeightCannotBeGreaterThanTip {
- /// The internal chain's tip height.
- tip_height: u32,
- /// The introduced transaction's confirmation height.
- tx_height: u32,
- },
-}
-
-impl fmt::Display for InsertTxError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- InsertTxError::ConfirmationHeightCannotBeGreaterThanTip {
- tip_height,
- tx_height,
- } => {
- write!(f, "cannot insert tx with confirmation height ({}) higher than internal tip height ({})", tx_height, tip_height)
- }
- }
- }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for InsertTxError {}
-
/// An error that may occur when applying a block to [`Wallet`].
#[derive(Debug)]
pub enum ApplyBlockError {
self.stage.append(additions.into());
}
+ /// Inserts an `anchor` for a transaction with the given `txid`.
+ ///
+ /// This stages the changes, you must persist them later.
+ pub fn insert_anchor(&mut self, txid: Txid, anchor: ConfirmationTimeHeightAnchor) {
+ let indexed_graph_changeset = self.indexed_graph.insert_anchor(txid, anchor);
+ self.stage.append(indexed_graph_changeset.into());
+ }
+
+ /// Inserts a unix timestamp of when a transaction is seen in the mempool.
+ ///
+ /// This is used for transaction conflict resolution where the transaction with the
+ /// later last-seen is prioritized. This stages the changes, you must persist them later.
+ pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) {
+ let indexed_graph_changeset = self.indexed_graph.insert_seen_at(txid, seen_at);
+ self.stage.append(indexed_graph_changeset.into());
+ }
+
/// Calculates the fee of a given transaction. Returns [`Amount::ZERO`] if `tx` is a coinbase transaction.
///
/// To calculate the fee for a [`Transaction`] with inputs not owned by this wallet you must
/// Add a transaction to the wallet's internal view of the chain. This stages the change,
/// you must persist it later.
///
- /// Returns whether anything changed with the transaction insertion (e.g. `false` if the
- /// transaction was already inserted at the same position).
+ /// This method inserts the given `tx` and returns whether anything changed after insertion,
+ /// which will be false if the same transaction already exists in the wallet's transaction
+ /// graph. Any changes are staged but not committed.
///
- /// A `tx` can be rejected if `position` has a height greater than the [`latest_checkpoint`].
- /// Therefore you should use [`insert_checkpoint`] to insert new checkpoints before manually
- /// inserting new transactions.
+ /// # Note
///
- /// **WARNING**: If `position` is confirmed, we anchor the `tx` to the lowest checkpoint that
- /// is >= the `position`'s height. The caller is responsible for ensuring the `tx` exists in our
- /// local view of the best chain's history.
- ///
- /// You must persist the changes resulting from one or more calls to this method if you need
- /// the inserted tx to be reloaded after closing the wallet.
- ///
- /// [`commit`]: Self::commit
- /// [`latest_checkpoint`]: Self::latest_checkpoint
- /// [`insert_checkpoint`]: Self::insert_checkpoint
- pub fn insert_tx(
- &mut self,
- tx: Transaction,
- position: ConfirmationTime,
- ) -> Result<bool, InsertTxError> {
- let (anchor, last_seen) = match position {
- ConfirmationTime::Confirmed { height, time } => {
- // anchor tx to checkpoint with lowest height that is >= position's height
- let anchor = self
- .chain
- .range(height..)
- .last()
- .ok_or(InsertTxError::ConfirmationHeightCannotBeGreaterThanTip {
- tip_height: self.chain.tip().height(),
- tx_height: height,
- })
- .map(|anchor_cp| ConfirmationTimeHeightAnchor {
- anchor_block: anchor_cp.block_id(),
- confirmation_height: height,
- confirmation_time: time,
- })?;
-
- (Some(anchor), None)
- }
- ConfirmationTime::Unconfirmed { last_seen } => (None, Some(last_seen)),
- };
-
+ /// By default the inserted `tx` won't be considered "canonical" because it's not known
+ /// whether the transaction exists in the best chain. To know whether it exists, the tx
+ /// must be broadcast to the network and the wallet synced via a chain source.
+ pub fn insert_tx(&mut self, tx: Transaction) -> bool {
let mut changeset = ChangeSet::default();
- let txid = tx.compute_txid();
changeset.append(self.indexed_graph.insert_tx(tx).into());
- if let Some(anchor) = anchor {
- changeset.append(self.indexed_graph.insert_anchor(txid, anchor).into());
- }
- if let Some(last_seen) = last_seen {
- changeset.append(self.indexed_graph.insert_seen_at(txid, last_seen).into());
- }
-
- let changed = !changeset.is_empty();
+ let ret = !changeset.is_empty();
self.stage.append(changeset);
- Ok(changed)
+ ret
}
/// Iterate over the transactions in the wallet.
macro_rules! doctest_wallet {
() => {{
use $crate::bitcoin::{BlockHash, Transaction, absolute, TxOut, Network, hashes::Hash};
- use $crate::chain::{ConfirmationTime, BlockId};
+ use $crate::chain::{ConfirmationTimeHeightAnchor, BlockId};
use $crate::{KeychainKind, wallet::Wallet};
let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)";
let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)";
script_pubkey: address.script_pubkey(),
}],
};
- let _ = wallet.insert_checkpoint(BlockId { height: 1_000, hash: BlockHash::all_zeros() });
- let _ = wallet.insert_tx(tx.clone(), ConfirmationTime::Confirmed {
- height: 500,
- time: 50_000
- });
+ let txid = tx.txid();
+ let block = BlockId { height: 1_000, hash: BlockHash::all_zeros() };
+ let _ = wallet.insert_checkpoint(block);
+ let _ = wallet.insert_tx(tx);
+ wallet
+ .insert_anchor(
+ txid,
+ ConfirmationTimeHeightAnchor {
+ confirmation_height: 500,
+ confirmation_time: 50_000,
+ anchor_block: block,
+ }
+ );
wallet
}}
#![allow(unused)]
use bdk_chain::indexed_tx_graph::Indexer;
-use bdk_chain::{BlockId, ConfirmationTime};
+use bdk_chain::{BlockId, ConfirmationTime, ConfirmationTimeHeightAnchor};
use bdk_wallet::{KeychainKind, LocalOutput, Wallet};
use bitcoin::hashes::Hash;
use bitcoin::{
hash: BlockHash::all_zeros(),
})
.unwrap();
- wallet
- .insert_tx(
- tx0,
- ConfirmationTime::Confirmed {
- height: 1_000,
- time: 100,
- },
- )
- .unwrap();
- wallet
- .insert_tx(
- tx1.clone(),
- ConfirmationTime::Confirmed {
- height: 2_000,
- time: 200,
- },
- )
- .unwrap();
+
+ wallet.insert_tx(tx0.clone());
+ insert_anchor_from_conf(
+ &mut wallet,
+ tx0.compute_txid(),
+ ConfirmationTime::Confirmed {
+ height: 1_000,
+ time: 100,
+ },
+ );
+
+ wallet.insert_tx(tx1.clone());
+ insert_anchor_from_conf(
+ &mut wallet,
+ tx1.compute_txid(),
+ ConfirmationTime::Confirmed {
+ height: 2_000,
+ time: 200,
+ },
+ );
(wallet, tx1.compute_txid())
}
let sat_kwu = (sat_vb * 250.0).ceil() as u64;
FeeRate::from_sat_per_kwu(sat_kwu)
}
+
+/// 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| ConfirmationTimeHeightAnchor {
+ anchor_block: anchor_cp.block_id(),
+ confirmation_height: height,
+ confirmation_time: time,
+ })
+ .expect("confirmation height cannot be greater than tip");
+
+ wallet.insert_anchor(txid, anchor);
+ }
+}
}],
};
- wallet.insert_tx(tx.clone(), height).unwrap();
+ let txid = tx.compute_txid();
+ wallet.insert_tx(tx);
- OutPoint {
- txid: tx.compute_txid(),
- vout: 0,
+ match height {
+ ConfirmationTime::Confirmed { .. } => {
+ insert_anchor_from_conf(wallet, txid, height);
+ }
+ ConfirmationTime::Unconfirmed { last_seen } => {
+ wallet.insert_seen_at(txid, last_seen);
+ }
}
+
+ OutPoint { txid, vout: 0 }
}
fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint {
version: transaction::Version::non_standard(0),
lock_time: absolute::LockTime::ZERO,
};
- wallet
- .insert_tx(
- small_output_tx.clone(),
- ConfirmationTime::Unconfirmed { last_seen: 0 },
- )
- .unwrap();
+ wallet.insert_tx(small_output_tx.clone());
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
lock_time: absolute::LockTime::ZERO,
};
- wallet
- .insert_tx(
- small_output_tx.clone(),
- ConfirmationTime::Unconfirmed { last_seen: 0 },
- )
- .unwrap();
+ wallet.insert_tx(small_output_tx.clone());
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
value: Amount::from_sat(50_000),
}],
};
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
+ wallet.insert_tx(tx);
let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap();
let root_id = external_policy.id;
let tx = psbt.extract_tx().expect("failed to extract tx");
let txid = tx.compute_txid();
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
+ wallet.insert_tx(tx);
wallet.build_fee_bump(txid).unwrap().finish().unwrap();
}
let tx = psbt.extract_tx().expect("failed to extract tx");
let txid = tx.compute_txid();
- wallet
- .insert_tx(
- tx,
- ConfirmationTime::Confirmed {
- height: 42,
- time: 42_000,
- },
- )
- .unwrap();
+ wallet.insert_tx(tx);
+ insert_anchor_from_conf(
+ &mut wallet,
+ txid,
+ ConfirmationTime::Confirmed {
+ height: 42,
+ time: 42_000,
+ },
+ );
wallet.build_fee_bump(txid).unwrap().finish().unwrap();
}
let tx = psbt.extract_tx().expect("failed to extract tx");
let txid = tx.compute_txid();
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
+ wallet.insert_tx(tx);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_rate(FeeRate::BROADCAST_MIN);
let tx = psbt.extract_tx().expect("failed to extract tx");
let txid = tx.compute_txid();
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
+ wallet.insert_tx(tx);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_absolute(Amount::from_sat(10));
let tx = psbt.extract_tx().expect("failed to extract tx");
let txid = tx.compute_txid();
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
+ wallet.insert_tx(tx);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_absolute(Amount::ZERO);
let tx = psbt.extract_tx().expect("failed to extract tx");
let txid = tx.compute_txid();
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
+ wallet.insert_tx(tx);
let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb
let mut builder = wallet.build_fee_bump(txid).unwrap();
let original_sent_received = wallet.sent_and_received(&tx);
let original_fee = check_fee!(wallet, psbt);
let txid = tx.compute_txid();
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
+ wallet.insert_tx(tx);
let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb
let mut builder = wallet.build_fee_bump(txid).unwrap();
let tx = psbt.extract_tx().expect("failed to extract tx");
let original_sent_received = wallet.sent_and_received(&tx);
let txid = tx.compute_txid();
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
+ wallet.insert_tx(tx);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder
value: Amount::from_sat(25_000),
}],
};
- wallet
- .insert_tx(
- tx.clone(),
- ConfirmationTime::Confirmed {
- height: wallet.latest_checkpoint().height(),
- time: 42_000,
- },
- )
- .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 addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
let original_sent_received = wallet.sent_and_received(&tx);
let txid = tx.compute_txid();
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
+ wallet.insert_tx(tx);
assert_eq!(original_sent_received.0, Amount::from_sat(25_000));
// for the new feerate, it should be enough to reduce the output, but since we specify
value: Amount::from_sat(25_000),
}],
};
- wallet
- .insert_tx(
- init_tx.clone(),
- wallet
- .transactions()
- .last()
- .unwrap()
- .chain_position
- .cloned()
- .into(),
- )
- .unwrap();
+ let position: ConfirmationTime = wallet
+ .transactions()
+ .last()
+ .unwrap()
+ .chain_position
+ .cloned()
+ .into();
+
+ wallet.insert_tx(init_tx.clone());
+ insert_anchor_from_conf(&mut wallet, init_tx.compute_txid(), position);
+
let outpoint = OutPoint {
txid: init_tx.compute_txid(),
vout: 0,
let tx = psbt.extract_tx().expect("failed to extract tx");
let original_sent_received = wallet.sent_and_received(&tx);
let txid = tx.compute_txid();
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
+ wallet.insert_tx(tx);
assert_eq!(original_sent_received.0, Amount::from_sat(25_000));
let mut builder = wallet.build_fee_bump(txid).unwrap();
value: Amount::from_sat(25_000),
}],
};
- let pos = wallet
+ let txid = init_tx.compute_txid();
+ let pos: ConfirmationTime = wallet
.transactions()
.last()
.unwrap()
.chain_position
.cloned()
.into();
- wallet.insert_tx(init_tx, pos).unwrap();
+ wallet.insert_tx(init_tx);
+ insert_anchor_from_conf(&mut wallet, txid, pos);
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
let tx = psbt.extract_tx().expect("failed to extract tx");
let original_details = wallet.sent_and_received(&tx);
let txid = tx.compute_txid();
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
+ wallet.insert_tx(tx);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(50));
let tx = psbt.extract_tx().expect("failed to extract tx");
let original_sent_received = wallet.sent_and_received(&tx);
let txid = tx.compute_txid();
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
+ wallet.insert_tx(tx);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_absolute(Amount::from_sat(6_000));
let tx = psbt.extract_tx().expect("failed to extract tx");
let txid = tx.compute_txid();
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
+ wallet.insert_tx(tx);
// Now bump the fees, the wallet should add an extra input and a change output, and leave
// the original output untouched.
assert_eq!(tx.input.len(), 1);
assert_eq!(tx.output.len(), 2);
let txid = tx.compute_txid();
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
+ wallet.insert_tx(tx);
let mut builder = wallet.build_fee_bump(txid).unwrap();
// We set a fee high enough that during rbf we are forced to add
for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
}
- wallet
- .insert_tx(tx.clone(), ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
+ wallet.insert_tx(tx.clone());
// the new fee_rate is low enough that just reducing the change would be fine, but we force
// the addition of an extra input with `add_utxo()`
let mut builder = wallet.build_fee_bump(txid).unwrap();
for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
}
- wallet
- .insert_tx(tx.clone(), ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
+ wallet.insert_tx(tx.clone());
// the new fee_rate is low enough that just reducing the change would be fine, but we force
// the addition of an extra input with `add_utxo()`
for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
}
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
+ wallet.insert_tx(tx);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(25));
builder.finish().unwrap();
for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
}
- wallet
- .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
- .unwrap();
+ wallet.insert_tx(tx);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder
value: Amount::from_sat(25_000),
}],
};
- wallet
- .insert_tx(
- coinbase_tx,
- ConfirmationTime::Confirmed {
- height: confirmation_height,
- time: 30_000,
- },
- )
- .unwrap();
+ 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 not_yet_mature_time = confirmation_height + COINBASE_MATURITY - 1;
let maturity_time = confirmation_height + COINBASE_MATURITY;