Skip to content

Commit

Permalink
feat: cancel tx
Browse files Browse the repository at this point in the history
  • Loading branch information
ValuedMammal committed Dec 8, 2024
1 parent bcff89d commit 01690d2
Show file tree
Hide file tree
Showing 4 changed files with 325 additions and 10 deletions.
77 changes: 76 additions & 1 deletion crates/wallet/src/wallet/changeset.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use bdk_chain::{
indexed_tx_graph, keychain_txout, local_chain, tx_graph, ConfirmationBlockTime, Merge,
};
use bitcoin::Txid;
use miniscript::{Descriptor, DescriptorPublicKey};

use crate::wallet::tx_details;

type IndexedTxGraphChangeSet =
indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet>;

Expand All @@ -15,6 +18,9 @@ pub struct ChangeSet {
pub change_descriptor: Option<Descriptor<DescriptorPublicKey>>,
/// Stores the network type of the transaction data.
pub network: Option<bitcoin::Network>,
/// Details and metadata of wallet transactions
pub tx_details: Option<tx_details::ChangeSet>,

/// Changes to the [`LocalChain`](local_chain::LocalChain).
pub local_chain: local_chain::ChangeSet,
/// Changes to [`TxGraph`](tx_graph::TxGraph).
Expand Down Expand Up @@ -49,6 +55,12 @@ impl Merge for ChangeSet {
self.network = other.network;
}

match (&mut self.tx_details, other.tx_details) {
(None, b) => self.tx_details = b,
(Some(a), Some(b)) => Merge::merge(a, b),
_ => {}
}

Merge::merge(&mut self.local_chain, other.local_chain);
Merge::merge(&mut self.tx_graph, other.tx_graph);
Merge::merge(&mut self.indexer, other.indexer);
Expand All @@ -58,6 +70,7 @@ impl Merge for ChangeSet {
self.descriptor.is_none()
&& self.change_descriptor.is_none()
&& self.network.is_none()
&& (self.tx_details.is_none() || self.tx_details.as_ref().unwrap().is_empty())
&& self.local_chain.is_empty()
&& self.tx_graph.is_empty()
&& self.indexer.is_empty()
Expand All @@ -70,6 +83,8 @@ impl ChangeSet {
pub const WALLET_SCHEMA_NAME: &'static str = "bdk_wallet";
/// Name of table to store wallet descriptors and network.
pub const WALLET_TABLE_NAME: &'static str = "bdk_wallet";
/// Name of table to store wallet tx details
pub const TX_DETAILS_TABLE_NAME: &'static str = "bdk_tx_details";

/// Initialize sqlite tables for wallet tables.
pub fn init_sqlite_tables(db_tx: &chain::rusqlite::Transaction) -> chain::rusqlite::Result<()> {
Expand All @@ -82,7 +97,19 @@ impl ChangeSet {
) STRICT;",
Self::WALLET_TABLE_NAME,
)];
crate::rusqlite_impl::migrate_schema(db_tx, Self::WALLET_SCHEMA_NAME, &[schema_v0])?;
// schema_v1 adds a table for tx-details
let schema_v1: &[&str] = &[&format!(
"CREATE TABLE {} ( \
txid TEXT PRIMARY KEY NOT NULL, \
is_canceled INTEGER DEFAULT 0 \
) STRICT;",
Self::TX_DETAILS_TABLE_NAME,
)];
crate::rusqlite_impl::migrate_schema(
db_tx,
Self::WALLET_SCHEMA_NAME,
&[schema_v0, schema_v1],
)?;

bdk_chain::local_chain::ChangeSet::init_sqlite_tables(db_tx)?;
bdk_chain::tx_graph::ChangeSet::<ConfirmationBlockTime>::init_sqlite_tables(db_tx)?;
Expand Down Expand Up @@ -119,6 +146,30 @@ impl ChangeSet {
changeset.network = network.map(Impl::into_inner);
}

// select tx details
let mut change = tx_details::ChangeSet::default();
let mut stmt = db_tx.prepare_cached(&format!(
"SELECT txid, is_canceled FROM {}",
Self::TX_DETAILS_TABLE_NAME,
))?;
let rows = stmt.query_map([], |row| {
Ok((
row.get::<_, Impl<Txid>>("txid")?,
row.get::<_, bool>("is_canceled")?,
))
})?;
for res in rows {
let (Impl(txid), is_canceled) = res?;
let det = tx_details::Details { is_canceled };
let record = tx_details::Record::Details(det);
change.records.push((txid, record));
}
changeset.tx_details = if change.is_empty() {
None
} else {
Some(change)
};

changeset.local_chain = local_chain::ChangeSet::from_sqlite(db_tx)?;
changeset.tx_graph = tx_graph::ChangeSet::<_>::from_sqlite(db_tx)?;
changeset.indexer = keychain_txout::ChangeSet::from_sqlite(db_tx)?;
Expand Down Expand Up @@ -167,6 +218,21 @@ impl ChangeSet {
})?;
}

// persist tx details
let mut cancel_tx_stmt = db_tx.prepare_cached(&format!(
"REPLACE INTO {}(txid, is_canceled) VALUES(:txid, 1)",
Self::TX_DETAILS_TABLE_NAME,
))?;
if let Some(change) = &self.tx_details {
for (txid, record) in &change.records {
if record == &tx_details::Record::Canceled {
cancel_tx_stmt.execute(named_params! {
":txid": Impl(*txid),
})?;
}
}
}

self.local_chain.persist_to_sqlite(db_tx)?;
self.tx_graph.persist_to_sqlite(db_tx)?;
self.indexer.persist_to_sqlite(db_tx)?;
Expand Down Expand Up @@ -210,3 +276,12 @@ impl From<keychain_txout::ChangeSet> for ChangeSet {
}
}
}

impl From<tx_details::ChangeSet> for ChangeSet {
fn from(value: tx_details::ChangeSet) -> Self {
Self {
tx_details: Some(value),
..Default::default()
}
}
}
86 changes: 77 additions & 9 deletions crates/wallet/src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ mod params;
mod persisted;
pub mod signer;
pub mod tx_builder;
mod tx_details;
pub(crate) mod utils;

use crate::collections::{BTreeMap, HashMap};
Expand All @@ -76,6 +77,7 @@ use crate::wallet::{
error::{BuildFeeBumpError, CreateTxError, MiniscriptPsbtError},
signer::{SignOptions, SignerError, SignerOrdering, SignersContainer, TransactionSigner},
tx_builder::{FeePolicy, TxBuilder, TxParams},
tx_details::TxDetails,
utils::{check_nsequence_rbf, After, Older, SecpCtx},
};

Expand Down Expand Up @@ -112,6 +114,7 @@ pub struct Wallet {
stage: ChangeSet,
network: Network,
secp: SecpCtx,
tx_details: TxDetails,
}

/// An update to [`Wallet`].
Expand Down Expand Up @@ -409,6 +412,7 @@ impl Wallet {
let change_descriptor = index.get_descriptor(KeychainKind::Internal).cloned();
let indexed_graph = IndexedTxGraph::new(index);
let indexed_graph_changeset = indexed_graph.initial_changeset();
let tx_details = TxDetails::default();

let stage = ChangeSet {
descriptor,
Expand All @@ -417,6 +421,7 @@ impl Wallet {
tx_graph: indexed_graph_changeset.tx_graph,
indexer: indexed_graph_changeset.indexer,
network: Some(network),
..Default::default()
};

Ok(Wallet {
Expand All @@ -427,6 +432,7 @@ impl Wallet {
indexed_graph,
stage,
secp,
tx_details,
})
}

Expand Down Expand Up @@ -609,6 +615,11 @@ impl Wallet {
indexed_graph.apply_changeset(changeset.indexer.into());
indexed_graph.apply_changeset(changeset.tx_graph.into());

let mut tx_details = TxDetails::default();
if let Some(change) = changeset.tx_details {
tx_details.apply_changeset(change);
}

let stage = ChangeSet::default();

Ok(Some(Wallet {
Expand All @@ -619,6 +630,7 @@ impl Wallet {
stage,
network,
secp,
tx_details,
}))
}

Expand Down Expand Up @@ -815,14 +827,34 @@ impl Wallet {

/// Return the list of unspent outputs of this wallet
pub fn list_unspent(&self) -> impl Iterator<Item = LocalOutput> + '_ {
self._list_unspent()
.map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
}

/// `list_unspent` that accounts for canceled txs
fn _list_unspent(
&self,
) -> impl Iterator<Item = ((KeychainKind, u32), FullTxOut<ConfirmationBlockTime>)> + '_ {
self.indexed_graph
.graph()
.filter_chain_unspents(
.filter_chain_txouts(
&self.chain,
self.chain.tip().block_id(),
self.indexed_graph.index.outpoints().iter().cloned(),
)
.map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
.filter(|(_, txo)| {
txo.chain_position.is_confirmed() || !self.is_canceled_tx(&txo.outpoint.txid)
})
.filter(|(_, txo)| {
match txo.spent_by {
// keep unspent
None => true,
// keep if spent by a canceled tx
Some((pos, spend_txid)) => {
self.is_canceled_tx(&spend_txid) && !pos.is_confirmed()
}
}
})
}

/// List all relevant outputs (includes both spent and unspent, confirmed and unconfirmed).
Expand Down Expand Up @@ -1103,12 +1135,38 @@ impl Wallet {
/// Return the balance, separated into available, trusted-pending, untrusted-pending and immature
/// values.
pub fn balance(&self) -> Balance {
self.indexed_graph.graph().balance(
&self.chain,
self.chain.tip().block_id(),
self.indexed_graph.index.outpoints().iter().cloned(),
|&(k, _), _| k == KeychainKind::Internal,
)
let mut immature = Amount::ZERO;
let mut trusted_pending = Amount::ZERO;
let mut untrusted_pending = Amount::ZERO;
let mut confirmed = Amount::ZERO;

let chain_tip = self.latest_checkpoint().height();

for (indexed, txo) in self._list_unspent() {
match &txo.chain_position {
ChainPosition::Confirmed { .. } => {
if txo.is_confirmed_and_spendable(chain_tip) {
confirmed += txo.txout.value;
} else if !txo.is_mature(chain_tip) {
immature += txo.txout.value;
}
}
ChainPosition::Unconfirmed { .. } => {
if let (KeychainKind::Internal, _) = indexed {
trusted_pending += txo.txout.value;
} else {
untrusted_pending += txo.txout.value;
}
}
}
}

Balance {
immature,
trusted_pending,
untrusted_pending,
confirmed,
}
}

/// Add an external signer
Expand Down Expand Up @@ -1937,10 +1995,18 @@ impl Wallet {
.0
}

/// Whether the transaction with `txid` was canceled
fn is_canceled_tx(&self, txid: &Txid) -> bool {
self.tx_details
.map
.get(txid)
.map(|v| v.is_canceled)
.unwrap_or(false)
}

/// Informs the wallet that you no longer intend to broadcast a tx that was built from it.
///
/// This frees up the change address used when creating the tx for use in future transactions.
// TODO: Make this free up reserved utxos when that's implemented
pub fn cancel_tx(&mut self, tx: &Transaction) {
let txout_index = &mut self.indexed_graph.index;
for txout in &tx.output {
Expand All @@ -1950,6 +2016,8 @@ impl Wallet {
txout_index.unmark_used(*keychain, *index);
}
}
self.stage
.merge(self.tx_details.cancel_tx(tx.compute_txid()).into());
}

fn get_descriptor_for_txout(&self, txout: &TxOut) -> Option<DerivedDescriptor> {
Expand Down
Loading

0 comments on commit 01690d2

Please sign in to comment.