From 161dc92c734b217c7dea151a8d3ce88bfc64d90a Mon Sep 17 00:00:00 2001 From: rustaceanrob Date: Thu, 1 May 2025 17:29:46 +0100 Subject: [PATCH 1/4] feat(bitcoin): introduce hash-like types Types like "Txid", "BlockHash", "DescriptorId" are all just 32 byte arrays that represent hashes with different meanings. Currently they are represented as strings at the FFI layer, but they are also meaningful arrays of bytes. Particularly if a user wants to implement persistence over the FFI layer, they would want to efficiently serialize these types. Here I am introducing a new group of types that all implement display, allow serialization to bytes, and may be constructed from an array of bytes. I went with a "rule of 3s" here, and also introduced a macro to do these implementations because there was a lot of boilerplate involved. Note that all of these are included in the wallet changeset, which is required to represent in-full for FFI-layer custom persistence. --- bdk-ffi/src/bitcoin.rs | 52 ++++++++++++++++++++++++++++++++++++++---- bdk-ffi/src/error.rs | 6 +++++ bdk-ffi/src/macros.rs | 29 +++++++++++++++++++++++ 3 files changed, 82 insertions(+), 5 deletions(-) diff --git a/bdk-ffi/src/bitcoin.rs b/bdk-ffi/src/bitcoin.rs index fb0f0d40..d45b21de 100644 --- a/bdk-ffi/src/bitcoin.rs +++ b/bdk-ffi/src/bitcoin.rs @@ -1,27 +1,33 @@ use crate::error::{ - AddressParseError, ExtractTxError, FeeRateError, FromScriptError, PsbtError, PsbtParseError, - TransactionError, + AddressParseError, ExtractTxError, FeeRateError, FromScriptError, HashParseError, PsbtError, + PsbtParseError, TransactionError, }; use crate::error::{ParseAmountError, PsbtFinalizeError}; -use crate::{impl_from_core_type, impl_into_core_type}; +use crate::{impl_from_core_type, impl_hash_like, impl_into_core_type}; use bdk_wallet::bitcoin::address::NetworkChecked; use bdk_wallet::bitcoin::address::NetworkUnchecked; use bdk_wallet::bitcoin::address::{Address as BdkAddress, AddressData as BdkAddressData}; use bdk_wallet::bitcoin::blockdata::block::Header as BdkHeader; +use bdk_wallet::bitcoin::consensus::encode::deserialize; use bdk_wallet::bitcoin::consensus::encode::serialize; use bdk_wallet::bitcoin::consensus::Decodable; +use bdk_wallet::bitcoin::hashes::sha256::Hash as BitcoinSha256Hash; +use bdk_wallet::bitcoin::hashes::sha256d::Hash as BitcoinDoubleSha256Hash; use bdk_wallet::bitcoin::io::Cursor; use bdk_wallet::bitcoin::secp256k1::Secp256k1; use bdk_wallet::bitcoin::Amount as BdkAmount; +use bdk_wallet::bitcoin::BlockHash as BitcoinBlockHash; use bdk_wallet::bitcoin::FeeRate as BdkFeeRate; use bdk_wallet::bitcoin::Network; +use bdk_wallet::bitcoin::OutPoint as BdkOutPoint; use bdk_wallet::bitcoin::Psbt as BdkPsbt; use bdk_wallet::bitcoin::ScriptBuf as BdkScriptBuf; use bdk_wallet::bitcoin::Transaction as BdkTransaction; use bdk_wallet::bitcoin::TxIn as BdkTxIn; use bdk_wallet::bitcoin::TxOut as BdkTxOut; -use bdk_wallet::bitcoin::{OutPoint as BdkOutPoint, Txid}; +use bdk_wallet::bitcoin::Txid as BitcoinTxid; +use bdk_wallet::bitcoin::Wtxid as BitcoinWtxid; use bdk_wallet::miniscript::psbt::PsbtExt; use bdk_wallet::serde_json; @@ -51,7 +57,7 @@ impl From<&BdkOutPoint> for OutPoint { impl From for BdkOutPoint { fn from(outpoint: OutPoint) -> Self { BdkOutPoint { - txid: Txid::from_str(&outpoint.txid).unwrap(), + txid: BitcoinTxid::from_str(&outpoint.txid).unwrap(), vout: outpoint.vout, } } @@ -575,6 +581,42 @@ impl From<&BdkTxOut> for TxOut { } } +/// A bitcoin Block hash +#[derive(Debug, PartialEq, Eq, std::hash::Hash, uniffi::Object)] +#[uniffi::export(Display, Eq, Hash)] +pub struct BlockHash(pub(crate) BitcoinBlockHash); + +impl_hash_like!(BlockHash, BitcoinBlockHash); + +/// A bitcoin transaction identifier +#[derive(Debug, PartialEq, Eq, std::hash::Hash, uniffi::Object)] +#[uniffi::export(Display, Eq, Hash)] +pub struct Txid(pub(crate) BitcoinTxid); + +impl_hash_like!(Txid, BitcoinTxid); + +/// A bitcoin transaction identifier, including witness data. +/// For transactions with no SegWit inputs, the `txid` will be equivalent to `wtxid`. +#[derive(Debug, PartialEq, Eq, std::hash::Hash, uniffi::Object)] +#[uniffi::export(Display, Eq, Hash)] +pub struct Wtxid(pub(crate) BitcoinWtxid); + +impl_hash_like!(Wtxid, BitcoinWtxid); + +/// A collision-proof unique identifier for a descriptor. +#[derive(Debug, PartialEq, Eq, std::hash::Hash, uniffi::Object)] +#[uniffi::export(Display, Eq, Hash)] +pub struct DescriptorId(pub(crate) BitcoinSha256Hash); + +impl_hash_like!(DescriptorId, BitcoinSha256Hash); + +/// The merkle root of the merkle tree corresponding to a block's transactions. +#[derive(Debug, PartialEq, Eq, std::hash::Hash, uniffi::Object)] +#[uniffi::export(Display, Eq, Hash)] +pub struct TxMerkleNode(pub(crate) BitcoinDoubleSha256Hash); + +impl_hash_like!(TxMerkleNode, BitcoinDoubleSha256Hash); + #[cfg(test)] mod tests { use crate::bitcoin::Address; diff --git a/bdk-ffi/src/error.rs b/bdk-ffi/src/error.rs index 0da00a0a..ebd14ec3 100644 --- a/bdk-ffi/src/error.rs +++ b/bdk-ffi/src/error.rs @@ -1507,6 +1507,12 @@ impl From for TransactionError { } } +#[derive(Debug, thiserror::Error, uniffi::Error)] +pub enum HashParseError { + #[error("invalid hash: expected length 32 bytes, got {len} bytes")] + InvalidHash { len: u32 }, +} + impl From for SqliteError { fn from(error: BdkSqliteError) -> Self { SqliteError::Sqlite { diff --git a/bdk-ffi/src/macros.rs b/bdk-ffi/src/macros.rs index faa1b5a3..9c089eb0 100644 --- a/bdk-ffi/src/macros.rs +++ b/bdk-ffi/src/macros.rs @@ -19,3 +19,32 @@ macro_rules! impl_into_core_type { } }; } + +#[macro_export] +macro_rules! impl_hash_like { + ($ffi_type:ident, $core_type:ident) => { + #[uniffi::export] + impl $ffi_type { + /// Construct a hash-like type from 32 bytes. + #[uniffi::constructor] + pub fn from_bytes(bytes: Vec) -> Result { + let hash_like: $core_type = deserialize(&bytes).map_err(|_| { + let len = bytes.len() as u32; + HashParseError::InvalidHash { len } + })?; + Ok(Self(hash_like)) + } + + /// Serialize this type into a 32 byte array. + pub fn serialize(&self) -> Vec { + serialize(&self.0) + } + } + + impl std::fmt::Display for $ffi_type { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } + } + }; +} From d6123973c4bde61753f7d8c35ab9afb47ff6b1e2 Mon Sep 17 00:00:00 2001 From: rustaceanrob Date: Wed, 7 May 2025 10:35:39 +0100 Subject: [PATCH 2/4] feat: add wallet changeset primitives --- bdk-ffi/src/bitcoin.rs | 50 +++++++- bdk-ffi/src/types.rs | 199 ++++++++++++++++++++++++++++-- bdk-ffi/tests/bindings/test.kts | 4 +- bdk-ffi/tests/bindings/test.py | 4 +- bdk-ffi/tests/bindings/test.swift | 2 +- 5 files changed, 242 insertions(+), 17 deletions(-) diff --git a/bdk-ffi/src/bitcoin.rs b/bdk-ffi/src/bitcoin.rs index d45b21de..9e2f6e18 100644 --- a/bdk-ffi/src/bitcoin.rs +++ b/bdk-ffi/src/bitcoin.rs @@ -37,10 +37,10 @@ use std::str::FromStr; use std::sync::{Arc, Mutex}; /// A reference to an unspent output by TXID and output index. -#[derive(Debug, Clone, Eq, PartialEq, uniffi:: Record)] +#[derive(Debug, Clone, Eq, PartialEq, std::hash::Hash, uniffi:: Record)] pub struct OutPoint { /// The transaction. - pub txid: String, + pub txid: Arc, /// The index of the output in the transaction. pub vout: u32, } @@ -48,21 +48,43 @@ pub struct OutPoint { impl From<&BdkOutPoint> for OutPoint { fn from(outpoint: &BdkOutPoint) -> Self { OutPoint { - txid: outpoint.txid.to_string(), + txid: Arc::new(Txid(outpoint.txid)), vout: outpoint.vout, } } } +impl From for OutPoint { + fn from(value: BdkOutPoint) -> Self { + Self { + txid: Arc::new(Txid(value.txid)), + vout: value.vout, + } + } +} + impl From for BdkOutPoint { fn from(outpoint: OutPoint) -> Self { BdkOutPoint { - txid: BitcoinTxid::from_str(&outpoint.txid).unwrap(), + txid: BitcoinTxid::from_raw_hash(outpoint.txid.0.into()), vout: outpoint.vout, } } } +/// An [`OutPoint`] suitable as a key in a hash map. +#[derive(Debug, PartialEq, Eq, std::hash::Hash, uniffi::Object)] +#[uniffi::export(Debug, Eq, Hash)] +pub struct HashableOutPoint(pub(crate) OutPoint); + +#[uniffi::export] +impl HashableOutPoint { + /// Get the internal [`OutPoint`] + pub fn outpoint(&self) -> OutPoint { + self.0.clone() + } +} + /// Represents fee rate. /// /// This is an integer type representing fee rate in sat/kwu. It provides protection against mixing @@ -547,7 +569,7 @@ impl From<&BdkTxIn> for TxIn { fn from(tx_in: &BdkTxIn) -> Self { TxIn { previous_output: OutPoint { - txid: tx_in.previous_output.txid.to_string(), + txid: Arc::new(Txid(tx_in.previous_output.txid)), vout: tx_in.previous_output.vout, }, script_sig: Arc::new(Script(tx_in.script_sig.clone())), @@ -581,6 +603,24 @@ impl From<&BdkTxOut> for TxOut { } } +impl From for TxOut { + fn from(tx_out: BdkTxOut) -> Self { + Self { + value: tx_out.value.to_sat(), + script_pubkey: Arc::new(Script(tx_out.script_pubkey)), + } + } +} + +impl From for BdkTxOut { + fn from(tx_out: TxOut) -> Self { + Self { + value: BdkAmount::from_sat(tx_out.value), + script_pubkey: tx_out.script_pubkey.0.clone(), + } + } +} + /// A bitcoin Block hash #[derive(Debug, PartialEq, Eq, std::hash::Hash, uniffi::Object)] #[uniffi::export(Display, Eq, Hash)] diff --git a/bdk-ffi/src/types.rs b/bdk-ffi/src/types.rs index b7710663..7810302f 100644 --- a/bdk-ffi/src/types.rs +++ b/bdk-ffi/src/types.rs @@ -1,4 +1,6 @@ -use crate::bitcoin::{Address, Amount, OutPoint, Script, Transaction, TxOut}; +use crate::bitcoin::{ + Address, Amount, BlockHash, DescriptorId, HashableOutPoint, OutPoint, Script, Transaction, TxOut, Txid +}; use crate::error::{CreateTxError, RequestBuilderError}; use bdk_core::bitcoin::absolute::LockTime as BdkLockTime; @@ -23,7 +25,7 @@ use bdk_wallet::Balance as BdkBalance; use bdk_wallet::LocalOutput as BdkLocalOutput; use bdk_wallet::Update as BdkUpdate; -use std::collections::HashMap; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::convert::TryFrom; use std::sync::{Arc, Mutex}; @@ -66,7 +68,7 @@ impl From> for ChainPosition { } => { let block_id = BlockId { height: anchor.block_id.height, - hash: anchor.block_id.hash.to_string(), + hash: Arc::new(BlockHash(anchor.block_id.hash)), }; ChainPosition::Confirmed { confirmation_block_time: ConfirmationBlockTime { @@ -84,7 +86,7 @@ impl From> for ChainPosition { } /// Represents the confirmation block and time of a transaction. -#[derive(Debug, uniffi::Record)] +#[derive(Debug, PartialEq, Eq, std::hash::Hash, uniffi::Record)] pub struct ConfirmationBlockTime { /// The anchor block. pub block_id: BlockId, @@ -92,13 +94,49 @@ pub struct ConfirmationBlockTime { pub confirmation_time: u64, } +impl From for ConfirmationBlockTime { + fn from(value: BdkConfirmationBlockTime) -> Self { + Self { + block_id: value.block_id.into(), + confirmation_time: value.confirmation_time, + } + } +} + +impl From for BdkConfirmationBlockTime { + fn from(value: ConfirmationBlockTime) -> Self { + Self { + block_id: value.block_id.into(), + confirmation_time: value.confirmation_time, + } + } +} + /// A reference to a block in the canonical chain. -#[derive(Debug, uniffi::Record)] +#[derive(Debug, PartialEq, Eq, std::hash::Hash, uniffi::Record)] pub struct BlockId { /// The height of the block. pub height: u32, /// The hash of the block. - pub hash: String, + pub hash: Arc, +} + +impl From for BlockId { + fn from(value: bdk_wallet::chain::BlockId) -> Self { + Self { + height: value.height, + hash: Arc::new(BlockHash(value.hash)), + } + } +} + +impl From for bdk_wallet::chain::BlockId { + fn from(value: BlockId) -> Self { + Self { + height: value.height, + hash: value.hash.0, + } + } } /// A transaction that is deemed to be part of the canonical history. @@ -203,7 +241,7 @@ impl From for LocalOutput { fn from(local_utxo: BdkLocalOutput) -> Self { LocalOutput { outpoint: OutPoint { - txid: local_utxo.outpoint.txid.to_string(), + txid: Arc::new(Txid(local_utxo.outpoint.txid)), vout: local_utxo.outpoint.vout, }, txout: TxOut { @@ -711,3 +749,150 @@ pub struct UnconfirmedTx { pub tx: Arc, pub last_seen: u64, } + +/// Mapping of descriptors to their last revealed index. +#[derive(Debug, uniffi::Record)] +pub struct IndexerChangeSet { + pub last_revealed: HashMap, u32>, +} + +impl From for IndexerChangeSet { + fn from(mut value: bdk_wallet::chain::indexer::keychain_txout::ChangeSet) -> Self { + let mut changes = HashMap::new(); + for (id, index) in core::mem::take(&mut value.last_revealed) { + changes.insert(Arc::new(DescriptorId(id.0)), index); + } + Self { + last_revealed: changes, + } + } +} + +impl From for bdk_wallet::chain::indexer::keychain_txout::ChangeSet { + fn from(mut value: IndexerChangeSet) -> Self { + let mut changes = BTreeMap::new(); + for (id, index) in core::mem::take(&mut value.last_revealed) { + let descriptor_id = bdk_wallet::chain::DescriptorId(id.0); + changes.insert(descriptor_id, index); + } + Self { + last_revealed: changes, + } + } +} + +/// The hash added or removed at the given height. +#[derive(Debug, uniffi::Record)] +pub struct ChainChange { + /// Effected height + pub height: u32, + /// A hash was added or must be removed. + pub hash: Option>, +} + +/// Changes to the local chain +#[derive(Debug, uniffi::Record)] +pub struct LocalChainChangeSet { + pub changes: Vec, +} + +impl From for LocalChainChangeSet { + fn from(mut value: bdk_wallet::chain::local_chain::ChangeSet) -> Self { + let mut changes = Vec::with_capacity(value.blocks.len()); + for (height, hash) in core::mem::take(&mut value.blocks) { + let hash = hash.map(|h| Arc::new(BlockHash(h))); + let change = ChainChange { height, hash }; + changes.push(change); + } + Self { changes } + } +} + +impl From for bdk_wallet::chain::local_chain::ChangeSet { + fn from(mut value: LocalChainChangeSet) -> Self { + let mut changes = BTreeMap::new(); + for change in core::mem::take(&mut value.changes) { + let height = change.height; + let hash = change.hash.map(|h| h.0); + changes.insert(height, hash); + } + Self { blocks: changes } + } +} + +#[derive(Debug, uniffi::Record)] +pub struct Anchor { + pub confirmation_block_time: ConfirmationBlockTime, + pub txid: Arc, +} + +#[derive(Debug, uniffi::Record)] +pub struct TxGraphChangeSet { + pub txs: Vec>, + pub txouts: HashMap, TxOut>, + pub anchors: Vec, + pub last_seen: HashMap, u64>, +} + +impl From> for TxGraphChangeSet { + fn from(mut value: bdk_wallet::chain::tx_graph::ChangeSet) -> Self { + let btree_txs = core::mem::take(&mut value.txs); + let txs = btree_txs + .into_iter() + .map(|tx| Arc::new(tx.as_ref().into())) + .collect::>>(); + let mut txouts = HashMap::new(); + for (outpoint, txout) in core::mem::take(&mut value.txouts) { + txouts.insert(Arc::new(HashableOutPoint(outpoint.into())), txout.into()); + } + let mut anchors = Vec::new(); + for anchor in core::mem::take(&mut value.anchors) { + let confirmation_block_time = anchor.0.into(); + let txid = Arc::new(Txid(anchor.1)); + let anchor = Anchor { + confirmation_block_time, + txid, + }; + anchors.push(anchor); + } + let mut last_seens = HashMap::new(); + for (txid, time) in core::mem::take(&mut value.last_seen) { + last_seens.insert(Arc::new(Txid(txid)), time); + } + TxGraphChangeSet { + txs, + txouts, + anchors, + last_seen: last_seens, + } + } +} + +impl From for bdk_wallet::chain::tx_graph::ChangeSet { + fn from(mut value: TxGraphChangeSet) -> Self { + let mut txs = BTreeSet::new(); + for tx in core::mem::take(&mut value.txs) { + let tx = Arc::new(tx.as_ref().into()); + txs.insert(tx); + } + let mut txouts = BTreeMap::new(); + for txout in core::mem::take(&mut value.txouts) { + txouts.insert(txout.0.outpoint().into(), txout.1.into()); + } + let mut anchors = BTreeSet::new(); + for anchor in core::mem::take(&mut value.anchors) { + let txid = anchor.txid.0; + anchors.insert((anchor.confirmation_block_time.into(), txid)); + } + let mut last_seen = BTreeMap::new(); + for (txid, time) in core::mem::take(&mut value.last_seen) { + last_seen.insert(txid.0, time); + } + Self { + txs, + txouts, + anchors, + last_seen, + } + } +} diff --git a/bdk-ffi/tests/bindings/test.kts b/bdk-ffi/tests/bindings/test.kts index ffb52291..4f7f2d1b 100644 --- a/bdk-ffi/tests/bindings/test.kts +++ b/bdk-ffi/tests/bindings/test.kts @@ -4,10 +4,10 @@ */ import org.bitcoindevkit.bitcoin.Network -import org.bitcoindevkit.BlockId +import org.bitcoindevkit.Condition // A type from bitcoin-ffi val network = Network.TESTNET // A type from bdk-ffi -val blockId = BlockId(0uL, "abcd") +val condition = Condition(null, null) diff --git a/bdk-ffi/tests/bindings/test.py b/bdk-ffi/tests/bindings/test.py index 4077ca8e..d3f9494b 100644 --- a/bdk-ffi/tests/bindings/test.py +++ b/bdk-ffi/tests/bindings/test.py @@ -1,4 +1,4 @@ -from bdkpython import BlockId +from bdkpython import Condition from bdkpython.bitcoin import Network import unittest @@ -11,7 +11,7 @@ def test_some_enum(self): # A type from the bdk-ffi library def test_some_dict(self): - block_id = BlockId(height=0, hash="abcd") + condition = Condition(csv = None, timelock = None) if __name__=='__main__': unittest.main() diff --git a/bdk-ffi/tests/bindings/test.swift b/bdk-ffi/tests/bindings/test.swift index 8e9ea599..68cb3b1d 100644 --- a/bdk-ffi/tests/bindings/test.swift +++ b/bdk-ffi/tests/bindings/test.swift @@ -10,4 +10,4 @@ import BitcoinDevKit let network = Network.testnet // A type from the bdk-ffi library -let blockId = BlockId(height: 32, hash: "abcd") +let condition = Condition(csv: nil, timelock: nil) From 1af01758aa4044441311dccc2f3cf60fda3f18ac Mon Sep 17 00:00:00 2001 From: rustaceanrob Date: Wed, 7 May 2025 13:59:40 +0100 Subject: [PATCH 3/4] feat: convert between FFI/Rust changesets --- bdk-ffi/src/bdk.udl | 4 --- bdk-ffi/src/lib.rs | 1 - bdk-ffi/src/types.rs | 62 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/bdk-ffi/src/bdk.udl b/bdk-ffi/src/bdk.udl index e0fbbf31..e2e4b332 100644 --- a/bdk-ffi/src/bdk.udl +++ b/bdk-ffi/src/bdk.udl @@ -304,10 +304,6 @@ interface FullScanScriptInspector { void inspect(KeychainKind keychain, u32 index, Script script); }; -/// A changeset for [`Wallet`]. -[Remote] -interface ChangeSet {}; - // ------------------------------------------------------------------------ // bdk_wallet crate - wallet module // ------------------------------------------------------------------------ diff --git a/bdk-ffi/src/lib.rs b/bdk-ffi/src/lib.rs index a00b727e..6bdef52e 100644 --- a/bdk-ffi/src/lib.rs +++ b/bdk-ffi/src/lib.rs @@ -47,7 +47,6 @@ use crate::types::SyncScriptInspector; use bdk_wallet::bitcoin::Network; use bdk_wallet::keys::bip39::WordCount; -use bdk_wallet::ChangeSet; use bdk_wallet::KeychainKind; uniffi::include_scaffolding!("bdk"); diff --git a/bdk-ffi/src/types.rs b/bdk-ffi/src/types.rs index 7810302f..affe0b88 100644 --- a/bdk-ffi/src/types.rs +++ b/bdk-ffi/src/types.rs @@ -1,7 +1,9 @@ use crate::bitcoin::{ - Address, Amount, BlockHash, DescriptorId, HashableOutPoint, OutPoint, Script, Transaction, TxOut, Txid + Address, Amount, BlockHash, DescriptorId, HashableOutPoint, OutPoint, Script, Transaction, + TxOut, Txid, }; use crate::error::{CreateTxError, RequestBuilderError}; +use crate::keys::DescriptorPublicKey; use bdk_core::bitcoin::absolute::LockTime as BdkLockTime; use bdk_core::spk_client::SyncItem; @@ -896,3 +898,61 @@ impl From for bdk_wallet::chain::tx_graph::ChangeSet>, + pub change_descriptor: Option>, + pub network: Option, + pub local_chain: LocalChainChangeSet, + pub tx_graph: TxGraphChangeSet, + pub indexer: IndexerChangeSet, +} + +impl From for bdk_wallet::ChangeSet { + fn from(value: ChangeSet) -> Self { + let descriptor = value.descriptor.map(|d| { + let str_repr = d.to_string(); + str_repr.parse::>().unwrap() + }); + let change_descriptor = value.change_descriptor.map(|d| { + let str_repr = d.to_string(); + str_repr.parse::>().unwrap() + }); + let network = value.network; + let local_chain = value.local_chain.into(); + let tx_graph = value.tx_graph.into(); + let indexer = value.indexer.into(); + Self { + descriptor, + change_descriptor, + network, + local_chain, + tx_graph, + indexer, + } + } +} + +impl From for ChangeSet { + fn from(value: bdk_wallet::ChangeSet) -> Self { + let descriptor = value + .descriptor + .map(|d| Arc::new(DescriptorPublicKey::from_string(d.to_string()).unwrap())); + let change_descriptor = value + .change_descriptor + .map(|d| Arc::new(DescriptorPublicKey::from_string(d.to_string()).unwrap())); + let network = value.network; + let local_chain = value.local_chain.into(); + let tx_graph = value.tx_graph.into(); + let indexer = value.indexer.into(); + Self { + descriptor, + change_descriptor, + network, + local_chain, + tx_graph, + indexer, + } + } +} From f04bdc43e879a74e129a62423e6fd5e04fe45ac4 Mon Sep 17 00:00:00 2001 From: rustaceanrob Date: Thu, 8 May 2025 12:07:34 +0100 Subject: [PATCH 4/4] feat(wallet): add arbitrary persistence --- bdk-ffi/src/bdk.udl | 2 +- bdk-ffi/src/error.rs | 18 ++++-------- bdk-ffi/src/store.rs | 66 +++++++++++++++++++++++++++++++++++++++++-- bdk-ffi/src/types.rs | 16 +++++------ bdk-ffi/src/wallet.rs | 46 +++++++++++++++--------------- 5 files changed, 101 insertions(+), 47 deletions(-) diff --git a/bdk-ffi/src/bdk.udl b/bdk-ffi/src/bdk.udl index e2e4b332..cb0fcae3 100644 --- a/bdk-ffi/src/bdk.udl +++ b/bdk-ffi/src/bdk.udl @@ -197,7 +197,7 @@ interface ParseAmountError { [Error] interface PersistenceError { - Write(string error_message); + Reason(string error_message); }; [Error] diff --git a/bdk-ffi/src/error.rs b/bdk-ffi/src/error.rs index ebd14ec3..2a2087ad 100644 --- a/bdk-ffi/src/error.rs +++ b/bdk-ffi/src/error.rs @@ -565,7 +565,7 @@ pub enum ParseAmountError { #[derive(Debug, thiserror::Error)] pub enum PersistenceError { #[error("writing to persistence error: {error_message}")] - Write { error_message: String }, + Reason { error_message: String }, } #[derive(Debug, thiserror::Error)] @@ -745,12 +745,6 @@ pub enum SignerError { Psbt { error_message: String }, } -#[derive(Debug, thiserror::Error, uniffi::Error)] -pub enum SqliteError { - #[error("sqlite error: {rusqlite_error}")] - Sqlite { rusqlite_error: String }, -} - #[derive(Debug, thiserror::Error)] pub enum TransactionError { #[error("io error")] @@ -1343,7 +1337,7 @@ impl From for ParseAmountError { impl From for PersistenceError { fn from(error: std::io::Error) -> Self { - PersistenceError::Write { + PersistenceError::Reason { error_message: error.to_string(), } } @@ -1513,10 +1507,10 @@ pub enum HashParseError { InvalidHash { len: u32 }, } -impl From for SqliteError { +impl From for PersistenceError { fn from(error: BdkSqliteError) -> Self { - SqliteError::Sqlite { - rusqlite_error: error.to_string(), + PersistenceError::Reason { + error_message: error.to_string(), } } } @@ -1941,7 +1935,7 @@ mod test { "writing to persistence error: unable to persist the new address", ), ( - PersistenceError::Write { + PersistenceError::Reason { error_message: "failed to write to storage".to_string(), }, "writing to persistence error: failed to write to storage", diff --git a/bdk-ffi/src/store.rs b/bdk-ffi/src/store.rs index 7df64891..3c01ec8a 100644 --- a/bdk-ffi/src/store.rs +++ b/bdk-ffi/src/store.rs @@ -1,7 +1,11 @@ -use crate::error::SqliteError; +use crate::error::PersistenceError; +use crate::types::ChangeSet; use bdk_wallet::rusqlite::Connection as BdkConnection; +use bdk_wallet::WalletPersister; +use std::ops::DerefMut; +use std::sync::Arc; use std::sync::Mutex; use std::sync::MutexGuard; @@ -14,14 +18,14 @@ impl Connection { /// Open a new connection to a SQLite database. If a database does not exist at the path, one is /// created. #[uniffi::constructor] - pub fn new(path: String) -> Result { + pub fn new(path: String) -> Result { let connection = BdkConnection::open(path)?; Ok(Self(Mutex::new(connection))) } /// Open a new connection to an in-memory SQLite database. #[uniffi::constructor] - pub fn new_in_memory() -> Result { + pub fn new_in_memory() -> Result { let connection = BdkConnection::open_in_memory()?; Ok(Self(Mutex::new(connection))) } @@ -32,3 +36,59 @@ impl Connection { self.0.lock().expect("must lock") } } + +#[uniffi::export] +pub trait Persister: Send + Sync { + fn initialize(&self) -> Result; + + fn persist(&self, changeset: &ChangeSet) -> Result<(), PersistenceError>; +} + +impl Persister for Connection { + fn initialize(&self) -> Result { + let mut store = self.get_store(); + let mut db = store.deref_mut(); + let changeset_res = bdk_wallet::rusqlite::Connection::initialize(&mut db); + let changeset = changeset_res.map_err(|e| PersistenceError::Reason { + error_message: e.to_string(), + })?; + Ok(changeset.into()) + } + + fn persist(&self, changeset: &ChangeSet) -> Result<(), PersistenceError> { + let mut store = self.get_store(); + let mut db = store.deref_mut(); + let changeset = changeset.clone().into(); + bdk_wallet::rusqlite::Connection::persist(&mut db, &changeset).map_err(|e| { + PersistenceError::Reason { + error_message: e.to_string(), + } + }) + } +} + +pub(crate) struct AnyPersistence { + inner: Arc, +} + +impl AnyPersistence { + pub(crate) fn new(persister: Arc) -> Self { + Self { inner: persister } + } +} + +impl WalletPersister for AnyPersistence { + type Error = PersistenceError; + + fn persist(persister: &mut Self, changeset: &bdk_wallet::ChangeSet) -> Result<(), Self::Error> { + let changeset = changeset.clone().into(); + persister.inner.persist(&changeset) + } + + fn initialize(persister: &mut Self) -> Result { + persister + .inner + .initialize() + .map(|changeset| changeset.into()) + } +} diff --git a/bdk-ffi/src/types.rs b/bdk-ffi/src/types.rs index affe0b88..0c2153d0 100644 --- a/bdk-ffi/src/types.rs +++ b/bdk-ffi/src/types.rs @@ -88,7 +88,7 @@ impl From> for ChainPosition { } /// Represents the confirmation block and time of a transaction. -#[derive(Debug, PartialEq, Eq, std::hash::Hash, uniffi::Record)] +#[derive(Debug, Clone, PartialEq, Eq, std::hash::Hash, uniffi::Record)] pub struct ConfirmationBlockTime { /// The anchor block. pub block_id: BlockId, @@ -115,7 +115,7 @@ impl From for BdkConfirmationBlockTime { } /// A reference to a block in the canonical chain. -#[derive(Debug, PartialEq, Eq, std::hash::Hash, uniffi::Record)] +#[derive(Debug, Clone, PartialEq, Eq, std::hash::Hash, uniffi::Record)] pub struct BlockId { /// The height of the block. pub height: u32, @@ -753,7 +753,7 @@ pub struct UnconfirmedTx { } /// Mapping of descriptors to their last revealed index. -#[derive(Debug, uniffi::Record)] +#[derive(Debug, Clone, uniffi::Record)] pub struct IndexerChangeSet { pub last_revealed: HashMap, u32>, } @@ -784,7 +784,7 @@ impl From for bdk_wallet::chain::indexer::keychain_txout::Chan } /// The hash added or removed at the given height. -#[derive(Debug, uniffi::Record)] +#[derive(Debug, Clone, uniffi::Record)] pub struct ChainChange { /// Effected height pub height: u32, @@ -793,7 +793,7 @@ pub struct ChainChange { } /// Changes to the local chain -#[derive(Debug, uniffi::Record)] +#[derive(Debug, Clone, uniffi::Record)] pub struct LocalChainChangeSet { pub changes: Vec, } @@ -822,13 +822,13 @@ impl From for bdk_wallet::chain::local_chain::ChangeSet { } } -#[derive(Debug, uniffi::Record)] +#[derive(Debug, Clone, uniffi::Record)] pub struct Anchor { pub confirmation_block_time: ConfirmationBlockTime, pub txid: Arc, } -#[derive(Debug, uniffi::Record)] +#[derive(Debug, Clone, uniffi::Record)] pub struct TxGraphChangeSet { pub txs: Vec>, pub txouts: HashMap, TxOut>, @@ -899,7 +899,7 @@ impl From for bdk_wallet::chain::tx_graph::ChangeSet>, pub change_descriptor: Option>, diff --git a/bdk-ffi/src/wallet.rs b/bdk-ffi/src/wallet.rs index 8fed093b..fabf5a82 100644 --- a/bdk-ffi/src/wallet.rs +++ b/bdk-ffi/src/wallet.rs @@ -2,20 +2,18 @@ use crate::bitcoin::{Amount, FeeRate, OutPoint, Psbt, Script, Transaction}; use crate::descriptor::Descriptor; use crate::error::{ CalculateFeeError, CannotConnectError, CreateWithPersistError, DescriptorError, - LoadWithPersistError, SignerError, SqliteError, TxidParseError, + LoadWithPersistError, PersistenceError, SignerError, TxidParseError, }; -use crate::store::Connection; +use crate::store::{AnyPersistence, Persister}; use crate::types::{ AddressInfo, Balance, CanonicalTx, FullScanRequestBuilder, KeychainAndIndex, LocalOutput, Policy, SentAndReceivedValues, SignOptions, SyncRequestBuilder, UnconfirmedTx, Update, }; use bdk_wallet::bitcoin::{Network, Txid}; -use bdk_wallet::rusqlite::Connection as BdkConnection; use bdk_wallet::signer::SignOptions as BdkSignOptions; use bdk_wallet::{KeychainKind, PersistedWallet, Wallet as BdkWallet}; -use std::borrow::BorrowMut; use std::str::FromStr; use std::sync::{Arc, Mutex, MutexGuard}; @@ -33,7 +31,7 @@ use std::sync::{Arc, Mutex, MutexGuard}; /// script pubkeys. See KeychainTxOutIndex::insert_descriptor() for more details. #[derive(uniffi::Object)] pub struct Wallet { - inner_mutex: Mutex>, + inner_mutex: Mutex>, } #[uniffi::export] @@ -46,17 +44,18 @@ impl Wallet { descriptor: Arc, change_descriptor: Arc, network: Network, - connection: Arc, + persistence: Arc, ) -> Result { let descriptor = descriptor.to_string_with_secret(); let change_descriptor = change_descriptor.to_string_with_secret(); - let mut binding = connection.get_store(); - let db: &mut BdkConnection = binding.borrow_mut(); - - let wallet: PersistedWallet = + let mut any_persist = AnyPersistence::new(persistence); + let wallet: PersistedWallet = BdkWallet::create(descriptor, change_descriptor) .network(network) - .create_wallet(db)?; + .create_wallet(&mut any_persist) + .map_err(|e| CreateWithPersistError::Persist { + error_message: e.to_string(), + })?; Ok(Wallet { inner_mutex: Mutex::new(wallet), @@ -70,18 +69,20 @@ impl Wallet { pub fn load( descriptor: Arc, change_descriptor: Arc, - connection: Arc, + persistence: Arc, ) -> Result { let descriptor = descriptor.to_string_with_secret(); let change_descriptor = change_descriptor.to_string_with_secret(); - let mut binding = connection.get_store(); - let db: &mut BdkConnection = binding.borrow_mut(); + let mut any_persist = AnyPersistence::new(persistence); - let wallet: PersistedWallet = BdkWallet::load() + let wallet: PersistedWallet = BdkWallet::load() .descriptor(KeychainKind::External, Some(descriptor)) .descriptor(KeychainKind::Internal, Some(change_descriptor)) .extract_keys() - .load_wallet(db)? + .load_wallet(&mut any_persist) + .map_err(|e| LoadWithPersistError::Persist { + error_message: e.to_string(), + })? .ok_or(LoadWithPersistError::CouldNotLoad)?; Ok(Wallet { @@ -407,19 +408,18 @@ impl Wallet { /// Returns whether any new changes were persisted. /// /// If the persister errors, the staged changes will not be cleared. - pub fn persist(&self, connection: Arc) -> Result { - let mut binding = connection.get_store(); - let db: &mut BdkConnection = binding.borrow_mut(); + pub fn persist(&self, connection: Arc) -> Result { + let mut persister = AnyPersistence::new(connection); self.get_wallet() - .persist(db) - .map_err(|e| SqliteError::Sqlite { - rusqlite_error: e.to_string(), + .persist(&mut persister) + .map_err(|e| PersistenceError::Reason { + error_message: e.to_string(), }) } } impl Wallet { - pub(crate) fn get_wallet(&self) -> MutexGuard> { + pub(crate) fn get_wallet(&self) -> MutexGuard> { self.inner_mutex.lock().expect("wallet") } }