Skip to content

Add abitrary persistence to Wallet #757

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

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
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
6 changes: 1 addition & 5 deletions bdk-ffi/src/bdk.udl
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ interface ParseAmountError {

[Error]
interface PersistenceError {
Write(string error_message);
Reason(string error_message);
};

[Error]
Expand Down Expand Up @@ -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
// ------------------------------------------------------------------------
Expand Down
100 changes: 91 additions & 9 deletions bdk-ffi/src/bitcoin.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -31,32 +37,54 @@ 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<Txid>,
/// The index of the output in the transaction.
pub vout: u32,
}

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<BdkOutPoint> for OutPoint {
fn from(value: BdkOutPoint) -> Self {
Self {
txid: Arc::new(Txid(value.txid)),
vout: value.vout,
}
}
}

impl From<OutPoint> for BdkOutPoint {
fn from(outpoint: OutPoint) -> Self {
BdkOutPoint {
txid: Txid::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
Expand Down Expand Up @@ -541,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())),
Expand Down Expand Up @@ -575,6 +603,60 @@ impl From<&BdkTxOut> for TxOut {
}
}

impl From<BdkTxOut> 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<TxOut> 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)]
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;
Expand Down
24 changes: 12 additions & 12 deletions bdk-ffi/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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")]
Expand Down Expand Up @@ -1343,7 +1337,7 @@ impl From<BdkParseAmountError> for ParseAmountError {

impl From<std::io::Error> for PersistenceError {
fn from(error: std::io::Error) -> Self {
PersistenceError::Write {
PersistenceError::Reason {
error_message: error.to_string(),
}
}
Expand Down Expand Up @@ -1507,10 +1501,16 @@ impl From<BdkEncodeError> for TransactionError {
}
}

impl From<BdkSqliteError> for SqliteError {
#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum HashParseError {
#[error("invalid hash: expected length 32 bytes, got {len} bytes")]
InvalidHash { len: u32 },
}

impl From<BdkSqliteError> for PersistenceError {
fn from(error: BdkSqliteError) -> Self {
SqliteError::Sqlite {
rusqlite_error: error.to_string(),
PersistenceError::Reason {
error_message: error.to_string(),
}
}
}
Expand Down Expand Up @@ -1935,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",
Expand Down
1 change: 0 additions & 1 deletion bdk-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
29 changes: 29 additions & 0 deletions bdk-ffi/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>) -> Result<Self, HashParseError> {
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<u8> {
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)
}
}
};
}
66 changes: 63 additions & 3 deletions bdk-ffi/src/store.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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<Self, SqliteError> {
pub fn new(path: String) -> Result<Self, PersistenceError> {
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<Self, SqliteError> {
pub fn new_in_memory() -> Result<Self, PersistenceError> {
let connection = BdkConnection::open_in_memory()?;
Ok(Self(Mutex::new(connection)))
}
Expand All @@ -32,3 +36,59 @@ impl Connection {
self.0.lock().expect("must lock")
}
}

#[uniffi::export]
pub trait Persister: Send + Sync {
fn initialize(&self) -> Result<ChangeSet, PersistenceError>;

fn persist(&self, changeset: &ChangeSet) -> Result<(), PersistenceError>;
}

impl Persister for Connection {
fn initialize(&self) -> Result<ChangeSet, PersistenceError> {
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<dyn Persister>,
}

impl AnyPersistence {
pub(crate) fn new(persister: Arc<dyn Persister>) -> 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<bdk_wallet::ChangeSet, Self::Error> {
persister
.inner
.initialize()
.map(|changeset| changeset.into())
}
}
Loading
Loading