Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proof of concept: implement cancel_tx #1764

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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