Skip to content

wip: Support N keychains #230

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 2 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
4 changes: 4 additions & 0 deletions wallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,7 @@ required-features = ["all-keys"]
name = "miniscriptc"
path = "examples/compiler.rs"
required-features = ["compiler"]

[[example]]
name = "keyring"
required-features = ["rusqlite"]
63 changes: 63 additions & 0 deletions wallet/examples/keyring.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#![allow(unused)]
#![allow(clippy::print_stdout)]

use bdk_chain::DescriptorExt;
use bdk_chain::DescriptorId;
use bitcoin::secp256k1::Secp256k1;
use bitcoin::Network;
use miniscript::{Descriptor, DescriptorPublicKey};

use bdk_wallet::chain as bdk_chain;
use bdk_wallet::multi_keychain::{KeyRing, Wallet};
use bdk_wallet::rusqlite;

// This example shows how to create a BDK wallet from a `KeyRing`.

fn main() -> anyhow::Result<()> {
let path = ".bdk_example_keyring.sqlite";
let mut conn = rusqlite::Connection::open(path)?;

let network = Network::Signet;

let desc = "wpkh([83737d5e/84'/1'/1']tpubDCzuCBKnZA5TNKhiJnASku7kq8Q4iqcVF82JV7mHo2NxWpXkLRbrJaGA5ToE7LCuWpcPErBbpDzbdWKN8aTdJzmRy1jQPmZvnqpwwDwCdy7/<0;1>/*)";
let desc2 = "tr([83737d5e/86'/1'/1']tpubDDR5GgtoxS8fNuSTJU6huqQKGzWshPaemb3UwFDoAXCsyakcQoRcFDMiGUVRX43Lofd7ZB82RcUvu1xnZ5oGZhbr43dRkY8xm2KGhpcq93o/<0;1>/*)";

let default_did: DescriptorId =
"6f3ba87443e825675b2b1cb8da505831422a7d214c515070570885180a1b2733".parse()?;

let mut wallet = match Wallet::from_sqlite(&mut conn)? {
Some(w) => w,
None => {
let mut keyring = KeyRing::new(network);
for multipath_desc in [desc, desc2] {
for (did, desc) in label_descriptors(multipath_desc) {
keyring.add_descriptor(did, desc);
}
}
let mut wallet = Wallet::new(keyring);
wallet.persist_to_sqlite(&mut conn)?;
wallet
}
};

let (indexed, addr) = wallet.reveal_next_address(default_did).unwrap();
println!("Address: {:?} {}", indexed, addr);

let changeset = wallet.persist_to_sqlite(&mut conn)?;
println!("Change persisted: {}", changeset.is_some());

Ok(())
}

/// Helper method to label descriptors by descriptor ID!
fn label_descriptors(
s: &str,
) -> impl Iterator<Item = (DescriptorId, Descriptor<DescriptorPublicKey>)> {
let desc = Descriptor::parse_descriptor(&Secp256k1::new(), s)
.expect("failed to parse descriptor")
.0;
desc.into_single_descriptors()
.expect("inavlid descriptor")
.into_iter()
.map(|desc| (desc.descriptor_id(), desc))
}
2 changes: 2 additions & 0 deletions wallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ pub mod test_utils;
mod types;
mod wallet;

pub mod multi_keychain;

pub(crate) use bdk_chain::collections;
#[cfg(feature = "rusqlite")]
pub use bdk_chain::rusqlite;
Expand Down
12 changes: 12 additions & 0 deletions wallet/src/multi_keychain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//! Module containing the multi-keychain [`Wallet`].

mod changeset;
pub mod keyring;
mod wallet;

pub use changeset::*;
pub use keyring::KeyRing;
pub use wallet::*;

/// Alias for [`DescriptorId`](bdk_chain::DescriptorId).
pub(crate) type Did = bdk_chain::DescriptorId;
227 changes: 227 additions & 0 deletions wallet/src/multi_keychain/changeset.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
use bdk_chain::{
indexed_tx_graph, keychain_txout, local_chain, tx_graph, ConfirmationBlockTime, DescriptorId,
Merge,
};
use bitcoin::Network;
use miniscript::{Descriptor, DescriptorPublicKey};
use serde::{Deserialize, Serialize};

use crate::multi_keychain::keyring;

/// Change set.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct ChangeSet<K: Ord> {
/// Keyring changeset.
pub keyring: keyring::ChangeSet<K>,
/// Changes to the [`LocalChain`](local_chain::LocalChain).
pub local_chain: local_chain::ChangeSet,
/// Changes to [`TxGraph`](tx_graph::TxGraph).
pub tx_graph: tx_graph::ChangeSet<ConfirmationBlockTime>,
/// Changes to [`KeychainTxOutIndex`](keychain_txout::KeychainTxOutIndex).
pub indexer: keychain_txout::ChangeSet,
}

impl<K: Ord> Default for ChangeSet<K> {
fn default() -> Self {
Self {
keyring: Default::default(),
local_chain: Default::default(),
tx_graph: Default::default(),
indexer: Default::default(),
}
}
}

impl<K: Ord> Merge for ChangeSet<K> {
fn merge(&mut self, other: Self) {
// merge keyring
self.keyring.merge(other.keyring);

// merge local chain, tx-graph, indexer
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);
}

fn is_empty(&self) -> bool {
self.keyring.is_empty()
&& self.local_chain.is_empty()
&& self.tx_graph.is_empty()
&& self.indexer.is_empty()
}
}

#[cfg(feature = "rusqlite")]
use bdk_chain::rusqlite;

#[cfg(feature = "rusqlite")]
impl ChangeSet<DescriptorId> {
/// Schema name for wallet.
pub const WALLET_SCHEMA_NAME: &'static str = "bdk_wallet";
/// Name of table to store wallet metainformation.
pub const WALLET_TABLE_NAME: &'static str = "bdk_wallet";
/// Name of table to store wallet descriptors.
pub const DESCRIPTORS_TABLE_NAME: &'static str = "bdk_descriptor";

/// Get v0 sqlite [ChangeSet] schema.
pub fn schema_v0() -> alloc::string::String {
format!(
"CREATE TABLE {} ( \
id INTEGER PRIMARY KEY NOT NULL, \
network TEXT NOT NULL \
); \
CREATE TABLE {} ( \
descriptor_id TEXT PRIMARY KEY NOT NULL, \
descriptor BLOB NOT NULL \
);",
Self::WALLET_TABLE_NAME,
Self::DESCRIPTORS_TABLE_NAME,
)
}

/// Initializes tables and returns the aggregate data if the database is non-empty
/// otherwise returns `Ok(None)`.
pub fn initialize(db_tx: &rusqlite::Transaction) -> rusqlite::Result<Option<Self>> {
Self::init_sqlite_tables(db_tx)?;
let changeset = Self::from_sqlite(db_tx)?;

if changeset.is_empty() {
Ok(None)
} else {
Ok(Some(changeset))
}
}

/// Initialize SQLite tables.
fn init_sqlite_tables(db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
crate::rusqlite_impl::migrate_schema(
db_tx,
Self::WALLET_SCHEMA_NAME,
&[&Self::schema_v0()],
)?;

local_chain::ChangeSet::init_sqlite_tables(db_tx)?;
tx_graph::ChangeSet::<ConfirmationBlockTime>::init_sqlite_tables(db_tx)?;
keychain_txout::ChangeSet::init_sqlite_tables(db_tx)?;

Ok(())
}

/// Construct self by reading all of the SQLite data. This should succeed
/// even if attempting to read an empty database.
fn from_sqlite(db_tx: &rusqlite::Transaction) -> rusqlite::Result<Self> {
use bdk_chain::Impl;
use rusqlite::OptionalExtension;
let mut changeset = Self::default();

let mut keyring = keyring::ChangeSet::default();

// Read network
let mut network_stmt = db_tx.prepare(&format!(
"SELECT network FROM {} WHERE id = 0",
Self::WALLET_TABLE_NAME,
))?;
let row = network_stmt
.query_row([], |row| row.get::<_, Impl<Network>>("network"))
.optional()?;
if let Some(Impl(network)) = row {
keyring.network = Some(network);
}

// Read descriptors
let mut descriptor_stmt = db_tx.prepare(&format!(
"SELECT descriptor_id, descriptor FROM {}",
Self::DESCRIPTORS_TABLE_NAME
))?;
let rows = descriptor_stmt.query_map([], |row| {
Ok((
row.get::<_, Impl<DescriptorId>>("descriptor_id")?,
row.get::<_, Impl<Descriptor<DescriptorPublicKey>>>("descriptor")?,
))
})?;
for row in rows {
let (Impl(did), Impl(descriptor)) = row?;
keyring.descriptors.insert(did, descriptor);
}

changeset.keyring = keyring;
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)?;

Ok(changeset)
}

/// Persist self to SQLite.
pub fn persist_to_sqlite(&self, db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
use chain::rusqlite::named_params;
use chain::Impl;

let keyring = &self.keyring;

// Write network
let mut network_stmt = db_tx.prepare_cached(&format!(
"REPLACE INTO {}(id, network) VALUES(:id, :network)",
Self::WALLET_TABLE_NAME,
))?;
if let Some(network) = keyring.network {
network_stmt.execute(named_params! {
":id": 0,
":network": Impl(network),
})?;
}

// Write descriptors
let mut descriptor_stmt = db_tx.prepare_cached(&format!(
"INSERT OR IGNORE INTO {}(descriptor_id, descriptor) VALUES(:descriptor_id, :descriptor)",
Self::DESCRIPTORS_TABLE_NAME,
))?;
for (&did, descriptor) in &keyring.descriptors {
descriptor_stmt.execute(named_params! {
":descriptor_id": Impl(did),
":descriptor": Impl(descriptor.clone()),
})?;
}

self.local_chain.persist_to_sqlite(db_tx)?;
self.tx_graph.persist_to_sqlite(db_tx)?;
self.indexer.persist_to_sqlite(db_tx)?;

Ok(())
}
}

impl<K: Ord> From<local_chain::ChangeSet> for ChangeSet<K> {
fn from(local_chain: local_chain::ChangeSet) -> Self {
Self {
local_chain,
..Default::default()
}
}
}

impl<K: Ord> From<indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet>>
for ChangeSet<K>
{
fn from(
indexed_tx_graph: indexed_tx_graph::ChangeSet<
ConfirmationBlockTime,
keychain_txout::ChangeSet,
>,
) -> Self {
Self {
tx_graph: indexed_tx_graph.tx_graph,
indexer: indexed_tx_graph.indexer,
..Default::default()
}
}
}

impl<K: Ord> From<keychain_txout::ChangeSet> for ChangeSet<K> {
fn from(indexer: keychain_txout::ChangeSet) -> Self {
Self {
indexer,
..Default::default()
}
}
}
Loading
Loading