Skip to content

Commit e6e00df

Browse files
committed
feat: add functions to persist the wallet
Also modified `simple_keyring.rs` to incorporate the new persistence functions.
1 parent 35ea57e commit e6e00df

File tree

7 files changed

+382
-125
lines changed

7 files changed

+382
-125
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,4 @@ name = "bitcoind_rpc"
7777

7878
[[example]]
7979
name = "simple_keyring"
80+
required-features = ["rusqlite"]

examples/simple_keyring.rs

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use bdk_wallet::keyring::KeyRing;
2-
use bdk_wallet::{KeychainKind, Wallet};
2+
use bdk_wallet::{KeychainKind, LoadParams, Wallet};
33
use bitcoin::Network;
44

55
static EXTERNAL_DESCRIPTOR: &str = "tr([5bc5d243/86'/1'/0']tpubDC72NVP1RK5qwy2QdEfWphDsUBAfBu7oiV6jEFooHP8tGQGFVUeFxhgZxuk1j6EQRJ1YsS3th2RyDgReRqCL4zqp4jtuV2z7gbiqDH2iyUS/0/*)";
@@ -8,18 +8,33 @@ static INTERNAL_DESCRIPTOR: &str = "tr([5bc5d243/86'/1'/0']tpubDC72NVP1RK5qwy2Qd
88
// Simple KeyRing, allowing us to build a standard 2-descriptor wallet with receive and change
99
// keychains.
1010

11+
use bdk_chain::rusqlite;
12+
1113
fn main() {
12-
let mut keyring: KeyRing<KeychainKind> = KeyRing::new(
13-
Network::Regtest,
14-
KeychainKind::External,
15-
EXTERNAL_DESCRIPTOR,
16-
)
17-
.unwrap();
18-
keyring
19-
.add_descriptor(KeychainKind::Internal, INTERNAL_DESCRIPTOR, false)
20-
.unwrap();
14+
let mut conn = rusqlite::Connection::open(".bdk_example_wallet.sqlite").unwrap();
15+
let params = LoadParams::new()
16+
.check_default(KeychainKind::External)
17+
.check_descriptor(KeychainKind::External, Some(EXTERNAL_DESCRIPTOR))
18+
.check_descriptor(KeychainKind::Internal, Some(INTERNAL_DESCRIPTOR))
19+
.check_genesis_hash(bitcoin::constants::genesis_block(Network::Regtest).block_hash())
20+
.check_network(Network::Regtest);
21+
let mut wallet = match Wallet::<KeychainKind>::from_sqlite(&mut conn, params).unwrap() {
22+
Some(wallet) => wallet,
23+
None => {
24+
let mut keyring: KeyRing<KeychainKind> = KeyRing::new(
25+
Network::Regtest,
26+
KeychainKind::External,
27+
EXTERNAL_DESCRIPTOR,
28+
)
29+
.unwrap();
30+
keyring
31+
.add_descriptor(KeychainKind::Internal, INTERNAL_DESCRIPTOR, false)
32+
.unwrap();
2133

22-
let mut wallet = Wallet::new(keyring);
34+
Wallet::new(keyring)
35+
}
36+
};
2337
let address = wallet.reveal_next_address(KeychainKind::External).unwrap();
24-
println!("Address at index {}: {}", address.index, address.address)
38+
println!("Address at index {}: {}", address.index, address.address);
39+
wallet.persist_to_sqlite(&mut conn).unwrap();
2540
}

src/keyring/changeset.rs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,139 @@ impl<K: Ord> Merge for ChangeSet<K> {
4545
self.network.is_none() && self.descriptors.is_empty()
4646
}
4747
}
48+
49+
#[cfg(feature = "rusqlite")]
50+
use chain::{
51+
rusqlite::{self, OptionalExtension},
52+
Impl,
53+
};
54+
55+
#[cfg(feature = "rusqlite")]
56+
use crate::CanBePersisted;
57+
58+
#[cfg(feature = "rusqlite")]
59+
impl<K> ChangeSet<K>
60+
where
61+
K: Ord + Clone + CanBePersisted,
62+
{
63+
/// Schema name for `KeyRing`
64+
pub const SCHEMA_NAME: &str = "bdk_keyring";
65+
/// Name of table storing the network
66+
pub const NETWORK_TABLE_NAME: &str = "bdk_network";
67+
/// Name of table storing the descriptors
68+
pub const DESCRIPTORS_TABLE_NAME: &str = "bdk_descriptor";
69+
70+
/// Returns the v0 sqlite schema for [`ChangeSet`]
71+
pub fn schema_v0() -> alloc::string::String {
72+
format!(
73+
"CREATE TABLE {} ( \
74+
id INTEGER PRIMARY KEY NOT NULL, \
75+
network TEXT NOT NULL \
76+
) STRICT ; \
77+
CREATE TABLE {} ( \
78+
keychain TEXT PRIMARY KEY NOT NULL, \
79+
descriptor TEXT UNIQUE NOT NULL, \
80+
is_default INTEGER NOT NULL CHECK ( is_default IN (0,1) ) \
81+
) STRICT ;",
82+
Self::NETWORK_TABLE_NAME,
83+
Self::DESCRIPTORS_TABLE_NAME
84+
)
85+
}
86+
87+
/// Initialize sqlite tables
88+
pub fn init_sqlite_tables(db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
89+
bdk_chain::rusqlite_impl::migrate_schema(db_tx, Self::SCHEMA_NAME, &[&Self::schema_v0()])?;
90+
Ok(())
91+
}
92+
93+
/// Construct the `KeyRing` from persistence
94+
///
95+
/// Remember to call [`Self::init_sqlite_tables`] beforehand.
96+
pub fn from_sqlite(db_tx: &rusqlite::Transaction) -> rusqlite::Result<Self> {
97+
let mut changeset = Self::default();
98+
let mut network_stmt = db_tx.prepare(&format!(
99+
"SELECT network FROM {} WHERE id = 0",
100+
Self::NETWORK_TABLE_NAME,
101+
))?;
102+
let row = network_stmt
103+
.query_row([], |row| row.get::<_, Impl<bitcoin::Network>>("network"))
104+
.optional()?;
105+
106+
if let Some(Impl(network)) = row {
107+
changeset.network = Some(network);
108+
}
109+
110+
let mut descriptor_stmt = db_tx.prepare(&format!(
111+
"SELECT keychain, descriptor, is_default FROM {}",
112+
Self::DESCRIPTORS_TABLE_NAME
113+
))?;
114+
115+
let rows = descriptor_stmt.query_map([], |row| {
116+
Ok((
117+
row.get::<_, K::Persistable>("keychain")?,
118+
row.get::<_, Impl<Descriptor<DescriptorPublicKey>>>("descriptor")?,
119+
row.get::<_, u8>("is_default")?,
120+
))
121+
})?;
122+
123+
for row in rows {
124+
let (keychain_persisted, Impl(descriptor), is_default) = row?;
125+
let keychain = K::from_persistable(keychain_persisted);
126+
changeset.descriptors.insert(keychain.clone(), descriptor);
127+
128+
if is_default == 1 {
129+
changeset.default_keychain = Some(keychain);
130+
}
131+
}
132+
133+
Ok(changeset)
134+
}
135+
136+
/// Persist the `KeyRing`
137+
///
138+
/// Remember to call [`Self::init_sqlite_tables`] beforehand.
139+
pub fn persist_to_sqlite(&self, db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
140+
use rusqlite::named_params;
141+
let mut network_stmt = db_tx.prepare_cached(&format!(
142+
"INSERT OR IGNORE INTO {}(id, network) VALUES(:id, :network)",
143+
Self::NETWORK_TABLE_NAME
144+
))?;
145+
146+
if let Some(network) = self.network {
147+
network_stmt.execute(named_params! {
148+
":id": 0,
149+
":network": Impl(network)
150+
})?;
151+
}
152+
153+
let mut descriptor_stmt = db_tx.prepare_cached(&format!(
154+
"INSERT OR IGNORE INTO {}(keychain, descriptor, is_default) VALUES(:keychain, :desc, :is_default)", Self::DESCRIPTORS_TABLE_NAME
155+
))?;
156+
157+
for (keychain, desc) in &self.descriptors {
158+
descriptor_stmt.execute(named_params! {
159+
":keychain": keychain.clone().to_persistable(),
160+
":desc": Impl(desc.clone()),
161+
":is_default": 0,
162+
})?;
163+
}
164+
165+
let mut remove_old_default_stmt = db_tx.prepare_cached(&format!(
166+
"UPDATE {} SET is_default = 0 WHERE is_default = 1",
167+
Self::DESCRIPTORS_TABLE_NAME,
168+
))?;
169+
170+
let mut add_default_stmt = db_tx.prepare_cached(&format!(
171+
"UPDATE {} SET is_default = 1 WHERE keychain = :keychain",
172+
Self::DESCRIPTORS_TABLE_NAME,
173+
))?;
174+
175+
if let Some(keychain) = &self.default_keychain {
176+
remove_old_default_stmt.execute(())?;
177+
add_default_stmt
178+
.execute(named_params! { ":keychain": keychain.clone().to_persistable(),})?;
179+
}
180+
181+
Ok(())
182+
}
183+
}

src/types.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ use bitcoin::{psbt, Weight};
1919

2020
use serde::{Deserialize, Serialize};
2121

22+
#[cfg(feature = "rusqlite")]
23+
use chain::rusqlite::{
24+
self,
25+
types::{FromSql, FromSqlResult, ToSql, ToSqlOutput, ValueRef},
26+
};
27+
2228
/// Types of keychains
2329
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
2430
pub enum KeychainKind {
@@ -56,6 +62,41 @@ impl AsRef<[u8]> for KeychainKind {
5662
}
5763
}
5864

65+
#[cfg(feature = "rusqlite")]
66+
impl FromSql for KeychainKind {
67+
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
68+
Ok(match value.as_str()? {
69+
"0" => KeychainKind::External,
70+
"1" => KeychainKind::Internal,
71+
_ => panic!("KeychainKind cannot be anything other than External(0) and Internal(1)"),
72+
})
73+
}
74+
}
75+
76+
#[cfg(feature = "rusqlite")]
77+
impl ToSql for KeychainKind {
78+
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
79+
Ok(match *self {
80+
KeychainKind::External => "0".into(),
81+
KeychainKind::Internal => "1".into(),
82+
})
83+
}
84+
}
85+
86+
#[cfg(feature = "rusqlite")]
87+
use crate::CanBePersisted;
88+
89+
#[cfg(feature = "rusqlite")]
90+
impl CanBePersisted for KeychainKind {
91+
type Persistable = KeychainKind;
92+
fn to_persistable(self) -> KeychainKind {
93+
self
94+
}
95+
fn from_persistable(persisted: KeychainKind) -> KeychainKind {
96+
persisted
97+
}
98+
}
99+
59100
/// An unspent output owned by a [`Wallet`].
60101
///
61102
/// [`Wallet`]: crate::Wallet

0 commit comments

Comments
 (0)