Merge bitcoindevkit/bdk#1416: [chain] Change tx_last_seen to Option<u64>
af75817d4bedbb5e148812d1073fd0729a23358b ref(tx_graph): Change last_seen to `HashMap<Txid, u64>` (valued mammal) 6204d2c766f968af6b63c664dda61fa8fc897e85 feat(tx_graph): Add method `txs_with_no_anchor_or_last_seen` (valued mammal) 496601b8b148afd2199a530d40edeafcb7967d46 test(tx_graph): Add test for `list_canonical_txs` (valued mammal) c4057297a96ccbd49d984b7139994b801c2120dc wallet: delete method `insert_anchor` (valued mammal) b34790c6b6d612661a4595bf10910294af862323 ref(tx_graph)!: Rename `list_chain_txs` to `list_canonical_txs` (valued mammal) 2ce4bb4dfc4f7b779a815eca45a3f6cee3d6c4e0 test(indexed_tx_graph): Add test_get_chain_position (valued mammal) 36f58870cb6eb24fe8c50ba4cf3ede910dd11fe8 test(wallet): Add test_insert_tx_balance_and_utxos (valued mammal) bbc19c3536b25c78be8b5f3fe0cd9810aa679742 fix(tx_graph)!: Change tx_last_seen to `Option<u64>` (valued mammal) 324eeb3eb4e5231c6a81e6d197df788ad08b23a8 fix(wallet)!: Rework `Wallet::insert_tx` to no longer insert anchors (valued mammal) Pull request description: The PR changes the type of last_seen to `Option<u64>` for `txs` member of `TxGraph`. This fixes an issue where unbroadcast and otherwise non-canonical transactions were returned from methods `list_chain_txs` and `Wallet::transactions` because every new tx inserted had a last_seen of 0 making it appear unconfirmed. fixes #1446 fixes #1396 ### Notes to the reviewers ### Changelog notice Changed - Member `last_seen_unconfirmed` of `TxNode` is changed to `Option<u64>` - Renamed `TxGraph` method `list_chain_txs` to `list_canonical_txs` - Changed `Wallet::insert_tx` to take a single `tx: Transaction` as parameter Added - Add method `txs_with_no_anchor_or_last_seen` for `TxGraph` - Add method `unbroadcast_transactions` for `Wallet` ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) * [x] I ran `cargo fmt` and `cargo clippy` before committing #### Bugfixes: * [x] This pull request breaks the existing API * [x] I've added tests to reproduce the issue which are now passing * [x] I'm linking the issue being fixed by this PR ACKs for top commit: notmandatory: Re ACK af75817d4bedbb5e148812d1073fd0729a23358b Tree-SHA512: e664b3b49e2f547873923f15dffbbc7fa032b6240e5b856b180e9e26123ca141864d10448912dc4a31bbb200c75bef4251a910a4330dac17ee6841b564612d13
This commit is contained in:
commit
a112b4d97c
@ -392,7 +392,6 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
|||||||
get_balance(&recv_chain, &recv_graph)?,
|
get_balance(&recv_chain, &recv_graph)?,
|
||||||
Balance {
|
Balance {
|
||||||
confirmed: SEND_AMOUNT * (ADDITIONAL_COUNT - reorg_count) as u64,
|
confirmed: SEND_AMOUNT * (ADDITIONAL_COUNT - reorg_count) as u64,
|
||||||
trusted_pending: SEND_AMOUNT * reorg_count as u64,
|
|
||||||
..Balance::default()
|
..Balance::default()
|
||||||
},
|
},
|
||||||
"reorg_count: {}",
|
"reorg_count: {}",
|
||||||
|
@ -109,10 +109,11 @@ use core::{
|
|||||||
/// [module-level documentation]: crate::tx_graph
|
/// [module-level documentation]: crate::tx_graph
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct TxGraph<A = ()> {
|
pub struct TxGraph<A = ()> {
|
||||||
// all transactions that the graph is aware of in format: `(tx_node, tx_anchors, tx_last_seen)`
|
// all transactions that the graph is aware of in format: `(tx_node, tx_anchors)`
|
||||||
txs: HashMap<Txid, (TxNodeInternal, BTreeSet<A>, u64)>,
|
txs: HashMap<Txid, (TxNodeInternal, BTreeSet<A>)>,
|
||||||
spends: BTreeMap<OutPoint, HashSet<Txid>>,
|
spends: BTreeMap<OutPoint, HashSet<Txid>>,
|
||||||
anchors: BTreeSet<(A, Txid)>,
|
anchors: BTreeSet<(A, Txid)>,
|
||||||
|
last_seen: HashMap<Txid, u64>,
|
||||||
|
|
||||||
// This atrocity exists so that `TxGraph::outspends()` can return a reference.
|
// This atrocity exists so that `TxGraph::outspends()` can return a reference.
|
||||||
// FIXME: This can be removed once `HashSet::new` is a const fn.
|
// FIXME: This can be removed once `HashSet::new` is a const fn.
|
||||||
@ -125,6 +126,7 @@ impl<A> Default for TxGraph<A> {
|
|||||||
txs: Default::default(),
|
txs: Default::default(),
|
||||||
spends: Default::default(),
|
spends: Default::default(),
|
||||||
anchors: Default::default(),
|
anchors: Default::default(),
|
||||||
|
last_seen: Default::default(),
|
||||||
empty_outspends: Default::default(),
|
empty_outspends: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -140,7 +142,7 @@ pub struct TxNode<'a, T, A> {
|
|||||||
/// The blocks that the transaction is "anchored" in.
|
/// The blocks that the transaction is "anchored" in.
|
||||||
pub anchors: &'a BTreeSet<A>,
|
pub anchors: &'a BTreeSet<A>,
|
||||||
/// The last-seen unix timestamp of the transaction as unconfirmed.
|
/// The last-seen unix timestamp of the transaction as unconfirmed.
|
||||||
pub last_seen_unconfirmed: u64,
|
pub last_seen_unconfirmed: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, T, A> Deref for TxNode<'a, T, A> {
|
impl<'a, T, A> Deref for TxNode<'a, T, A> {
|
||||||
@ -210,7 +212,7 @@ impl<A> TxGraph<A> {
|
|||||||
///
|
///
|
||||||
/// This includes txouts of both full transactions as well as floating transactions.
|
/// This includes txouts of both full transactions as well as floating transactions.
|
||||||
pub fn all_txouts(&self) -> impl Iterator<Item = (OutPoint, &TxOut)> {
|
pub fn all_txouts(&self) -> impl Iterator<Item = (OutPoint, &TxOut)> {
|
||||||
self.txs.iter().flat_map(|(txid, (tx, _, _))| match tx {
|
self.txs.iter().flat_map(|(txid, (tx, _))| match tx {
|
||||||
TxNodeInternal::Whole(tx) => tx
|
TxNodeInternal::Whole(tx) => tx
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.output
|
.output
|
||||||
@ -232,7 +234,7 @@ impl<A> TxGraph<A> {
|
|||||||
pub fn floating_txouts(&self) -> impl Iterator<Item = (OutPoint, &TxOut)> {
|
pub fn floating_txouts(&self) -> impl Iterator<Item = (OutPoint, &TxOut)> {
|
||||||
self.txs
|
self.txs
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(txid, (tx_node, _, _))| match tx_node {
|
.filter_map(|(txid, (tx_node, _))| match tx_node {
|
||||||
TxNodeInternal::Whole(_) => None,
|
TxNodeInternal::Whole(_) => None,
|
||||||
TxNodeInternal::Partial(txouts) => Some(
|
TxNodeInternal::Partial(txouts) => Some(
|
||||||
txouts
|
txouts
|
||||||
@ -247,17 +249,30 @@ impl<A> TxGraph<A> {
|
|||||||
pub fn full_txs(&self) -> impl Iterator<Item = TxNode<'_, Arc<Transaction>, A>> {
|
pub fn full_txs(&self) -> impl Iterator<Item = TxNode<'_, Arc<Transaction>, A>> {
|
||||||
self.txs
|
self.txs
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(&txid, (tx, anchors, last_seen))| match tx {
|
.filter_map(|(&txid, (tx, anchors))| match tx {
|
||||||
TxNodeInternal::Whole(tx) => Some(TxNode {
|
TxNodeInternal::Whole(tx) => Some(TxNode {
|
||||||
txid,
|
txid,
|
||||||
tx: tx.clone(),
|
tx: tx.clone(),
|
||||||
anchors,
|
anchors,
|
||||||
last_seen_unconfirmed: *last_seen,
|
last_seen_unconfirmed: self.last_seen.get(&txid).copied(),
|
||||||
}),
|
}),
|
||||||
TxNodeInternal::Partial(_) => None,
|
TxNodeInternal::Partial(_) => None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Iterate over graph transactions with no anchors or last-seen.
|
||||||
|
pub fn txs_with_no_anchor_or_last_seen(
|
||||||
|
&self,
|
||||||
|
) -> impl Iterator<Item = TxNode<'_, Arc<Transaction>, A>> {
|
||||||
|
self.full_txs().filter_map(|tx| {
|
||||||
|
if tx.anchors.is_empty() && tx.last_seen_unconfirmed.is_none() {
|
||||||
|
Some(tx)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Get a transaction by txid. This only returns `Some` for full transactions.
|
/// Get a transaction by txid. This only returns `Some` for full transactions.
|
||||||
///
|
///
|
||||||
/// Refer to [`get_txout`] for getting a specific [`TxOut`].
|
/// Refer to [`get_txout`] for getting a specific [`TxOut`].
|
||||||
@ -270,11 +285,11 @@ impl<A> TxGraph<A> {
|
|||||||
/// Get a transaction node by txid. This only returns `Some` for full transactions.
|
/// Get a transaction node by txid. This only returns `Some` for full transactions.
|
||||||
pub fn get_tx_node(&self, txid: Txid) -> Option<TxNode<'_, Arc<Transaction>, A>> {
|
pub fn get_tx_node(&self, txid: Txid) -> Option<TxNode<'_, Arc<Transaction>, A>> {
|
||||||
match &self.txs.get(&txid)? {
|
match &self.txs.get(&txid)? {
|
||||||
(TxNodeInternal::Whole(tx), anchors, last_seen) => Some(TxNode {
|
(TxNodeInternal::Whole(tx), anchors) => Some(TxNode {
|
||||||
txid,
|
txid,
|
||||||
tx: tx.clone(),
|
tx: tx.clone(),
|
||||||
anchors,
|
anchors,
|
||||||
last_seen_unconfirmed: *last_seen,
|
last_seen_unconfirmed: self.last_seen.get(&txid).copied(),
|
||||||
}),
|
}),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
@ -504,7 +519,6 @@ impl<A: Clone + Ord> TxGraph<A> {
|
|||||||
(
|
(
|
||||||
TxNodeInternal::Partial([(outpoint.vout, txout)].into()),
|
TxNodeInternal::Partial([(outpoint.vout, txout)].into()),
|
||||||
BTreeSet::new(),
|
BTreeSet::new(),
|
||||||
0,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
self.apply_update(update)
|
self.apply_update(update)
|
||||||
@ -518,7 +532,7 @@ impl<A: Clone + Ord> TxGraph<A> {
|
|||||||
let mut update = Self::default();
|
let mut update = Self::default();
|
||||||
update.txs.insert(
|
update.txs.insert(
|
||||||
tx.compute_txid(),
|
tx.compute_txid(),
|
||||||
(TxNodeInternal::Whole(tx), BTreeSet::new(), 0),
|
(TxNodeInternal::Whole(tx), BTreeSet::new()),
|
||||||
);
|
);
|
||||||
self.apply_update(update)
|
self.apply_update(update)
|
||||||
}
|
}
|
||||||
@ -559,8 +573,7 @@ impl<A: Clone + Ord> TxGraph<A> {
|
|||||||
/// [`update_last_seen_unconfirmed`]: Self::update_last_seen_unconfirmed
|
/// [`update_last_seen_unconfirmed`]: Self::update_last_seen_unconfirmed
|
||||||
pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet<A> {
|
pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet<A> {
|
||||||
let mut update = Self::default();
|
let mut update = Self::default();
|
||||||
let (_, _, update_last_seen) = update.txs.entry(txid).or_default();
|
update.last_seen.insert(txid, seen_at);
|
||||||
*update_last_seen = seen_at;
|
|
||||||
self.apply_update(update)
|
self.apply_update(update)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -607,7 +620,7 @@ impl<A: Clone + Ord> TxGraph<A> {
|
|||||||
.txs
|
.txs
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(
|
.filter_map(
|
||||||
|(&txid, (_, anchors, _))| {
|
|(&txid, (_, anchors))| {
|
||||||
if anchors.is_empty() {
|
if anchors.is_empty() {
|
||||||
Some(txid)
|
Some(txid)
|
||||||
} else {
|
} else {
|
||||||
@ -656,10 +669,10 @@ impl<A: Clone + Ord> TxGraph<A> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
match self.txs.get_mut(&txid) {
|
match self.txs.get_mut(&txid) {
|
||||||
Some((tx_node @ TxNodeInternal::Partial(_), _, _)) => {
|
Some((tx_node @ TxNodeInternal::Partial(_), _)) => {
|
||||||
*tx_node = TxNodeInternal::Whole(wrapped_tx.clone());
|
*tx_node = TxNodeInternal::Whole(wrapped_tx.clone());
|
||||||
}
|
}
|
||||||
Some((TxNodeInternal::Whole(tx), _, _)) => {
|
Some((TxNodeInternal::Whole(tx), _)) => {
|
||||||
debug_assert_eq!(
|
debug_assert_eq!(
|
||||||
tx.as_ref().compute_txid(),
|
tx.as_ref().compute_txid(),
|
||||||
txid,
|
txid,
|
||||||
@ -667,10 +680,8 @@ impl<A: Clone + Ord> TxGraph<A> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
self.txs.insert(
|
self.txs
|
||||||
txid,
|
.insert(txid, (TxNodeInternal::Whole(wrapped_tx), BTreeSet::new()));
|
||||||
(TxNodeInternal::Whole(wrapped_tx), BTreeSet::new(), 0),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -679,9 +690,8 @@ impl<A: Clone + Ord> TxGraph<A> {
|
|||||||
let tx_entry = self.txs.entry(outpoint.txid).or_default();
|
let tx_entry = self.txs.entry(outpoint.txid).or_default();
|
||||||
|
|
||||||
match tx_entry {
|
match tx_entry {
|
||||||
(TxNodeInternal::Whole(_), _, _) => { /* do nothing since we already have full tx */
|
(TxNodeInternal::Whole(_), _) => { /* do nothing since we already have full tx */ }
|
||||||
}
|
(TxNodeInternal::Partial(txouts), _) => {
|
||||||
(TxNodeInternal::Partial(txouts), _, _) => {
|
|
||||||
txouts.insert(outpoint.vout, txout);
|
txouts.insert(outpoint.vout, txout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -689,13 +699,13 @@ impl<A: Clone + Ord> TxGraph<A> {
|
|||||||
|
|
||||||
for (anchor, txid) in changeset.anchors {
|
for (anchor, txid) in changeset.anchors {
|
||||||
if self.anchors.insert((anchor.clone(), txid)) {
|
if self.anchors.insert((anchor.clone(), txid)) {
|
||||||
let (_, anchors, _) = self.txs.entry(txid).or_default();
|
let (_, anchors) = self.txs.entry(txid).or_default();
|
||||||
anchors.insert(anchor);
|
anchors.insert(anchor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (txid, new_last_seen) in changeset.last_seen {
|
for (txid, new_last_seen) in changeset.last_seen {
|
||||||
let (_, _, last_seen) = self.txs.entry(txid).or_default();
|
let last_seen = self.last_seen.entry(txid).or_default();
|
||||||
if new_last_seen > *last_seen {
|
if new_last_seen > *last_seen {
|
||||||
*last_seen = new_last_seen;
|
*last_seen = new_last_seen;
|
||||||
}
|
}
|
||||||
@ -709,11 +719,10 @@ impl<A: Clone + Ord> TxGraph<A> {
|
|||||||
pub(crate) fn determine_changeset(&self, update: TxGraph<A>) -> ChangeSet<A> {
|
pub(crate) fn determine_changeset(&self, update: TxGraph<A>) -> ChangeSet<A> {
|
||||||
let mut changeset = ChangeSet::<A>::default();
|
let mut changeset = ChangeSet::<A>::default();
|
||||||
|
|
||||||
for (&txid, (update_tx_node, _, update_last_seen)) in &update.txs {
|
for (&txid, (update_tx_node, _)) in &update.txs {
|
||||||
let prev_last_seen: u64 = match (self.txs.get(&txid), update_tx_node) {
|
match (self.txs.get(&txid), update_tx_node) {
|
||||||
(None, TxNodeInternal::Whole(update_tx)) => {
|
(None, TxNodeInternal::Whole(update_tx)) => {
|
||||||
changeset.txs.insert(update_tx.clone());
|
changeset.txs.insert(update_tx.clone());
|
||||||
0
|
|
||||||
}
|
}
|
||||||
(None, TxNodeInternal::Partial(update_txos)) => {
|
(None, TxNodeInternal::Partial(update_txos)) => {
|
||||||
changeset.txouts.extend(
|
changeset.txouts.extend(
|
||||||
@ -721,18 +730,13 @@ impl<A: Clone + Ord> TxGraph<A> {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|(&vout, txo)| (OutPoint::new(txid, vout), txo.clone())),
|
.map(|(&vout, txo)| (OutPoint::new(txid, vout), txo.clone())),
|
||||||
);
|
);
|
||||||
0
|
|
||||||
}
|
}
|
||||||
(Some((TxNodeInternal::Whole(_), _, last_seen)), _) => *last_seen,
|
(Some((TxNodeInternal::Whole(_), _)), _) => {}
|
||||||
(
|
(Some((TxNodeInternal::Partial(_), _)), TxNodeInternal::Whole(update_tx)) => {
|
||||||
Some((TxNodeInternal::Partial(_), _, last_seen)),
|
|
||||||
TxNodeInternal::Whole(update_tx),
|
|
||||||
) => {
|
|
||||||
changeset.txs.insert(update_tx.clone());
|
changeset.txs.insert(update_tx.clone());
|
||||||
*last_seen
|
|
||||||
}
|
}
|
||||||
(
|
(
|
||||||
Some((TxNodeInternal::Partial(txos), _, last_seen)),
|
Some((TxNodeInternal::Partial(txos), _)),
|
||||||
TxNodeInternal::Partial(update_txos),
|
TxNodeInternal::Partial(update_txos),
|
||||||
) => {
|
) => {
|
||||||
changeset.txouts.extend(
|
changeset.txouts.extend(
|
||||||
@ -741,12 +745,14 @@ impl<A: Clone + Ord> TxGraph<A> {
|
|||||||
.filter(|(vout, _)| !txos.contains_key(*vout))
|
.filter(|(vout, _)| !txos.contains_key(*vout))
|
||||||
.map(|(&vout, txo)| (OutPoint::new(txid, vout), txo.clone())),
|
.map(|(&vout, txo)| (OutPoint::new(txid, vout), txo.clone())),
|
||||||
);
|
);
|
||||||
*last_seen
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if *update_last_seen > prev_last_seen {
|
for (txid, update_last_seen) in update.last_seen {
|
||||||
changeset.last_seen.insert(txid, *update_last_seen);
|
let prev_last_seen = self.last_seen.get(&txid).copied();
|
||||||
|
if Some(update_last_seen) > prev_last_seen {
|
||||||
|
changeset.last_seen.insert(txid, update_last_seen);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -786,7 +792,7 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
chain_tip: BlockId,
|
chain_tip: BlockId,
|
||||||
txid: Txid,
|
txid: Txid,
|
||||||
) -> Result<Option<ChainPosition<&A>>, C::Error> {
|
) -> Result<Option<ChainPosition<&A>>, C::Error> {
|
||||||
let (tx_node, anchors, last_seen) = match self.txs.get(&txid) {
|
let (tx_node, anchors) = match self.txs.get(&txid) {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => return Ok(None),
|
None => return Ok(None),
|
||||||
};
|
};
|
||||||
@ -798,6 +804,13 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If no anchors are in best chain and we don't have a last_seen, we can return
|
||||||
|
// early because by definition the tx doesn't have a chain position.
|
||||||
|
let last_seen = match self.last_seen.get(&txid) {
|
||||||
|
Some(t) => *t,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
// The tx is not anchored to a block in the best chain, which means that it
|
// The tx is not anchored to a block in the best chain, which means that it
|
||||||
// might be in mempool, or it might have been dropped already.
|
// might be in mempool, or it might have been dropped already.
|
||||||
// Let's check conflicts to find out!
|
// Let's check conflicts to find out!
|
||||||
@ -884,7 +897,7 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
if conflicting_tx.last_seen_unconfirmed > tx_last_seen {
|
if conflicting_tx.last_seen_unconfirmed > tx_last_seen {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
if conflicting_tx.last_seen_unconfirmed == *last_seen
|
if conflicting_tx.last_seen_unconfirmed == Some(last_seen)
|
||||||
&& conflicting_tx.as_ref().compute_txid() > tx.as_ref().compute_txid()
|
&& conflicting_tx.as_ref().compute_txid() > tx.as_ref().compute_txid()
|
||||||
{
|
{
|
||||||
// Conflicting tx has priority if txid of conflicting tx > txid of original tx
|
// Conflicting tx has priority if txid of conflicting tx > txid of original tx
|
||||||
@ -893,7 +906,7 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Some(ChainPosition::Unconfirmed(*last_seen)))
|
Ok(Some(ChainPosition::Unconfirmed(last_seen)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the position of the transaction in `chain` with tip `chain_tip`.
|
/// Get the position of the transaction in `chain` with tip `chain_tip`.
|
||||||
@ -971,10 +984,10 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
/// If the [`ChainOracle`] implementation (`chain`) fails, an error will be returned with the
|
/// If the [`ChainOracle`] implementation (`chain`) fails, an error will be returned with the
|
||||||
/// returned item.
|
/// returned item.
|
||||||
///
|
///
|
||||||
/// If the [`ChainOracle`] is infallible, [`list_chain_txs`] can be used instead.
|
/// If the [`ChainOracle`] is infallible, [`list_canonical_txs`] can be used instead.
|
||||||
///
|
///
|
||||||
/// [`list_chain_txs`]: Self::list_chain_txs
|
/// [`list_canonical_txs`]: Self::list_canonical_txs
|
||||||
pub fn try_list_chain_txs<'a, C: ChainOracle + 'a>(
|
pub fn try_list_canonical_txs<'a, C: ChainOracle + 'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
chain: &'a C,
|
chain: &'a C,
|
||||||
chain_tip: BlockId,
|
chain_tip: BlockId,
|
||||||
@ -993,15 +1006,15 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
|
|
||||||
/// List graph transactions that are in `chain` with `chain_tip`.
|
/// List graph transactions that are in `chain` with `chain_tip`.
|
||||||
///
|
///
|
||||||
/// This is the infallible version of [`try_list_chain_txs`].
|
/// This is the infallible version of [`try_list_canonical_txs`].
|
||||||
///
|
///
|
||||||
/// [`try_list_chain_txs`]: Self::try_list_chain_txs
|
/// [`try_list_canonical_txs`]: Self::try_list_canonical_txs
|
||||||
pub fn list_chain_txs<'a, C: ChainOracle + 'a>(
|
pub fn list_canonical_txs<'a, C: ChainOracle + 'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
chain: &'a C,
|
chain: &'a C,
|
||||||
chain_tip: BlockId,
|
chain_tip: BlockId,
|
||||||
) -> impl Iterator<Item = CanonicalTx<'a, Arc<Transaction>, A>> {
|
) -> impl Iterator<Item = CanonicalTx<'a, Arc<Transaction>, A>> {
|
||||||
self.try_list_chain_txs(chain, chain_tip)
|
self.try_list_canonical_txs(chain, chain_tip)
|
||||||
.map(|r| r.expect("oracle is infallible"))
|
.map(|r| r.expect("oracle is infallible"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,9 +131,7 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>(
|
|||||||
for anchor in tx_tmp.anchors.iter() {
|
for anchor in tx_tmp.anchors.iter() {
|
||||||
let _ = graph.insert_anchor(tx.compute_txid(), anchor.clone());
|
let _ = graph.insert_anchor(tx.compute_txid(), anchor.clone());
|
||||||
}
|
}
|
||||||
if let Some(seen_at) = tx_tmp.last_seen {
|
let _ = graph.insert_seen_at(tx.compute_txid(), tx_tmp.last_seen.unwrap_or(0));
|
||||||
let _ = graph.insert_seen_at(tx.compute_txid(), seen_at);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
(graph, spk_index, tx_ids)
|
(graph, spk_index, tx_ids)
|
||||||
}
|
}
|
||||||
|
@ -116,8 +116,8 @@ fn insert_relevant_txs() {
|
|||||||
/// tx1: A Coinbase, sending 70000 sats to "trusted" address. [Block 0]
|
/// tx1: A Coinbase, sending 70000 sats to "trusted" address. [Block 0]
|
||||||
/// tx2: A external Receive, sending 30000 sats to "untrusted" address. [Block 1]
|
/// tx2: A external Receive, sending 30000 sats to "untrusted" address. [Block 1]
|
||||||
/// tx3: Internal Spend. Spends tx2 and returns change of 10000 to "trusted" address. [Block 2]
|
/// tx3: Internal Spend. Spends tx2 and returns change of 10000 to "trusted" address. [Block 2]
|
||||||
/// tx4: Mempool tx, sending 20000 sats to "trusted" address.
|
/// tx4: Mempool tx, sending 20000 sats to "untrusted" address.
|
||||||
/// tx5: Mempool tx, sending 15000 sats to "untested" address.
|
/// tx5: Mempool tx, sending 15000 sats to "trusted" address.
|
||||||
/// tx6: Complete unrelated tx. [Block 3]
|
/// tx6: Complete unrelated tx. [Block 3]
|
||||||
///
|
///
|
||||||
/// Different transactions are added via `insert_relevant_txs`.
|
/// Different transactions are added via `insert_relevant_txs`.
|
||||||
@ -160,7 +160,7 @@ fn test_list_owned_txouts() {
|
|||||||
let mut untrusted_spks: Vec<ScriptBuf> = Vec::new();
|
let mut untrusted_spks: Vec<ScriptBuf> = Vec::new();
|
||||||
|
|
||||||
{
|
{
|
||||||
// we need to scope here to take immutanble reference of the graph
|
// we need to scope here to take immutable reference of the graph
|
||||||
for _ in 0..10 {
|
for _ in 0..10 {
|
||||||
let ((_, script), _) = graph
|
let ((_, script), _) = graph
|
||||||
.index
|
.index
|
||||||
@ -226,7 +226,7 @@ fn test_list_owned_txouts() {
|
|||||||
..common::new_tx(0)
|
..common::new_tx(0)
|
||||||
};
|
};
|
||||||
|
|
||||||
// tx5 is spending tx3 and receiving change at trusted keychain, unconfirmed.
|
// tx5 is an external transaction receiving at trusted keychain, unconfirmed.
|
||||||
let tx5 = Transaction {
|
let tx5 = Transaction {
|
||||||
output: vec![TxOut {
|
output: vec![TxOut {
|
||||||
value: Amount::from_sat(15000),
|
value: Amount::from_sat(15000),
|
||||||
@ -239,7 +239,7 @@ fn test_list_owned_txouts() {
|
|||||||
let tx6 = common::new_tx(0);
|
let tx6 = common::new_tx(0);
|
||||||
|
|
||||||
// Insert transactions into graph with respective anchors
|
// Insert transactions into graph with respective anchors
|
||||||
// For unconfirmed txs we pass in `None`.
|
// Insert unconfirmed txs with a last_seen timestamp
|
||||||
|
|
||||||
let _ =
|
let _ =
|
||||||
graph.batch_insert_relevant([&tx1, &tx2, &tx3, &tx6].iter().enumerate().map(|(i, tx)| {
|
graph.batch_insert_relevant([&tx1, &tx2, &tx3, &tx6].iter().enumerate().map(|(i, tx)| {
|
||||||
@ -291,9 +291,6 @@ fn test_list_owned_txouts() {
|
|||||||
|_, spk: &Script| trusted_spks.contains(&spk.to_owned()),
|
|_, spk: &Script| trusted_spks.contains(&spk.to_owned()),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(txouts.len(), 5);
|
|
||||||
assert_eq!(utxos.len(), 4);
|
|
||||||
|
|
||||||
let confirmed_txouts_txid = txouts
|
let confirmed_txouts_txid = txouts
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(_, full_txout)| {
|
.filter_map(|(_, full_txout)| {
|
||||||
@ -359,29 +356,25 @@ fn test_list_owned_txouts() {
|
|||||||
balance,
|
balance,
|
||||||
) = fetch(0, &graph);
|
) = fetch(0, &graph);
|
||||||
|
|
||||||
|
// tx1 is a confirmed txout and is unspent
|
||||||
|
// tx4, tx5 are unconfirmed
|
||||||
assert_eq!(confirmed_txouts_txid, [tx1.compute_txid()].into());
|
assert_eq!(confirmed_txouts_txid, [tx1.compute_txid()].into());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
unconfirmed_txouts_txid,
|
unconfirmed_txouts_txid,
|
||||||
[
|
[tx4.compute_txid(), tx5.compute_txid()].into()
|
||||||
tx2.compute_txid(),
|
|
||||||
tx3.compute_txid(),
|
|
||||||
tx4.compute_txid(),
|
|
||||||
tx5.compute_txid()
|
|
||||||
]
|
|
||||||
.into()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(confirmed_utxos_txid, [tx1.compute_txid()].into());
|
assert_eq!(confirmed_utxos_txid, [tx1.compute_txid()].into());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
unconfirmed_utxos_txid,
|
unconfirmed_utxos_txid,
|
||||||
[tx3.compute_txid(), tx4.compute_txid(), tx5.compute_txid()].into()
|
[tx4.compute_txid(), tx5.compute_txid()].into()
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
balance,
|
balance,
|
||||||
Balance {
|
Balance {
|
||||||
immature: Amount::from_sat(70000), // immature coinbase
|
immature: Amount::from_sat(70000), // immature coinbase
|
||||||
trusted_pending: Amount::from_sat(25000), // tx3 + tx5
|
trusted_pending: Amount::from_sat(15000), // tx5
|
||||||
untrusted_pending: Amount::from_sat(20000), // tx4
|
untrusted_pending: Amount::from_sat(20000), // tx4
|
||||||
confirmed: Amount::ZERO // Nothing is confirmed yet
|
confirmed: Amount::ZERO // Nothing is confirmed yet
|
||||||
}
|
}
|
||||||
@ -405,23 +398,26 @@ fn test_list_owned_txouts() {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
unconfirmed_txouts_txid,
|
unconfirmed_txouts_txid,
|
||||||
[tx3.compute_txid(), tx4.compute_txid(), tx5.compute_txid()].into()
|
[tx4.compute_txid(), tx5.compute_txid()].into()
|
||||||
);
|
);
|
||||||
|
|
||||||
// tx2 doesn't get into confirmed utxos set
|
// tx2 gets into confirmed utxos set
|
||||||
assert_eq!(confirmed_utxos_txid, [tx1.compute_txid()].into());
|
assert_eq!(
|
||||||
|
confirmed_utxos_txid,
|
||||||
|
[tx1.compute_txid(), tx2.compute_txid()].into()
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
unconfirmed_utxos_txid,
|
unconfirmed_utxos_txid,
|
||||||
[tx3.compute_txid(), tx4.compute_txid(), tx5.compute_txid()].into()
|
[tx4.compute_txid(), tx5.compute_txid()].into()
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
balance,
|
balance,
|
||||||
Balance {
|
Balance {
|
||||||
immature: Amount::from_sat(70000), // immature coinbase
|
immature: Amount::from_sat(70000), // immature coinbase
|
||||||
trusted_pending: Amount::from_sat(25000), // tx3 + tx5
|
trusted_pending: Amount::from_sat(15000), // tx5
|
||||||
untrusted_pending: Amount::from_sat(20000), // tx4
|
untrusted_pending: Amount::from_sat(20000), // tx4
|
||||||
confirmed: Amount::ZERO // Nothing is confirmed yet
|
confirmed: Amount::from_sat(30_000) // tx2 got confirmed
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -477,6 +473,7 @@ fn test_list_owned_txouts() {
|
|||||||
balance,
|
balance,
|
||||||
) = fetch(98, &graph);
|
) = fetch(98, &graph);
|
||||||
|
|
||||||
|
// no change compared to block 2
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
confirmed_txouts_txid,
|
confirmed_txouts_txid,
|
||||||
[tx1.compute_txid(), tx2.compute_txid(), tx3.compute_txid()].into()
|
[tx1.compute_txid(), tx2.compute_txid(), tx3.compute_txid()].into()
|
||||||
@ -502,14 +499,14 @@ fn test_list_owned_txouts() {
|
|||||||
immature: Amount::from_sat(70000), // immature coinbase
|
immature: Amount::from_sat(70000), // immature coinbase
|
||||||
trusted_pending: Amount::from_sat(15000), // tx5
|
trusted_pending: Amount::from_sat(15000), // tx5
|
||||||
untrusted_pending: Amount::from_sat(20000), // tx4
|
untrusted_pending: Amount::from_sat(20000), // tx4
|
||||||
confirmed: Amount::from_sat(10000) // tx1 got matured
|
confirmed: Amount::from_sat(10000) // tx3 is confirmed
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// AT Block 99
|
// AT Block 99
|
||||||
{
|
{
|
||||||
let (_, _, _, _, balance) = fetch(100, &graph);
|
let (_, _, _, _, balance) = fetch(99, &graph);
|
||||||
|
|
||||||
// Coinbase maturity hits
|
// Coinbase maturity hits
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -523,3 +520,147 @@ fn test_list_owned_txouts() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Given a `LocalChain`, `IndexedTxGraph`, and a `Transaction`, when we insert some anchor
|
||||||
|
/// (possibly non-canonical) and/or a last-seen timestamp into the graph, we expect the
|
||||||
|
/// result of `get_chain_position` in these cases:
|
||||||
|
///
|
||||||
|
/// - tx with no anchors or last_seen has no `ChainPosition`
|
||||||
|
/// - tx with any last_seen will be `Unconfirmed`
|
||||||
|
/// - tx with an anchor in best chain will be `Confirmed`
|
||||||
|
/// - tx with an anchor not in best chain (no last_seen) has no `ChainPosition`
|
||||||
|
#[test]
|
||||||
|
fn test_get_chain_position() {
|
||||||
|
use bdk_chain::local_chain::CheckPoint;
|
||||||
|
use bdk_chain::BlockId;
|
||||||
|
use bdk_chain::SpkTxOutIndex;
|
||||||
|
|
||||||
|
struct TestCase<A> {
|
||||||
|
name: &'static str,
|
||||||
|
tx: Transaction,
|
||||||
|
anchor: Option<A>,
|
||||||
|
last_seen: Option<u64>,
|
||||||
|
exp_pos: Option<ChainPosition<A>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// addr: bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm
|
||||||
|
let spk = ScriptBuf::from_hex("0014c692ecf13534982a9a2834565cbd37add8027140").unwrap();
|
||||||
|
let mut graph = IndexedTxGraph::new({
|
||||||
|
let mut index = SpkTxOutIndex::default();
|
||||||
|
let _ = index.insert_spk(0u32, spk.clone());
|
||||||
|
index
|
||||||
|
});
|
||||||
|
|
||||||
|
// Anchors to test
|
||||||
|
let blocks = vec![block_id!(0, "g"), block_id!(1, "A"), block_id!(2, "B")];
|
||||||
|
|
||||||
|
let cp = CheckPoint::from_block_ids(blocks.clone()).unwrap();
|
||||||
|
let chain = LocalChain::from_tip(cp).unwrap();
|
||||||
|
|
||||||
|
// The test will insert a transaction into the indexed tx graph
|
||||||
|
// along with any anchors and timestamps, then check the value
|
||||||
|
// returned by `get_chain_position`.
|
||||||
|
fn run(
|
||||||
|
chain: &LocalChain,
|
||||||
|
graph: &mut IndexedTxGraph<BlockId, SpkTxOutIndex<u32>>,
|
||||||
|
test: TestCase<BlockId>,
|
||||||
|
) {
|
||||||
|
let TestCase {
|
||||||
|
name,
|
||||||
|
tx,
|
||||||
|
anchor,
|
||||||
|
last_seen,
|
||||||
|
exp_pos,
|
||||||
|
} = test;
|
||||||
|
|
||||||
|
// add data to graph
|
||||||
|
let txid = tx.compute_txid();
|
||||||
|
let _ = graph.insert_tx(tx);
|
||||||
|
if let Some(anchor) = anchor {
|
||||||
|
let _ = graph.insert_anchor(txid, anchor);
|
||||||
|
}
|
||||||
|
if let Some(seen_at) = last_seen {
|
||||||
|
let _ = graph.insert_seen_at(txid, seen_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check chain position
|
||||||
|
let res = graph
|
||||||
|
.graph()
|
||||||
|
.get_chain_position(chain, chain.tip().block_id(), txid);
|
||||||
|
assert_eq!(
|
||||||
|
res.map(ChainPosition::cloned),
|
||||||
|
exp_pos,
|
||||||
|
"failed test case: {name}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[
|
||||||
|
TestCase {
|
||||||
|
name: "tx no anchors or last_seen - no chain pos",
|
||||||
|
tx: Transaction {
|
||||||
|
output: vec![TxOut {
|
||||||
|
value: Amount::ONE_BTC,
|
||||||
|
script_pubkey: spk.clone(),
|
||||||
|
}],
|
||||||
|
..common::new_tx(0)
|
||||||
|
},
|
||||||
|
anchor: None,
|
||||||
|
last_seen: None,
|
||||||
|
exp_pos: None,
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "tx last_seen - unconfirmed",
|
||||||
|
tx: Transaction {
|
||||||
|
output: vec![TxOut {
|
||||||
|
value: Amount::ONE_BTC,
|
||||||
|
script_pubkey: spk.clone(),
|
||||||
|
}],
|
||||||
|
..common::new_tx(1)
|
||||||
|
},
|
||||||
|
anchor: None,
|
||||||
|
last_seen: Some(2),
|
||||||
|
exp_pos: Some(ChainPosition::Unconfirmed(2)),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "tx anchor in best chain - confirmed",
|
||||||
|
tx: Transaction {
|
||||||
|
output: vec![TxOut {
|
||||||
|
value: Amount::ONE_BTC,
|
||||||
|
script_pubkey: spk.clone(),
|
||||||
|
}],
|
||||||
|
..common::new_tx(2)
|
||||||
|
},
|
||||||
|
anchor: Some(blocks[1]),
|
||||||
|
last_seen: None,
|
||||||
|
exp_pos: Some(ChainPosition::Confirmed(blocks[1])),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "tx unknown anchor with last_seen - unconfirmed",
|
||||||
|
tx: Transaction {
|
||||||
|
output: vec![TxOut {
|
||||||
|
value: Amount::ONE_BTC,
|
||||||
|
script_pubkey: spk.clone(),
|
||||||
|
}],
|
||||||
|
..common::new_tx(3)
|
||||||
|
},
|
||||||
|
anchor: Some(block_id!(2, "B'")),
|
||||||
|
last_seen: Some(2),
|
||||||
|
exp_pos: Some(ChainPosition::Unconfirmed(2)),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "tx unknown anchor - no chain pos",
|
||||||
|
tx: Transaction {
|
||||||
|
output: vec![TxOut {
|
||||||
|
value: Amount::ONE_BTC,
|
||||||
|
script_pubkey: spk.clone(),
|
||||||
|
}],
|
||||||
|
..common::new_tx(4)
|
||||||
|
},
|
||||||
|
anchor: Some(block_id!(2, "B'")),
|
||||||
|
last_seen: None,
|
||||||
|
exp_pos: None,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|t| run(&chain, &mut graph, t));
|
||||||
|
}
|
||||||
|
@ -977,16 +977,6 @@ fn test_chain_spends() {
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
// Even if unconfirmed tx has a last_seen of 0, it can still be part of a chain spend.
|
|
||||||
assert_eq!(
|
|
||||||
graph.get_chain_spend(
|
|
||||||
&local_chain,
|
|
||||||
tip.block_id(),
|
|
||||||
OutPoint::new(tx_0.compute_txid(), 1)
|
|
||||||
),
|
|
||||||
Some((ChainPosition::Unconfirmed(0), tx_2.compute_txid())),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Mark the unconfirmed as seen and check correct ObservedAs status is returned.
|
// Mark the unconfirmed as seen and check correct ObservedAs status is returned.
|
||||||
let _ = graph.insert_seen_at(tx_2.compute_txid(), 1234567);
|
let _ = graph.insert_seen_at(tx_2.compute_txid(), 1234567);
|
||||||
|
|
||||||
@ -1099,10 +1089,10 @@ fn update_last_seen_unconfirmed() {
|
|||||||
let txid = tx.compute_txid();
|
let txid = tx.compute_txid();
|
||||||
|
|
||||||
// insert a new tx
|
// insert a new tx
|
||||||
// initially we have a last_seen of 0, and no anchors
|
// initially we have a last_seen of None and no anchors
|
||||||
let _ = graph.insert_tx(tx);
|
let _ = graph.insert_tx(tx);
|
||||||
let tx = graph.full_txs().next().unwrap();
|
let tx = graph.full_txs().next().unwrap();
|
||||||
assert_eq!(tx.last_seen_unconfirmed, 0);
|
assert_eq!(tx.last_seen_unconfirmed, None);
|
||||||
assert!(tx.anchors.is_empty());
|
assert!(tx.anchors.is_empty());
|
||||||
|
|
||||||
// higher timestamp should update last seen
|
// higher timestamp should update last seen
|
||||||
@ -1117,7 +1107,56 @@ fn update_last_seen_unconfirmed() {
|
|||||||
let _ = graph.insert_anchor(txid, ());
|
let _ = graph.insert_anchor(txid, ());
|
||||||
let changeset = graph.update_last_seen_unconfirmed(4);
|
let changeset = graph.update_last_seen_unconfirmed(4);
|
||||||
assert!(changeset.is_empty());
|
assert!(changeset.is_empty());
|
||||||
assert_eq!(graph.full_txs().next().unwrap().last_seen_unconfirmed, 2);
|
assert_eq!(
|
||||||
|
graph
|
||||||
|
.full_txs()
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.last_seen_unconfirmed
|
||||||
|
.unwrap(),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transactions_inserted_into_tx_graph_are_not_canonical_until_they_have_an_anchor_in_best_chain() {
|
||||||
|
let txs = vec![new_tx(0), new_tx(1)];
|
||||||
|
let txids: Vec<Txid> = txs.iter().map(Transaction::compute_txid).collect();
|
||||||
|
|
||||||
|
// graph
|
||||||
|
let mut graph = TxGraph::<BlockId>::new(txs);
|
||||||
|
let full_txs: Vec<_> = graph.full_txs().collect();
|
||||||
|
assert_eq!(full_txs.len(), 2);
|
||||||
|
let unseen_txs: Vec<_> = graph.txs_with_no_anchor_or_last_seen().collect();
|
||||||
|
assert_eq!(unseen_txs.len(), 2);
|
||||||
|
|
||||||
|
// chain
|
||||||
|
let blocks: BTreeMap<u32, BlockHash> = [(0, h!("g")), (1, h!("A")), (2, h!("B"))]
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
let chain = LocalChain::from_blocks(blocks).unwrap();
|
||||||
|
let canonical_txs: Vec<_> = graph
|
||||||
|
.list_canonical_txs(&chain, chain.tip().block_id())
|
||||||
|
.collect();
|
||||||
|
assert!(canonical_txs.is_empty());
|
||||||
|
|
||||||
|
// tx0 with seen_at should be returned by canonical txs
|
||||||
|
let _ = graph.insert_seen_at(txids[0], 2);
|
||||||
|
let mut canonical_txs = graph.list_canonical_txs(&chain, chain.tip().block_id());
|
||||||
|
assert_eq!(
|
||||||
|
canonical_txs.next().map(|tx| tx.tx_node.txid).unwrap(),
|
||||||
|
txids[0]
|
||||||
|
);
|
||||||
|
drop(canonical_txs);
|
||||||
|
|
||||||
|
// tx1 with anchor is also canonical
|
||||||
|
let _ = graph.insert_anchor(txids[1], block_id!(2, "B"));
|
||||||
|
let canonical_txids: Vec<_> = graph
|
||||||
|
.list_canonical_txs(&chain, chain.tip().block_id())
|
||||||
|
.map(|tx| tx.tx_node.txid)
|
||||||
|
.collect();
|
||||||
|
assert!(canonical_txids.contains(&txids[1]));
|
||||||
|
assert!(graph.txs_with_no_anchor_or_last_seen().next().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -15,7 +15,7 @@ struct Scenario<'a> {
|
|||||||
name: &'a str,
|
name: &'a str,
|
||||||
/// Transaction templates
|
/// Transaction templates
|
||||||
tx_templates: &'a [TxTemplate<'a, BlockId>],
|
tx_templates: &'a [TxTemplate<'a, BlockId>],
|
||||||
/// Names of txs that must exist in the output of `list_chain_txs`
|
/// Names of txs that must exist in the output of `list_canonical_txs`
|
||||||
exp_chain_txs: HashSet<&'a str>,
|
exp_chain_txs: HashSet<&'a str>,
|
||||||
/// Outpoints that must exist in the output of `filter_chain_txouts`
|
/// Outpoints that must exist in the output of `filter_chain_txouts`
|
||||||
exp_chain_txouts: HashSet<(&'a str, u32)>,
|
exp_chain_txouts: HashSet<(&'a str, u32)>,
|
||||||
@ -27,7 +27,7 @@ struct Scenario<'a> {
|
|||||||
|
|
||||||
/// This test ensures that [`TxGraph`] will reliably filter out irrelevant transactions when
|
/// This test ensures that [`TxGraph`] will reliably filter out irrelevant transactions when
|
||||||
/// presented with multiple conflicting transaction scenarios using the [`TxTemplate`] structure.
|
/// presented with multiple conflicting transaction scenarios using the [`TxTemplate`] structure.
|
||||||
/// This test also checks that [`TxGraph::list_chain_txs`], [`TxGraph::filter_chain_txouts`],
|
/// This test also checks that [`TxGraph::list_canonical_txs`], [`TxGraph::filter_chain_txouts`],
|
||||||
/// [`TxGraph::filter_chain_unspents`], and [`TxGraph::balance`] return correct data.
|
/// [`TxGraph::filter_chain_unspents`], and [`TxGraph::balance`] return correct data.
|
||||||
#[test]
|
#[test]
|
||||||
fn test_tx_conflict_handling() {
|
fn test_tx_conflict_handling() {
|
||||||
@ -597,7 +597,7 @@ fn test_tx_conflict_handling() {
|
|||||||
let (tx_graph, spk_index, exp_tx_ids) = init_graph(scenario.tx_templates.iter());
|
let (tx_graph, spk_index, exp_tx_ids) = init_graph(scenario.tx_templates.iter());
|
||||||
|
|
||||||
let txs = tx_graph
|
let txs = tx_graph
|
||||||
.list_chain_txs(&local_chain, chain_tip)
|
.list_canonical_txs(&local_chain, chain_tip)
|
||||||
.map(|tx| tx.tx_node.txid)
|
.map(|tx| tx.tx_node.txid)
|
||||||
.collect::<BTreeSet<_>>();
|
.collect::<BTreeSet<_>>();
|
||||||
let exp_txs = scenario
|
let exp_txs = scenario
|
||||||
@ -607,7 +607,7 @@ fn test_tx_conflict_handling() {
|
|||||||
.collect::<BTreeSet<_>>();
|
.collect::<BTreeSet<_>>();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
txs, exp_txs,
|
txs, exp_txs,
|
||||||
"\n[{}] 'list_chain_txs' failed",
|
"\n[{}] 'list_canonical_txs' failed",
|
||||||
scenario.name
|
scenario.name
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -198,17 +198,14 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
|||||||
.apply_update(update.chain_update)
|
.apply_update(update.chain_update)
|
||||||
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
|
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
|
||||||
|
|
||||||
// Check to see if a new anchor is added during current reorg.
|
// Check that no new anchors are added during current reorg.
|
||||||
if !initial_anchors.is_superset(update.graph_update.all_anchors()) {
|
assert!(initial_anchors.is_superset(update.graph_update.all_anchors()));
|
||||||
println!("New anchor added at reorg depth {}", depth);
|
|
||||||
}
|
|
||||||
let _ = recv_graph.apply_update(update.graph_update);
|
let _ = recv_graph.apply_update(update.graph_update);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
get_balance(&recv_chain, &recv_graph)?,
|
get_balance(&recv_chain, &recv_graph)?,
|
||||||
Balance {
|
Balance {
|
||||||
confirmed: SEND_AMOUNT * (REORG_COUNT - depth) as u64,
|
confirmed: SEND_AMOUNT * (REORG_COUNT - depth) as u64,
|
||||||
trusted_pending: SEND_AMOUNT * depth as u64,
|
|
||||||
..Balance::default()
|
..Balance::default()
|
||||||
},
|
},
|
||||||
"reorg_count: {}",
|
"reorg_count: {}",
|
||||||
|
@ -214,7 +214,7 @@ mod test {
|
|||||||
use core::str::FromStr;
|
use core::str::FromStr;
|
||||||
|
|
||||||
use crate::std::string::ToString;
|
use crate::std::string::ToString;
|
||||||
use bdk_chain::{BlockId, ConfirmationTime};
|
use bdk_chain::{BlockId, ConfirmationTimeHeightAnchor};
|
||||||
use bitcoin::hashes::Hash;
|
use bitcoin::hashes::Hash;
|
||||||
use bitcoin::{transaction, BlockHash, Network, Transaction};
|
use bitcoin::{transaction, BlockHash, Network, Transaction};
|
||||||
|
|
||||||
@ -222,6 +222,8 @@ mod test {
|
|||||||
use crate::wallet::Wallet;
|
use crate::wallet::Wallet;
|
||||||
|
|
||||||
fn get_test_wallet(descriptor: &str, change_descriptor: &str, network: Network) -> Wallet {
|
fn get_test_wallet(descriptor: &str, change_descriptor: &str, network: Network) -> Wallet {
|
||||||
|
use crate::wallet::Update;
|
||||||
|
use bdk_chain::TxGraph;
|
||||||
let mut wallet = Wallet::new(descriptor, change_descriptor, network).unwrap();
|
let mut wallet = Wallet::new(descriptor, change_descriptor, network).unwrap();
|
||||||
let transaction = Transaction {
|
let transaction = Transaction {
|
||||||
input: vec![],
|
input: vec![],
|
||||||
@ -229,22 +231,27 @@ mod test {
|
|||||||
version: transaction::Version::non_standard(0),
|
version: transaction::Version::non_standard(0),
|
||||||
lock_time: bitcoin::absolute::LockTime::ZERO,
|
lock_time: bitcoin::absolute::LockTime::ZERO,
|
||||||
};
|
};
|
||||||
|
let txid = transaction.compute_txid();
|
||||||
|
let block_id = BlockId {
|
||||||
|
height: 5001,
|
||||||
|
hash: BlockHash::all_zeros(),
|
||||||
|
};
|
||||||
|
wallet.insert_checkpoint(block_id).unwrap();
|
||||||
|
wallet.insert_tx(transaction);
|
||||||
|
let anchor = ConfirmationTimeHeightAnchor {
|
||||||
|
confirmation_height: 5000,
|
||||||
|
confirmation_time: 0,
|
||||||
|
anchor_block: block_id,
|
||||||
|
};
|
||||||
|
let mut graph = TxGraph::default();
|
||||||
|
let _ = graph.insert_anchor(txid, anchor);
|
||||||
wallet
|
wallet
|
||||||
.insert_checkpoint(BlockId {
|
.apply_update(Update {
|
||||||
height: 5001,
|
graph,
|
||||||
hash: BlockHash::all_zeros(),
|
..Default::default()
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
wallet
|
wallet
|
||||||
.insert_tx(
|
|
||||||
transaction,
|
|
||||||
ConfirmationTime::Confirmed {
|
|
||||||
height: 5000,
|
|
||||||
time: 0,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
wallet
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -27,7 +27,7 @@ use bdk_chain::{
|
|||||||
self, ApplyHeaderError, CannotConnectError, CheckPoint, CheckPointIter, LocalChain,
|
self, ApplyHeaderError, CannotConnectError, CheckPoint, CheckPointIter, LocalChain,
|
||||||
},
|
},
|
||||||
spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult},
|
spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult},
|
||||||
tx_graph::{CanonicalTx, TxGraph},
|
tx_graph::{CanonicalTx, TxGraph, TxNode},
|
||||||
Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeHeightAnchor, FullTxOut,
|
Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeHeightAnchor, FullTxOut,
|
||||||
Indexed, IndexedTxGraph,
|
Indexed, IndexedTxGraph,
|
||||||
};
|
};
|
||||||
@ -290,35 +290,6 @@ impl fmt::Display for NewOrLoadError {
|
|||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
impl std::error::Error for NewOrLoadError {}
|
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`].
|
/// An error that may occur when applying a block to [`Wallet`].
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ApplyBlockError {
|
pub enum ApplyBlockError {
|
||||||
@ -1085,63 +1056,21 @@ impl Wallet {
|
|||||||
/// Add a transaction to the wallet's internal view of the chain. This stages the change,
|
/// Add a transaction to the wallet's internal view of the chain. This stages the change,
|
||||||
/// you must persist it later.
|
/// you must persist it later.
|
||||||
///
|
///
|
||||||
/// Returns whether anything changed with the transaction insertion (e.g. `false` if the
|
/// This method inserts the given `tx` and returns whether anything changed after insertion,
|
||||||
/// transaction was already inserted at the same position).
|
/// 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`].
|
/// # Note
|
||||||
/// Therefore you should use [`insert_checkpoint`] to insert new checkpoints before manually
|
|
||||||
/// inserting new transactions.
|
|
||||||
///
|
///
|
||||||
/// **WARNING**: If `position` is confirmed, we anchor the `tx` to the lowest checkpoint that
|
/// By default the inserted `tx` won't be considered "canonical" because it's not known
|
||||||
/// is >= the `position`'s height. The caller is responsible for ensuring the `tx` exists in our
|
/// whether the transaction exists in the best chain. To know whether it exists, the tx
|
||||||
/// local view of the best chain's history.
|
/// must be broadcast to the network and the wallet synced via a chain source.
|
||||||
///
|
pub fn insert_tx(&mut self, tx: Transaction) -> bool {
|
||||||
/// 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)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut changeset = ChangeSet::default();
|
let mut changeset = ChangeSet::default();
|
||||||
let txid = tx.compute_txid();
|
|
||||||
changeset.append(self.indexed_graph.insert_tx(tx).into());
|
changeset.append(self.indexed_graph.insert_tx(tx).into());
|
||||||
if let Some(anchor) = anchor {
|
let ret = !changeset.is_empty();
|
||||||
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();
|
|
||||||
self.stage.append(changeset);
|
self.stage.append(changeset);
|
||||||
Ok(changed)
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterate over the transactions in the wallet.
|
/// Iterate over the transactions in the wallet.
|
||||||
@ -1151,7 +1080,7 @@ impl Wallet {
|
|||||||
{
|
{
|
||||||
self.indexed_graph
|
self.indexed_graph
|
||||||
.graph()
|
.graph()
|
||||||
.list_chain_txs(&self.chain, self.chain.tip().block_id())
|
.list_canonical_txs(&self.chain, self.chain.tip().block_id())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the balance, separated into available, trusted-pending, untrusted-pending and immature
|
/// Return the balance, separated into available, trusted-pending, untrusted-pending and immature
|
||||||
@ -2326,6 +2255,14 @@ impl Wallet {
|
|||||||
self.indexed_graph.graph()
|
self.indexed_graph.graph()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Iterate over transactions in the wallet that are unseen and unanchored likely
|
||||||
|
/// because they haven't been broadcast.
|
||||||
|
pub fn unbroadcast_transactions(
|
||||||
|
&self,
|
||||||
|
) -> impl Iterator<Item = TxNode<'_, Arc<Transaction>, ConfirmationTimeHeightAnchor>> {
|
||||||
|
self.tx_graph().txs_with_no_anchor_or_last_seen()
|
||||||
|
}
|
||||||
|
|
||||||
/// Get a reference to the inner [`KeychainTxOutIndex`].
|
/// Get a reference to the inner [`KeychainTxOutIndex`].
|
||||||
pub fn spk_index(&self) -> &KeychainTxOutIndex<KeychainKind> {
|
pub fn spk_index(&self) -> &KeychainTxOutIndex<KeychainKind> {
|
||||||
&self.indexed_graph.index
|
&self.indexed_graph.index
|
||||||
@ -2545,8 +2482,9 @@ macro_rules! floating_rate {
|
|||||||
macro_rules! doctest_wallet {
|
macro_rules! doctest_wallet {
|
||||||
() => {{
|
() => {{
|
||||||
use $crate::bitcoin::{BlockHash, Transaction, absolute, TxOut, Network, hashes::Hash};
|
use $crate::bitcoin::{BlockHash, Transaction, absolute, TxOut, Network, hashes::Hash};
|
||||||
use $crate::chain::{ConfirmationTime, BlockId};
|
use $crate::chain::{ConfirmationTimeHeightAnchor, BlockId, TxGraph};
|
||||||
use $crate::{KeychainKind, wallet::Wallet};
|
use $crate::wallet::{Update, Wallet};
|
||||||
|
use $crate::KeychainKind;
|
||||||
let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)";
|
let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)";
|
||||||
let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)";
|
let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)";
|
||||||
|
|
||||||
@ -2566,12 +2504,19 @@ macro_rules! doctest_wallet {
|
|||||||
script_pubkey: address.script_pubkey(),
|
script_pubkey: address.script_pubkey(),
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
let _ = wallet.insert_checkpoint(BlockId { height: 1_000, hash: BlockHash::all_zeros() });
|
let txid = tx.txid();
|
||||||
let _ = wallet.insert_tx(tx.clone(), ConfirmationTime::Confirmed {
|
let block = BlockId { height: 1_000, hash: BlockHash::all_zeros() };
|
||||||
height: 500,
|
let _ = wallet.insert_checkpoint(block);
|
||||||
time: 50_000
|
let _ = wallet.insert_tx(tx);
|
||||||
});
|
let anchor = ConfirmationTimeHeightAnchor {
|
||||||
|
confirmation_height: 500,
|
||||||
|
confirmation_time: 50_000,
|
||||||
|
anchor_block: block,
|
||||||
|
};
|
||||||
|
let mut graph = TxGraph::default();
|
||||||
|
let _ = graph.insert_anchor(txid, anchor);
|
||||||
|
let update = Update { graph, ..Default::default() };
|
||||||
|
wallet.apply_update(update).unwrap();
|
||||||
wallet
|
wallet
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
#![allow(unused)]
|
#![allow(unused)]
|
||||||
|
|
||||||
use bdk_chain::indexed_tx_graph::Indexer;
|
use bdk_chain::indexed_tx_graph::Indexer;
|
||||||
use bdk_chain::{BlockId, ConfirmationTime};
|
use bdk_chain::{BlockId, ConfirmationTime, ConfirmationTimeHeightAnchor, TxGraph};
|
||||||
|
use bdk_wallet::wallet::Update;
|
||||||
use bdk_wallet::{KeychainKind, LocalOutput, Wallet};
|
use bdk_wallet::{KeychainKind, LocalOutput, Wallet};
|
||||||
use bitcoin::hashes::Hash;
|
use bitcoin::hashes::Hash;
|
||||||
use bitcoin::{
|
use bitcoin::{
|
||||||
@ -77,24 +78,26 @@ pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet,
|
|||||||
hash: BlockHash::all_zeros(),
|
hash: BlockHash::all_zeros(),
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
wallet
|
|
||||||
.insert_tx(
|
wallet.insert_tx(tx0.clone());
|
||||||
tx0,
|
insert_anchor_from_conf(
|
||||||
ConfirmationTime::Confirmed {
|
&mut wallet,
|
||||||
height: 1_000,
|
tx0.compute_txid(),
|
||||||
time: 100,
|
ConfirmationTime::Confirmed {
|
||||||
},
|
height: 1_000,
|
||||||
)
|
time: 100,
|
||||||
.unwrap();
|
},
|
||||||
wallet
|
);
|
||||||
.insert_tx(
|
|
||||||
tx1.clone(),
|
wallet.insert_tx(tx1.clone());
|
||||||
ConfirmationTime::Confirmed {
|
insert_anchor_from_conf(
|
||||||
height: 2_000,
|
&mut wallet,
|
||||||
time: 200,
|
tx1.compute_txid(),
|
||||||
},
|
ConfirmationTime::Confirmed {
|
||||||
)
|
height: 2_000,
|
||||||
.unwrap();
|
time: 200,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
(wallet, tx1.compute_txid())
|
(wallet, tx1.compute_txid())
|
||||||
}
|
}
|
||||||
@ -192,3 +195,31 @@ pub fn feerate_unchecked(sat_vb: f64) -> FeeRate {
|
|||||||
let sat_kwu = (sat_vb * 250.0).ceil() as u64;
|
let sat_kwu = (sat_vb * 250.0).ceil() as u64;
|
||||||
FeeRate::from_sat_per_kwu(sat_kwu)
|
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");
|
||||||
|
|
||||||
|
let mut graph = TxGraph::default();
|
||||||
|
let _ = graph.insert_anchor(txid, anchor);
|
||||||
|
wallet
|
||||||
|
.apply_update(Update {
|
||||||
|
graph,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -51,12 +51,19 @@ fn receive_output_to_address(
|
|||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
||||||
wallet.insert_tx(tx.clone(), height).unwrap();
|
let txid = tx.compute_txid();
|
||||||
|
wallet.insert_tx(tx);
|
||||||
|
|
||||||
OutPoint {
|
match height {
|
||||||
txid: tx.compute_txid(),
|
ConfirmationTime::Confirmed { .. } => {
|
||||||
vout: 0,
|
insert_anchor_from_conf(wallet, txid, height);
|
||||||
|
}
|
||||||
|
ConfirmationTime::Unconfirmed { last_seen } => {
|
||||||
|
insert_seen_at(wallet, txid, last_seen);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OutPoint { txid, vout: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint {
|
fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint {
|
||||||
@ -70,6 +77,18 @@ fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint {
|
|||||||
receive_output(wallet, value, anchor)
|
receive_output(wallet, value, anchor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn insert_seen_at(wallet: &mut Wallet, txid: Txid, seen_at: u64) {
|
||||||
|
use bdk_wallet::wallet::Update;
|
||||||
|
let mut graph = bdk_chain::TxGraph::default();
|
||||||
|
let _ = graph.insert_seen_at(txid, seen_at);
|
||||||
|
wallet
|
||||||
|
.apply_update(Update {
|
||||||
|
graph,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
// The satisfaction size of a P2WPKH is 112 WU =
|
// The satisfaction size of a P2WPKH is 112 WU =
|
||||||
// 1 (elements in witness) + 1 (OP_PUSH) + 33 (pk) + 1 (OP_PUSH) + 72 (signature + sighash) + 1*4 (script len)
|
// 1 (elements in witness) + 1 (OP_PUSH) + 33 (pk) + 1 (OP_PUSH) + 72 (signature + sighash) + 1*4 (script len)
|
||||||
// On the witness itself, we have to push once for the pk (33WU) and once for signature + sighash (72WU), for
|
// On the witness itself, we have to push once for the pk (33WU) and once for signature + sighash (72WU), for
|
||||||
@ -1188,12 +1207,16 @@ fn test_create_tx_add_utxo() {
|
|||||||
version: transaction::Version::non_standard(0),
|
version: transaction::Version::non_standard(0),
|
||||||
lock_time: absolute::LockTime::ZERO,
|
lock_time: absolute::LockTime::ZERO,
|
||||||
};
|
};
|
||||||
wallet
|
let txid = small_output_tx.compute_txid();
|
||||||
.insert_tx(
|
wallet.insert_tx(small_output_tx);
|
||||||
small_output_tx.clone(),
|
insert_anchor_from_conf(
|
||||||
ConfirmationTime::Unconfirmed { last_seen: 0 },
|
&mut wallet,
|
||||||
)
|
txid,
|
||||||
.unwrap();
|
ConfirmationTime::Confirmed {
|
||||||
|
height: 2000,
|
||||||
|
time: 200,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
|
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@ -1201,10 +1224,7 @@ fn test_create_tx_add_utxo() {
|
|||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder
|
||||||
.add_recipient(addr.script_pubkey(), Amount::from_sat(30_000))
|
.add_recipient(addr.script_pubkey(), Amount::from_sat(30_000))
|
||||||
.add_utxo(OutPoint {
|
.add_utxo(OutPoint { txid, vout: 0 })
|
||||||
txid: small_output_tx.compute_txid(),
|
|
||||||
vout: 0,
|
|
||||||
})
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let psbt = builder.finish().unwrap();
|
let psbt = builder.finish().unwrap();
|
||||||
let sent_received =
|
let sent_received =
|
||||||
@ -1237,13 +1257,16 @@ fn test_create_tx_manually_selected_insufficient() {
|
|||||||
version: transaction::Version::non_standard(0),
|
version: transaction::Version::non_standard(0),
|
||||||
lock_time: absolute::LockTime::ZERO,
|
lock_time: absolute::LockTime::ZERO,
|
||||||
};
|
};
|
||||||
|
let txid = small_output_tx.compute_txid();
|
||||||
wallet
|
wallet.insert_tx(small_output_tx.clone());
|
||||||
.insert_tx(
|
insert_anchor_from_conf(
|
||||||
small_output_tx.clone(),
|
&mut wallet,
|
||||||
ConfirmationTime::Unconfirmed { last_seen: 0 },
|
txid,
|
||||||
)
|
ConfirmationTime::Confirmed {
|
||||||
.unwrap();
|
height: 2000,
|
||||||
|
time: 200,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
|
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@ -1251,10 +1274,7 @@ fn test_create_tx_manually_selected_insufficient() {
|
|||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder
|
builder
|
||||||
.add_recipient(addr.script_pubkey(), Amount::from_sat(30_000))
|
.add_recipient(addr.script_pubkey(), Amount::from_sat(30_000))
|
||||||
.add_utxo(OutPoint {
|
.add_utxo(OutPoint { txid, vout: 0 })
|
||||||
txid: small_output_tx.compute_txid(),
|
|
||||||
vout: 0,
|
|
||||||
})
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.manually_selected_only();
|
.manually_selected_only();
|
||||||
builder.finish().unwrap();
|
builder.finish().unwrap();
|
||||||
@ -1289,9 +1309,9 @@ fn test_create_tx_policy_path_no_csv() {
|
|||||||
value: Amount::from_sat(50_000),
|
value: Amount::from_sat(50_000),
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
wallet
|
let txid = tx.compute_txid();
|
||||||
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
wallet.insert_tx(tx);
|
||||||
.unwrap();
|
insert_seen_at(&mut wallet, txid, 0);
|
||||||
|
|
||||||
let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap();
|
let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap();
|
||||||
let root_id = external_policy.id;
|
let root_id = external_policy.id;
|
||||||
@ -1659,9 +1679,8 @@ fn test_bump_fee_irreplaceable_tx() {
|
|||||||
|
|
||||||
let tx = psbt.extract_tx().expect("failed to extract tx");
|
let tx = psbt.extract_tx().expect("failed to extract tx");
|
||||||
let txid = tx.compute_txid();
|
let txid = tx.compute_txid();
|
||||||
wallet
|
wallet.insert_tx(tx);
|
||||||
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
insert_seen_at(&mut wallet, txid, 0);
|
||||||
.unwrap();
|
|
||||||
wallet.build_fee_bump(txid).unwrap().finish().unwrap();
|
wallet.build_fee_bump(txid).unwrap().finish().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1677,15 +1696,15 @@ fn test_bump_fee_confirmed_tx() {
|
|||||||
let tx = psbt.extract_tx().expect("failed to extract tx");
|
let tx = psbt.extract_tx().expect("failed to extract tx");
|
||||||
let txid = tx.compute_txid();
|
let txid = tx.compute_txid();
|
||||||
|
|
||||||
wallet
|
wallet.insert_tx(tx);
|
||||||
.insert_tx(
|
insert_anchor_from_conf(
|
||||||
tx,
|
&mut wallet,
|
||||||
ConfirmationTime::Confirmed {
|
txid,
|
||||||
height: 42,
|
ConfirmationTime::Confirmed {
|
||||||
time: 42_000,
|
height: 42,
|
||||||
},
|
time: 42_000,
|
||||||
)
|
},
|
||||||
.unwrap();
|
);
|
||||||
|
|
||||||
wallet.build_fee_bump(txid).unwrap().finish().unwrap();
|
wallet.build_fee_bump(txid).unwrap().finish().unwrap();
|
||||||
}
|
}
|
||||||
@ -1704,9 +1723,8 @@ fn test_bump_fee_low_fee_rate() {
|
|||||||
let tx = psbt.extract_tx().expect("failed to extract tx");
|
let tx = psbt.extract_tx().expect("failed to extract tx");
|
||||||
let txid = tx.compute_txid();
|
let txid = tx.compute_txid();
|
||||||
|
|
||||||
wallet
|
wallet.insert_tx(tx);
|
||||||
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
insert_seen_at(&mut wallet, txid, 0);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
builder.fee_rate(FeeRate::BROADCAST_MIN);
|
builder.fee_rate(FeeRate::BROADCAST_MIN);
|
||||||
@ -1737,9 +1755,8 @@ fn test_bump_fee_low_abs() {
|
|||||||
let tx = psbt.extract_tx().expect("failed to extract tx");
|
let tx = psbt.extract_tx().expect("failed to extract tx");
|
||||||
let txid = tx.compute_txid();
|
let txid = tx.compute_txid();
|
||||||
|
|
||||||
wallet
|
wallet.insert_tx(tx);
|
||||||
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
insert_seen_at(&mut wallet, txid, 0);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
builder.fee_absolute(Amount::from_sat(10));
|
builder.fee_absolute(Amount::from_sat(10));
|
||||||
@ -1759,9 +1776,8 @@ fn test_bump_fee_zero_abs() {
|
|||||||
|
|
||||||
let tx = psbt.extract_tx().expect("failed to extract tx");
|
let tx = psbt.extract_tx().expect("failed to extract tx");
|
||||||
let txid = tx.compute_txid();
|
let txid = tx.compute_txid();
|
||||||
wallet
|
wallet.insert_tx(tx);
|
||||||
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
insert_seen_at(&mut wallet, txid, 0);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
builder.fee_absolute(Amount::ZERO);
|
builder.fee_absolute(Amount::ZERO);
|
||||||
@ -1785,9 +1801,8 @@ fn test_bump_fee_reduce_change() {
|
|||||||
|
|
||||||
let tx = psbt.extract_tx().expect("failed to extract tx");
|
let tx = psbt.extract_tx().expect("failed to extract tx");
|
||||||
let txid = tx.compute_txid();
|
let txid = tx.compute_txid();
|
||||||
wallet
|
wallet.insert_tx(tx);
|
||||||
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
insert_seen_at(&mut wallet, txid, 0);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb
|
let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb
|
||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
@ -1883,9 +1898,8 @@ fn test_bump_fee_reduce_single_recipient() {
|
|||||||
let original_sent_received = wallet.sent_and_received(&tx);
|
let original_sent_received = wallet.sent_and_received(&tx);
|
||||||
let original_fee = check_fee!(wallet, psbt);
|
let original_fee = check_fee!(wallet, psbt);
|
||||||
let txid = tx.compute_txid();
|
let txid = tx.compute_txid();
|
||||||
wallet
|
wallet.insert_tx(tx);
|
||||||
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
insert_seen_at(&mut wallet, txid, 0);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb
|
let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb
|
||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
@ -1931,9 +1945,8 @@ fn test_bump_fee_absolute_reduce_single_recipient() {
|
|||||||
let tx = psbt.extract_tx().expect("failed to extract tx");
|
let tx = psbt.extract_tx().expect("failed to extract tx");
|
||||||
let original_sent_received = wallet.sent_and_received(&tx);
|
let original_sent_received = wallet.sent_and_received(&tx);
|
||||||
let txid = tx.compute_txid();
|
let txid = tx.compute_txid();
|
||||||
wallet
|
wallet.insert_tx(tx);
|
||||||
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
insert_seen_at(&mut wallet, txid, 0);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
builder
|
builder
|
||||||
@ -1976,15 +1989,18 @@ fn test_bump_fee_drain_wallet() {
|
|||||||
value: Amount::from_sat(25_000),
|
value: Amount::from_sat(25_000),
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
wallet
|
let txid = tx.compute_txid();
|
||||||
.insert_tx(
|
let tip = wallet.latest_checkpoint().height();
|
||||||
tx.clone(),
|
wallet.insert_tx(tx.clone());
|
||||||
ConfirmationTime::Confirmed {
|
insert_anchor_from_conf(
|
||||||
height: wallet.latest_checkpoint().height(),
|
&mut wallet,
|
||||||
time: 42_000,
|
txid,
|
||||||
},
|
ConfirmationTime::Confirmed {
|
||||||
)
|
height: tip,
|
||||||
.unwrap();
|
time: 42_000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
|
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.assume_checked();
|
.assume_checked();
|
||||||
@ -2004,9 +2020,8 @@ fn test_bump_fee_drain_wallet() {
|
|||||||
let original_sent_received = wallet.sent_and_received(&tx);
|
let original_sent_received = wallet.sent_and_received(&tx);
|
||||||
|
|
||||||
let txid = tx.compute_txid();
|
let txid = tx.compute_txid();
|
||||||
wallet
|
wallet.insert_tx(tx);
|
||||||
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
insert_seen_at(&mut wallet, txid, 0);
|
||||||
.unwrap();
|
|
||||||
assert_eq!(original_sent_received.0, Amount::from_sat(25_000));
|
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
|
// for the new feerate, it should be enough to reduce the output, but since we specify
|
||||||
@ -2041,18 +2056,17 @@ fn test_bump_fee_remove_output_manually_selected_only() {
|
|||||||
value: Amount::from_sat(25_000),
|
value: Amount::from_sat(25_000),
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
wallet
|
let position: ConfirmationTime = wallet
|
||||||
.insert_tx(
|
.transactions()
|
||||||
init_tx.clone(),
|
.last()
|
||||||
wallet
|
.unwrap()
|
||||||
.transactions()
|
.chain_position
|
||||||
.last()
|
.cloned()
|
||||||
.unwrap()
|
.into();
|
||||||
.chain_position
|
|
||||||
.cloned()
|
wallet.insert_tx(init_tx.clone());
|
||||||
.into(),
|
insert_anchor_from_conf(&mut wallet, init_tx.compute_txid(), position);
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let outpoint = OutPoint {
|
let outpoint = OutPoint {
|
||||||
txid: init_tx.compute_txid(),
|
txid: init_tx.compute_txid(),
|
||||||
vout: 0,
|
vout: 0,
|
||||||
@ -2071,9 +2085,8 @@ fn test_bump_fee_remove_output_manually_selected_only() {
|
|||||||
let tx = psbt.extract_tx().expect("failed to extract tx");
|
let tx = psbt.extract_tx().expect("failed to extract tx");
|
||||||
let original_sent_received = wallet.sent_and_received(&tx);
|
let original_sent_received = wallet.sent_and_received(&tx);
|
||||||
let txid = tx.compute_txid();
|
let txid = tx.compute_txid();
|
||||||
wallet
|
wallet.insert_tx(tx);
|
||||||
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
insert_seen_at(&mut wallet, txid, 0);
|
||||||
.unwrap();
|
|
||||||
assert_eq!(original_sent_received.0, Amount::from_sat(25_000));
|
assert_eq!(original_sent_received.0, Amount::from_sat(25_000));
|
||||||
|
|
||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
@ -2097,14 +2110,16 @@ fn test_bump_fee_add_input() {
|
|||||||
value: Amount::from_sat(25_000),
|
value: Amount::from_sat(25_000),
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
let pos = wallet
|
let txid = init_tx.compute_txid();
|
||||||
|
let pos: ConfirmationTime = wallet
|
||||||
.transactions()
|
.transactions()
|
||||||
.last()
|
.last()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.chain_position
|
.chain_position
|
||||||
.cloned()
|
.cloned()
|
||||||
.into();
|
.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")
|
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@ -2117,9 +2132,8 @@ fn test_bump_fee_add_input() {
|
|||||||
let tx = psbt.extract_tx().expect("failed to extract tx");
|
let tx = psbt.extract_tx().expect("failed to extract tx");
|
||||||
let original_details = wallet.sent_and_received(&tx);
|
let original_details = wallet.sent_and_received(&tx);
|
||||||
let txid = tx.compute_txid();
|
let txid = tx.compute_txid();
|
||||||
wallet
|
wallet.insert_tx(tx);
|
||||||
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
insert_seen_at(&mut wallet, txid, 0);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(50));
|
builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(50));
|
||||||
@ -2174,9 +2188,8 @@ fn test_bump_fee_absolute_add_input() {
|
|||||||
let tx = psbt.extract_tx().expect("failed to extract tx");
|
let tx = psbt.extract_tx().expect("failed to extract tx");
|
||||||
let original_sent_received = wallet.sent_and_received(&tx);
|
let original_sent_received = wallet.sent_and_received(&tx);
|
||||||
let txid = tx.compute_txid();
|
let txid = tx.compute_txid();
|
||||||
wallet
|
wallet.insert_tx(tx);
|
||||||
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
insert_seen_at(&mut wallet, txid, 0);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
builder.fee_absolute(Amount::from_sat(6_000));
|
builder.fee_absolute(Amount::from_sat(6_000));
|
||||||
@ -2240,9 +2253,8 @@ fn test_bump_fee_no_change_add_input_and_change() {
|
|||||||
|
|
||||||
let tx = psbt.extract_tx().expect("failed to extract tx");
|
let tx = psbt.extract_tx().expect("failed to extract tx");
|
||||||
let txid = tx.compute_txid();
|
let txid = tx.compute_txid();
|
||||||
wallet
|
wallet.insert_tx(tx);
|
||||||
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
insert_seen_at(&mut wallet, txid, 0);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Now bump the fees, the wallet should add an extra input and a change output, and leave
|
// Now bump the fees, the wallet should add an extra input and a change output, and leave
|
||||||
// the original output untouched.
|
// the original output untouched.
|
||||||
@ -2310,9 +2322,8 @@ fn test_bump_fee_add_input_change_dust() {
|
|||||||
assert_eq!(tx.input.len(), 1);
|
assert_eq!(tx.input.len(), 1);
|
||||||
assert_eq!(tx.output.len(), 2);
|
assert_eq!(tx.output.len(), 2);
|
||||||
let txid = tx.compute_txid();
|
let txid = tx.compute_txid();
|
||||||
wallet
|
wallet.insert_tx(tx);
|
||||||
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
insert_seen_at(&mut wallet, txid, 0);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
// We set a fee high enough that during rbf we are forced to add
|
// We set a fee high enough that during rbf we are forced to add
|
||||||
@ -2381,9 +2392,8 @@ fn test_bump_fee_force_add_input() {
|
|||||||
for txin in &mut tx.input {
|
for txin in &mut tx.input {
|
||||||
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
|
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
|
||||||
}
|
}
|
||||||
wallet
|
wallet.insert_tx(tx.clone());
|
||||||
.insert_tx(tx.clone(), ConfirmationTime::Unconfirmed { last_seen: 0 })
|
insert_seen_at(&mut wallet, txid, 0);
|
||||||
.unwrap();
|
|
||||||
// the new fee_rate is low enough that just reducing the change would be fine, but we force
|
// 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()`
|
// the addition of an extra input with `add_utxo()`
|
||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
@ -2448,9 +2458,8 @@ fn test_bump_fee_absolute_force_add_input() {
|
|||||||
for txin in &mut tx.input {
|
for txin in &mut tx.input {
|
||||||
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
|
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
|
||||||
}
|
}
|
||||||
wallet
|
wallet.insert_tx(tx.clone());
|
||||||
.insert_tx(tx.clone(), ConfirmationTime::Unconfirmed { last_seen: 0 })
|
insert_seen_at(&mut wallet, txid, 0);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// the new fee_rate is low enough that just reducing the change would be fine, but we force
|
// 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()`
|
// the addition of an extra input with `add_utxo()`
|
||||||
@ -2527,9 +2536,8 @@ fn test_bump_fee_unconfirmed_inputs_only() {
|
|||||||
for txin in &mut tx.input {
|
for txin in &mut tx.input {
|
||||||
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
|
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
|
||||||
}
|
}
|
||||||
wallet
|
wallet.insert_tx(tx);
|
||||||
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
insert_seen_at(&mut wallet, txid, 0);
|
||||||
.unwrap();
|
|
||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(25));
|
builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(25));
|
||||||
builder.finish().unwrap();
|
builder.finish().unwrap();
|
||||||
@ -2560,9 +2568,8 @@ fn test_bump_fee_unconfirmed_input() {
|
|||||||
for txin in &mut tx.input {
|
for txin in &mut tx.input {
|
||||||
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
|
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
|
||||||
}
|
}
|
||||||
wallet
|
wallet.insert_tx(tx);
|
||||||
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
|
insert_seen_at(&mut wallet, txid, 0);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||||
builder
|
builder
|
||||||
@ -3755,15 +3762,16 @@ fn test_spend_coinbase() {
|
|||||||
value: Amount::from_sat(25_000),
|
value: Amount::from_sat(25_000),
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
wallet
|
let txid = coinbase_tx.compute_txid();
|
||||||
.insert_tx(
|
wallet.insert_tx(coinbase_tx);
|
||||||
coinbase_tx,
|
insert_anchor_from_conf(
|
||||||
ConfirmationTime::Confirmed {
|
&mut wallet,
|
||||||
height: confirmation_height,
|
txid,
|
||||||
time: 30_000,
|
ConfirmationTime::Confirmed {
|
||||||
},
|
height: confirmation_height,
|
||||||
)
|
time: 30_000,
|
||||||
.unwrap();
|
},
|
||||||
|
);
|
||||||
|
|
||||||
let not_yet_mature_time = confirmation_height + COINBASE_MATURITY - 1;
|
let not_yet_mature_time = confirmation_height + COINBASE_MATURITY - 1;
|
||||||
let maturity_time = confirmation_height + COINBASE_MATURITY;
|
let maturity_time = confirmation_height + COINBASE_MATURITY;
|
||||||
@ -4092,3 +4100,45 @@ fn test_thread_safety() {
|
|||||||
fn thread_safe<T: Send + Sync>() {}
|
fn thread_safe<T: Send + Sync>() {}
|
||||||
thread_safe::<Wallet>(); // compiles only if true
|
thread_safe::<Wallet>(); // compiles only if true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_insert_tx_balance_and_utxos() {
|
||||||
|
// creating many txs has no effect on the wallet's available utxos
|
||||||
|
let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv());
|
||||||
|
let addr = Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")
|
||||||
|
.unwrap()
|
||||||
|
.assume_checked();
|
||||||
|
|
||||||
|
let unspent: Vec<_> = wallet.list_unspent().collect();
|
||||||
|
assert!(!unspent.is_empty());
|
||||||
|
|
||||||
|
let balance = wallet.balance().total();
|
||||||
|
let fee = Amount::from_sat(143);
|
||||||
|
let amt = balance - fee;
|
||||||
|
|
||||||
|
for _ in 0..3 {
|
||||||
|
let mut builder = wallet.build_tx();
|
||||||
|
builder.add_recipient(addr.script_pubkey(), amt);
|
||||||
|
let mut psbt = builder.finish().unwrap();
|
||||||
|
assert!(wallet.sign(&mut psbt, SignOptions::default()).unwrap());
|
||||||
|
let tx = psbt.extract_tx().unwrap();
|
||||||
|
let _ = wallet.insert_tx(tx);
|
||||||
|
}
|
||||||
|
assert_eq!(wallet.list_unspent().collect::<Vec<_>>(), unspent);
|
||||||
|
assert_eq!(wallet.balance().confirmed, balance);
|
||||||
|
|
||||||
|
// manually setting a tx last_seen will consume the wallet's available utxos
|
||||||
|
let addr = Address::from_str("bcrt1qfjg5lv3dvc9az8patec8fjddrs4aqtauadnagr")
|
||||||
|
.unwrap()
|
||||||
|
.assume_checked();
|
||||||
|
let mut builder = wallet.build_tx();
|
||||||
|
builder.add_recipient(addr.script_pubkey(), amt);
|
||||||
|
let mut psbt = builder.finish().unwrap();
|
||||||
|
assert!(wallet.sign(&mut psbt, SignOptions::default()).unwrap());
|
||||||
|
let tx = psbt.extract_tx().unwrap();
|
||||||
|
let txid = tx.compute_txid();
|
||||||
|
let _ = wallet.insert_tx(tx);
|
||||||
|
insert_seen_at(&mut wallet, txid, 2);
|
||||||
|
assert!(wallet.list_unspent().next().is_none());
|
||||||
|
assert_eq!(wallet.balance().total().to_sat(), 0);
|
||||||
|
}
|
||||||
|
@ -277,7 +277,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
if unconfirmed {
|
if unconfirmed {
|
||||||
let unconfirmed_txids = graph
|
let unconfirmed_txids = graph
|
||||||
.graph()
|
.graph()
|
||||||
.list_chain_txs(&*chain, chain_tip.block_id())
|
.list_canonical_txs(&*chain, chain_tip.block_id())
|
||||||
.filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
|
.filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
|
||||||
.map(|canonical_tx| canonical_tx.tx_node.txid)
|
.map(|canonical_tx| canonical_tx.tx_node.txid)
|
||||||
.collect::<Vec<Txid>>();
|
.collect::<Vec<Txid>>();
|
||||||
|
@ -307,7 +307,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
// `EsploraExt::update_tx_graph_without_keychain`.
|
// `EsploraExt::update_tx_graph_without_keychain`.
|
||||||
let unconfirmed_txids = graph
|
let unconfirmed_txids = graph
|
||||||
.graph()
|
.graph()
|
||||||
.list_chain_txs(&*chain, local_tip.block_id())
|
.list_canonical_txs(&*chain, local_tip.block_id())
|
||||||
.filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
|
.filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
|
||||||
.map(|canonical_tx| canonical_tx.tx_node.txid)
|
.map(|canonical_tx| canonical_tx.tx_node.txid)
|
||||||
.collect::<Vec<Txid>>();
|
.collect::<Vec<Txid>>();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user