Skip to content

Commit

Permalink
Dump and restore wallet from descriptors (#3048)
Browse files Browse the repository at this point in the history
  • Loading branch information
raphjaph authored Jan 26, 2024
1 parent 17236d9 commit 13c0fa1
Show file tree
Hide file tree
Showing 14 changed files with 339 additions and 34 deletions.
10 changes: 7 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ use {
env,
fmt::{self, Display, Formatter},
fs::{self, File},
io::{self, Cursor},
io::{self, Cursor, Read},
mem,
net::ToSocketAddrs,
ops::{Add, AddAssign, Sub},
Expand Down Expand Up @@ -242,7 +242,11 @@ pub fn main() {
})
.expect("Error setting <CTRL-C> handler");

match Arguments::parse().run() {
let args = Arguments::parse();

let minify = args.options.minify;

match args.run() {
Err(err) => {
eprintln!("error: {err}");
err
Expand All @@ -262,7 +266,7 @@ pub fn main() {
}
Ok(output) => {
if let Some(output) = output {
output.print_json();
output.print_json(minify);
}
gracefully_shutdown_indexer();
}
Expand Down
2 changes: 2 additions & 0 deletions src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use {super::*, bitcoincore_rpc::Auth};
.args(&["chain_argument", "signet", "regtest", "testnet"]),
))]
pub struct Options {
#[arg(long, help = "Minify JSON output.")]
pub(crate) minify: bool,
#[arg(long, help = "Load Bitcoin Core data dir from <BITCOIN_DATA_DIR>.")]
pub(crate) bitcoin_data_dir: Option<PathBuf>,
#[arg(long, help = "Authenticate to Bitcoin Core RPC with <RPC_PASS>.")]
Expand Down
10 changes: 7 additions & 3 deletions src/subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,19 @@ impl Subcommand {
}

pub trait Output: Send {
fn print_json(&self);
fn print_json(&self, minify: bool);
}

impl<T> Output for T
where
T: Serialize + Send,
{
fn print_json(&self) {
serde_json::to_writer_pretty(io::stdout(), self).ok();
fn print_json(&self, minify: bool) {
if minify {
serde_json::to_writer(io::stdout(), self).ok();
} else {
serde_json::to_writer_pretty(io::stdout(), self).ok();
}
println!();
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/subcommand/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ use {
inscribe::{Batch, Batchfile, Mode},
Wallet,
},
bitcoincore_rpc::bitcoincore_rpc_json::ListDescriptorsResult,
reqwest::Url,
};

pub mod balance;
pub mod cardinals;
pub mod create;
pub mod dump;
pub mod etch;
pub mod inscribe;
pub mod inscriptions;
Expand Down Expand Up @@ -43,6 +45,8 @@ pub(crate) enum Subcommand {
Balance,
#[command(about = "Create new wallet")]
Create(create::Create),
#[command(about = "Dump wallet descriptors")]
Dump,
#[command(about = "Create rune")]
Etch(etch::Etch),
#[command(about = "Create inscription")]
Expand Down Expand Up @@ -77,6 +81,7 @@ impl WalletCommand {
match self.subcommand {
Subcommand::Balance => balance::run(wallet),
Subcommand::Create(create) => create.run(wallet),
Subcommand::Dump => dump::run(wallet),
Subcommand::Etch(etch) => etch.run(wallet),
Subcommand::Inscribe(inscribe) => inscribe.run(wallet),
Subcommand::Inscriptions => inscriptions::run(wallet),
Expand Down
14 changes: 14 additions & 0 deletions src/subcommand/wallet/dump.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use super::*;

pub(crate) fn run(wallet: Wallet) -> SubcommandResult {
eprintln!(
"==========================================
= THIS STRING CONTAINS YOUR PRIVATE KEYS =
= DO NOT SHARE WITH ANYONE =
=========================================="
);

Ok(Some(Box::new(
wallet.bitcoin_client()?.list_descriptors(Some(true))?,
)))
}
50 changes: 45 additions & 5 deletions src/subcommand/wallet/restore.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,61 @@
use super::*;

#[derive(Debug, Parser)]
#[clap(group(
ArgGroup::new("source").required(true).args(&["descriptor", "mnemonic"]))
)]
pub(crate) struct Restore {
#[arg(help = "Restore wallet from <MNEMONIC>")]
mnemonic: Mnemonic,
#[arg(long, help = "Restore wallet from <DESCRIPTOR> from stdin.")]
descriptor: bool,
#[arg(long, help = "Restore wallet from <MNEMONIC>.")]
mnemonic: Option<Mnemonic>,
#[arg(
long,
default_value = "",
requires = "mnemonic",
help = "Use <PASSPHRASE> when deriving wallet"
)]
pub(crate) passphrase: String,
pub(crate) passphrase: Option<String>,
}

impl Restore {
pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult {
wallet.initialize(self.mnemonic.to_seed(self.passphrase))?;
ensure!(!wallet.exists()?, "wallet `{}` already exists", wallet.name);

if self.descriptor {
let mut buffer = Vec::new();
std::io::stdin().read_to_end(&mut buffer)?;

let wallet_descriptors: ListDescriptorsResult = serde_json::from_slice(&buffer)?;

wallet.initialize_from_descriptors(wallet_descriptors.descriptors)?;
} else if let Some(mnemonic) = self.mnemonic {
wallet.initialize(mnemonic.to_seed(self.passphrase.unwrap_or_default()))?;
} else {
unreachable!();
}

Ok(None)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn descriptor_and_mnemonic_conflict() {
assert_regex_match!(
Arguments::try_parse_from([
"ord",
"wallet",
"restore",
"--descriptor",
"--mnemonic",
"oil oil oil oil oil oil oil oil oil oil oil oil"
])
.unwrap_err()
.to_string(),
".*--descriptor.*cannot be used with.*--mnemonic.*"
);
}
}
82 changes: 63 additions & 19 deletions src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ use {
bip32::{ChildNumber, DerivationPath, ExtendedPrivKey, Fingerprint},
Network,
},
bitcoincore_rpc::bitcoincore_rpc_json::{ImportDescriptors, Timestamp},
bitcoincore_rpc::bitcoincore_rpc_json::{Descriptor, ImportDescriptors, Timestamp},
fee_rate::FeeRate,
http::StatusCode,
inscribe::ParentInfo,
miniscript::descriptor::{Descriptor, DescriptorSecretKey, DescriptorXKey, Wildcard},
miniscript::descriptor::{DescriptorSecretKey, DescriptorXKey, Wildcard},
reqwest::{header, Url},
transaction_builder::TransactionBuilder,
};
Expand All @@ -32,21 +32,7 @@ impl Wallet {
client.load_wallet(&self.name)?;
}

let descriptors = client.list_descriptors(None)?.descriptors;

let tr = descriptors
.iter()
.filter(|descriptor| descriptor.desc.starts_with("tr("))
.count();

let rawtr = descriptors
.iter()
.filter(|descriptor| descriptor.desc.starts_with("rawtr("))
.count();

if tr != 2 || descriptors.len() != 2 + rawtr {
bail!("wallet \"{}\" contains unexpected output descriptors, and does not appear to be an `ord` wallet, create a new wallet with `ord wallet create`", self.name);
}
self.check_descriptors(client.list_descriptors(None)?.descriptors)?;

Ok(client)
}
Expand Down Expand Up @@ -382,6 +368,64 @@ impl Wallet {
self.options.chain()
}

pub(crate) fn exists(&self) -> Result<bool> {
Ok(
self
.options
.bitcoin_rpc_client(None)?
.list_wallet_dir()?
.iter()
.any(|name| name == &self.name),
)
}

pub(crate) fn check_descriptors(&self, descriptors: Vec<Descriptor>) -> Result<Vec<Descriptor>> {
let tr = descriptors
.iter()
.filter(|descriptor| descriptor.desc.starts_with("tr("))
.count();

let rawtr = descriptors
.iter()
.filter(|descriptor| descriptor.desc.starts_with("rawtr("))
.count();

if tr != 2 || descriptors.len() != 2 + rawtr {
bail!("wallet \"{}\" contains unexpected output descriptors, and does not appear to be an `ord` wallet, create a new wallet with `ord wallet create`", self.name);
}

Ok(descriptors)
}

pub(crate) fn initialize_from_descriptors(&self, descriptors: Vec<Descriptor>) -> Result {
let client = check_version(self.options.bitcoin_rpc_client(Some(self.name.clone()))?)?;

let descriptors = self.check_descriptors(descriptors)?;

client.create_wallet(&self.name, None, Some(true), None, None)?;

for descriptor in descriptors {
client.import_descriptors(ImportDescriptors {
descriptor: descriptor.desc,
timestamp: descriptor.timestamp,
active: Some(true),
range: descriptor.range.map(|(start, end)| {
(
usize::try_from(start).unwrap_or(0),
usize::try_from(end).unwrap_or(0),
)
}),
next_index: descriptor
.next
.map(|next| usize::try_from(next).unwrap_or(0)),
internal: descriptor.internal,
label: None,
})?;
}

Ok(())
}

pub(crate) fn initialize(&self, seed: [u8; 64]) -> Result {
check_version(self.options.bitcoin_rpc_client(None)?)?.create_wallet(
&self.name,
Expand Down Expand Up @@ -441,13 +485,13 @@ impl Wallet {
let mut key_map = std::collections::HashMap::new();
key_map.insert(public_key.clone(), secret_key);

let desc = Descriptor::new_tr(public_key, None)?;
let descriptor = miniscript::descriptor::Descriptor::new_tr(public_key, None)?;

self
.options
.bitcoin_rpc_client(Some(self.name.clone()))?
.import_descriptors(ImportDescriptors {
descriptor: desc.to_string_with_secret(&key_map),
descriptor: descriptor.to_string_with_secret(&key_map),
timestamp: Timestamp::Now,
active: Some(true),
range: None,
Expand Down
8 changes: 7 additions & 1 deletion test-bitcoincore-rpc/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,17 @@ pub trait Api {
) -> Result<bool, jsonrpc_core::Error>;

#[rpc(name = "listdescriptors")]
fn list_descriptors(&self) -> Result<ListDescriptorsResult, jsonrpc_core::Error>;
fn list_descriptors(
&self,
_with_private_keys: Option<bool>,
) -> Result<ListDescriptorsResult, jsonrpc_core::Error>;

#[rpc(name = "loadwallet")]
fn load_wallet(&self, wallet: String) -> Result<LoadWalletResult, jsonrpc_core::Error>;

#[rpc(name = "listwallets")]
fn list_wallets(&self) -> Result<Vec<String>, jsonrpc_core::Error>;

#[rpc(name = "listwalletdir")]
fn list_wallet_dir(&self) -> Result<ListWalletDirResult, jsonrpc_core::Error>;
}
3 changes: 2 additions & 1 deletion test-bitcoincore-rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ use {
GetRawTransactionResultVoutScriptPubKey, GetTransactionResult, GetTransactionResultDetail,
GetTransactionResultDetailCategory, GetTxOutResult, GetWalletInfoResult, ImportDescriptors,
ImportMultiResult, ListDescriptorsResult, ListTransactionResult, ListUnspentResultEntry,
LoadWalletResult, SignRawTransactionInput, SignRawTransactionResult, Timestamp, WalletTxInfo,
ListWalletDirItem, ListWalletDirResult, LoadWalletResult, SignRawTransactionInput,
SignRawTransactionResult, Timestamp, WalletTxInfo,
},
jsonrpc_core::{IoHandler, Value},
jsonrpc_http_server::{CloseHandle, ServerBuilder},
Expand Down
15 changes: 14 additions & 1 deletion test-bitcoincore-rpc/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -732,7 +732,10 @@ impl Api for Server {
Ok(true)
}

fn list_descriptors(&self) -> Result<ListDescriptorsResult, jsonrpc_core::Error> {
fn list_descriptors(
&self,
_with_private_keys: Option<bool>,
) -> Result<ListDescriptorsResult, jsonrpc_core::Error> {
Ok(ListDescriptorsResult {
wallet_name: "ord".into(),
descriptors: self
Expand Down Expand Up @@ -773,4 +776,14 @@ impl Api for Server {
.collect::<Vec<String>>(),
)
}

fn list_wallet_dir(&self) -> Result<ListWalletDirResult, jsonrpc_core::Error> {
Ok(ListWalletDirResult {
wallets: self
.list_wallets()?
.into_iter()
.map(|name| ListWalletDirItem { name })
.collect(),
})
}
}
1 change: 1 addition & 0 deletions tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use {
blockdata::constants::COIN_VALUE,
Network, OutPoint, Txid,
},
bitcoincore_rpc::bitcoincore_rpc_json::ListDescriptorsResult,
chrono::{DateTime, Utc},
executable_path::executable_path,
ord::{
Expand Down
1 change: 1 addition & 0 deletions tests/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use super::*;
mod balance;
mod cardinals;
mod create;
mod dump;
mod inscribe;
mod inscriptions;
mod outputs;
Expand Down
Loading

0 comments on commit 13c0fa1

Please sign in to comment.