Skip to content

Commit 1f974bc

Browse files
committed
wip: Support N keychains
1 parent 8eaf1c0 commit 1f974bc

File tree

13 files changed

+659
-226
lines changed

13 files changed

+659
-226
lines changed

examples/example_wallet_electrum/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use bdk_electrum::BdkElectrumClient;
77
use bdk_wallet::bitcoin::Amount;
88
use bdk_wallet::bitcoin::Network;
99
use bdk_wallet::chain::collections::HashSet;
10-
use bdk_wallet::{KeychainKind, SignOptions};
10+
use bdk_wallet::{Keychain, KeychainKind, SignOptions};
1111

1212
const DB_MAGIC: &str = "bdk_wallet_electrum_example";
1313
const SEND_AMOUNT: Amount = Amount::from_sat(5000);
@@ -53,7 +53,7 @@ fn main() -> Result<(), anyhow::Error> {
5353

5454
let request = wallet.start_full_scan().inspect({
5555
let mut stdout = std::io::stdout();
56-
let mut once = HashSet::<KeychainKind>::new();
56+
let mut once = HashSet::<Keychain>::new();
5757
move |k, spk_i, _| {
5858
if once.insert(k) {
5959
print!("\nScanning keychain [{:?}]", k);
Lines changed: 107 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,128 @@
1+
#![allow(unused)]
12
use std::{collections::BTreeSet, io::Write};
23

34
use anyhow::Ok;
45
use bdk_esplora::{esplora_client, EsploraAsyncExt};
56
use bdk_wallet::{
67
bitcoin::{Amount, Network},
8+
chain::{DescriptorExt, DescriptorId},
79
rusqlite::Connection,
8-
KeychainKind, SignOptions, Wallet,
10+
ChangeSet, CreateParams, Keychain, Keyring, SignOptions, Wallet,
911
};
1012

1113
const SEND_AMOUNT: Amount = Amount::from_sat(5000);
1214
const STOP_GAP: usize = 5;
1315
const PARALLEL_REQUESTS: usize = 5;
1416

15-
const DB_PATH: &str = "bdk-example-esplora-async.sqlite";
17+
// const DB_PATH: &str = "bdk-example-esplora-async.sqlite";
18+
const DB_PATH: &str = ".bdk_example_wallet_esplora_async.sqlite";
1619
const NETWORK: Network = Network::Signet;
17-
const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
18-
const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
20+
// const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
21+
// const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
1922
const ESPLORA_URL: &str = "http://signet.bitcoindevkit.net";
2023

24+
const MULTIPATH_DESCRIPTOR: &str = "wpkh([e273fe42/84'/1'/0']tpubDCmr3Luq75npLaYmRqqW1rLfSbfpnBXwLwAmUbR333fp95wjCHar3zoc9zSWovZFwrWr53mm3NTVqt6d1Pt6G26uf4etQjc3Pr5Hxe9QEQ2/<0;1>/*)";
25+
const PK_DESCRIPTOR: &str = "tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55)";
26+
// Desc ID of pk descriptor: "ef8a67b77b83797a1ad56504cc79e8c6990408265f1afdce72990b8a5baf7d3b"
27+
2128
#[tokio::main]
2229
async fn main() -> Result<(), anyhow::Error> {
23-
let mut conn = Connection::open(DB_PATH)?;
24-
25-
let wallet_opt = Wallet::load()
26-
.descriptor(KeychainKind::External, Some(EXTERNAL_DESC))
27-
.descriptor(KeychainKind::Internal, Some(INTERNAL_DESC))
28-
.extract_keys()
29-
.check_network(NETWORK)
30-
.load_wallet(&mut conn)?;
31-
let mut wallet = match wallet_opt {
32-
Some(wallet) => wallet,
33-
None => Wallet::create(EXTERNAL_DESC, INTERNAL_DESC)
34-
.network(NETWORK)
35-
.create_wallet(&mut conn)?,
36-
};
37-
38-
let address = wallet.next_unused_address(KeychainKind::External);
39-
wallet.persist(&mut conn)?;
40-
println!("Next unused address: ({}) {}", address.index, address);
41-
42-
let balance = wallet.balance();
43-
println!("Wallet balance before syncing: {}", balance.total());
44-
45-
print!("Syncing...");
46-
let client = esplora_client::Builder::new(ESPLORA_URL).build_async()?;
47-
48-
let request = wallet.start_full_scan().inspect({
49-
let mut stdout = std::io::stdout();
50-
let mut once = BTreeSet::<KeychainKind>::new();
51-
move |keychain, spk_i, _| {
52-
if once.insert(keychain) {
53-
print!("\nScanning keychain [{:?}]", keychain);
54-
}
55-
print!(" {:<3}", spk_i);
56-
stdout.flush().expect("must flush")
57-
}
58-
});
59-
60-
let update = client
61-
.full_scan(request, STOP_GAP, PARALLEL_REQUESTS)
62-
.await?;
63-
64-
wallet.apply_update(update)?;
65-
wallet.persist(&mut conn)?;
66-
println!();
67-
68-
let balance = wallet.balance();
69-
println!("Wallet balance after syncing: {}", balance.total());
70-
71-
if balance.total() < SEND_AMOUNT {
72-
println!(
73-
"Please send at least {} to the receiving address",
74-
SEND_AMOUNT
75-
);
76-
std::process::exit(0);
77-
}
78-
79-
let mut tx_builder = wallet.build_tx();
80-
tx_builder.add_recipient(address.script_pubkey(), SEND_AMOUNT);
81-
82-
let mut psbt = tx_builder.finish()?;
83-
let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
84-
assert!(finalized);
85-
86-
let tx = psbt.extract_tx()?;
87-
client.broadcast(&tx).await?;
88-
println!("Tx broadcasted! Txid: {}", tx.compute_txid());
30+
// let mut conn = Connection::open(DB_PATH)?;
31+
let mut conn = Connection::open_in_memory()?;
32+
33+
// Setup: Initialize Keyring from a list of descriptors
34+
let mut keyring = Keyring::new(NETWORK);
35+
let _ = keyring.add_descriptors([MULTIPATH_DESCRIPTOR, PK_DESCRIPTOR])?;
36+
37+
// Test 1: Create wallet with keyring and params
38+
let mut wallet = Wallet::with_keyring(keyring.clone())
39+
.network(NETWORK)
40+
.create_wallet(&mut conn)?;
41+
42+
assert_eq!(wallet.keychains().count(), 3);
43+
let desc_id: DescriptorId =
44+
"ef8a67b77b83797a1ad56504cc79e8c6990408265f1afdce72990b8a5baf7d3b".parse()?;
45+
let (keychain, index, addr) = wallet.new_address(desc_id).expect("should reveal address");
46+
println!("New address: {:?} {}", (keychain, index), addr);
47+
48+
// Test 2: Persist the keyring first and then load wallet from changeset
49+
// let changeset = keyring.initial_changeset();
50+
// let tx = conn.transaction()?;
51+
// ChangeSet::init_sqlite_tables(&tx)?;
52+
// changeset.persist_to_sqlite(&tx)?;
53+
// tx.commit()?;
54+
55+
// let wallet = Wallet::load()
56+
// .load_wallet(&mut conn)?
57+
// .expect("should have persisted wallet");
58+
59+
// assert_eq!(wallet.keychains().count(), 3);
60+
61+
// More example code
62+
63+
// let wallet_opt = Wallet::load()
64+
// .descriptor(0, Some(EXTERNAL_DESC))
65+
// .extract_keys()
66+
// .check_network(NETWORK)
67+
// .load_wallet(&mut conn)?;
68+
// let mut wallet = match wallet_opt {
69+
// Some(wallet) => wallet,
70+
// None => Wallet::create(EXTERNAL_DESC, INTERNAL_DESC)
71+
// .network(NETWORK)
72+
// .create_wallet(&mut conn)?,
73+
// };
74+
75+
// let address = wallet.next_unused_address(Keychain::ZERO);
76+
// wallet.persist(&mut conn)?;
77+
// println!("Next unused address: ({}) {}", address.index, address);
78+
79+
// let balance = wallet.balance();
80+
// println!("Wallet balance before syncing: {}", balance.total());
81+
82+
// print!("Syncing...");
83+
// let client = esplora_client::Builder::new(ESPLORA_URL).build_async()?;
84+
85+
// let request = wallet.start_full_scan().inspect({
86+
// let mut stdout = std::io::stdout();
87+
// let mut once = BTreeSet::<Keychain>::new();
88+
// move |keychain, spk_i, _| {
89+
// if once.insert(keychain) {
90+
// print!("\nScanning keychain [{:?}]", keychain);
91+
// }
92+
// print!(" {:<3}", spk_i);
93+
// stdout.flush().expect("must flush")
94+
// }
95+
// });
96+
97+
// let update = client
98+
// .full_scan(request, STOP_GAP, PARALLEL_REQUESTS)
99+
// .await?;
100+
101+
// wallet.apply_update(update)?;
102+
// wallet.persist(&mut conn)?;
103+
// println!();
104+
105+
// let balance = wallet.balance();
106+
// println!("Wallet balance after syncing: {}", balance.total());
107+
108+
// if balance.total() < SEND_AMOUNT {
109+
// println!(
110+
// "Please send at least {} to the receiving address",
111+
// SEND_AMOUNT
112+
// );
113+
// std::process::exit(0);
114+
// }
115+
116+
// let mut tx_builder = wallet.build_tx();
117+
// tx_builder.add_recipient(address.script_pubkey(), SEND_AMOUNT);
118+
119+
// let mut psbt = tx_builder.finish()?;
120+
// let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
121+
// assert!(finalized);
122+
123+
// let tx = psbt.extract_tx()?;
124+
// client.broadcast(&tx).await?;
125+
// println!("Tx broadcasted! Txid: {}", tx.compute_txid());
89126

90127
Ok(())
91128
}

examples/example_wallet_esplora_blocking/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use bdk_esplora::{esplora_client, EsploraExt};
44
use bdk_wallet::{
55
bitcoin::{Amount, Network},
66
file_store::Store,
7-
KeychainKind, SignOptions, Wallet,
7+
Keychain, KeychainKind, SignOptions, Wallet,
88
};
99

1010
const DB_MAGIC: &str = "bdk_wallet_esplora_example";
@@ -49,7 +49,7 @@ fn main() -> Result<(), anyhow::Error> {
4949

5050
let request = wallet.start_full_scan().inspect({
5151
let mut stdout = std::io::stdout();
52-
let mut once = BTreeSet::<KeychainKind>::new();
52+
let mut once = BTreeSet::<Keychain>::new();
5353
move |keychain, spk_i, _| {
5454
if once.insert(keychain) {
5555
print!("\nScanning keychain [{:?}] ", keychain);

wallet/src/descriptor/template.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ impl<K: IntoDescriptorKey<Tap>> DescriptorTemplate for P2TR<K> {
228228
/// .create_wallet_no_persist()?;
229229
///
230230
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89");
231-
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt");
231+
/// assert_eq!(wallet.public_descriptor(KeychainKind::External.into()).to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt");
232232
/// # Ok::<_, Box<dyn std::error::Error>>(())
233233
/// ```
234234
#[derive(Debug, Clone)]
@@ -267,7 +267,7 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
267267
/// .create_wallet_no_persist()?;
268268
///
269269
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
270-
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "pkh([c55b303f/44'/1'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#cfhumdqz");
270+
/// assert_eq!(wallet.public_descriptor(KeychainKind::External.into()).to_string(), "pkh([c55b303f/44'/1'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#cfhumdqz");
271271
/// # Ok::<_, Box<dyn std::error::Error>>(())
272272
/// ```
273273
#[derive(Debug, Clone)]
@@ -305,7 +305,7 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
305305
/// .create_wallet_no_persist()?;
306306
///
307307
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB");
308-
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDDYr4kdnZgjjShzYNjZUZXUUtpXaofdkMaipyS8ThEh45qFmhT4hKYways7UXmg6V7het1QiFo9kf4kYUXyDvV4rHEyvSpys9pjCB3pukxi/0/*))#s9vxlc8e");
308+
/// assert_eq!(wallet.public_descriptor(KeychainKind::External.into()).to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDDYr4kdnZgjjShzYNjZUZXUUtpXaofdkMaipyS8ThEh45qFmhT4hKYways7UXmg6V7het1QiFo9kf4kYUXyDvV4rHEyvSpys9pjCB3pukxi/0/*))#s9vxlc8e");
309309
/// # Ok::<_, Box<dyn std::error::Error>>(())
310310
/// ```
311311
#[derive(Debug, Clone)]
@@ -344,7 +344,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
344344
/// .create_wallet_no_persist()?;
345345
///
346346
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
347-
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#3tka9g0q");
347+
/// assert_eq!(wallet.public_descriptor(KeychainKind::External.into()).to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#3tka9g0q");
348348
/// # Ok::<_, Box<dyn std::error::Error>>(())
349349
/// ```
350350
#[derive(Debug, Clone)]
@@ -382,7 +382,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
382382
/// .create_wallet_no_persist()?;
383383
///
384384
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n");
385-
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr");
385+
/// assert_eq!(wallet.public_descriptor(KeychainKind::External.into()).to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr");
386386
/// # Ok::<_, Box<dyn std::error::Error>>(())
387387
/// ```
388388
#[derive(Debug, Clone)]
@@ -421,7 +421,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
421421
/// .create_wallet_no_persist()?;
422422
///
423423
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
424-
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "wpkh([c55b303f/84'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#dhu402yv");
424+
/// assert_eq!(wallet.public_descriptor(KeychainKind::External.into()).to_string(), "wpkh([c55b303f/84'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#dhu402yv");
425425
/// # Ok::<_, Box<dyn std::error::Error>>(())
426426
/// ```
427427
#[derive(Debug, Clone)]
@@ -459,7 +459,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
459459
/// .create_wallet_no_persist()?;
460460
///
461461
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1p5unlj09djx8xsjwe97269kqtxqpwpu2epeskgqjfk4lnf69v4tnqpp35qu");
462-
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "tr([c55b303f/86'/1'/0']tpubDCiHofpEs47kx358bPdJmTZHmCDqQ8qw32upCSxHrSEdeeBs2T5Mq6QMB2ukeMqhNBiyhosBvJErteVhfURPGXPv3qLJPw5MVpHUewsbP2m/0/*)#dkgvr5hm");
462+
/// assert_eq!(wallet.public_descriptor(KeychainKind::External.into()).to_string(), "tr([c55b303f/86'/1'/0']tpubDCiHofpEs47kx358bPdJmTZHmCDqQ8qw32upCSxHrSEdeeBs2T5Mq6QMB2ukeMqhNBiyhosBvJErteVhfURPGXPv3qLJPw5MVpHUewsbP2m/0/*)#dkgvr5hm");
463463
/// # Ok::<_, Box<dyn std::error::Error>>(())
464464
/// ```
465465
#[derive(Debug, Clone)]
@@ -498,7 +498,7 @@ impl<K: DerivableKey<Tap>> DescriptorTemplate for Bip86<K> {
498498
/// .create_wallet_no_persist()?;
499499
///
500500
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1pwjp9f2k5n0xq73ecuu0c5njvgqr3vkh7yaylmpqvsuuaafymh0msvcmh37");
501-
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "tr([c55b303f/86'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#2p65srku");
501+
/// assert_eq!(wallet.public_descriptor(KeychainKind::External.into()).to_string(), "tr([c55b303f/86'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#2p65srku");
502502
/// # Ok::<_, Box<dyn std::error::Error>>(())
503503
/// ```
504504
#[derive(Debug, Clone)]

wallet/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ pub use tx_builder::*;
4646
pub use types::*;
4747
pub use wallet::*;
4848

49+
pub(crate) use bdk_chain::keychain_txout::DEFAULT_LOOKAHEAD;
50+
4951
/// Get the version of [`bdk_wallet`](crate) at runtime.
5052
pub fn version() -> &'static str {
5153
env!("CARGO_PKG_VERSION", "unknown")

wallet/src/types.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,19 @@ impl AsRef<[u8]> for KeychainKind {
4646
}
4747
}
4848

49+
use crate::Keychain;
50+
51+
impl KeychainKind {
52+
/// From keychain.
53+
pub(crate) fn from_keychain(keychain: Keychain) -> Option<Self> {
54+
match keychain {
55+
Keychain::ZERO => Some(KeychainKind::External),
56+
Keychain::ONE => Some(KeychainKind::Internal),
57+
_ => None,
58+
}
59+
}
60+
}
61+
4962
/// An unspent output owned by a [`Wallet`].
5063
///
5164
/// [`Wallet`]: crate::Wallet

0 commit comments

Comments
 (0)