Skip to content

Commit

Permalink
Create taproot-only wallets (ordinals#1158)
Browse files Browse the repository at this point in the history
  • Loading branch information
raphjaph authored Jan 9, 2023
1 parent 251a177 commit 311d5de
Show file tree
Hide file tree
Showing 20 changed files with 402 additions and 112 deletions.
16 changes: 12 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ lazy_static = "1.4.0"
log = "0.4.14"
mime = "0.3.16"
mime_guess = "2.0.4"
ord-bitcoincore-rpc = "0.16.0"
miniscript = "9.0.0"
ord-bitcoincore-rpc = { git = "https://github.com/casey/rust-bitcoincore-rpc", branch = "ord" }
redb = "0.11.0"
regex = "1.6.0"
rust-embed = "6.4.0"
Expand Down
46 changes: 42 additions & 4 deletions src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,15 @@ impl Options {
Ok(self.chain().join_with_data_dir(&base))
}

fn format_bitcoin_core_version(version: usize) -> String {
format!(
"{}.{}.{}",
version / 10000,
version % 10000 / 100,
version % 100
)
}

pub(crate) fn bitcoin_rpc_client(&self) -> Result<Client> {
let cookie_file = self.cookie_file()?;
let rpc_url = self.rpc_url();
Expand Down Expand Up @@ -135,13 +144,42 @@ impl Options {
Ok(client)
}

pub(crate) fn bitcoin_rpc_client_for_wallet_command(&self, command: &str) -> Result<Client> {
pub(crate) fn bitcoin_rpc_client_for_wallet_command(&self, create: bool) -> Result<Client> {
let client = self.bitcoin_rpc_client()?;

let wallet_info = client.get_wallet_info()?;
const MIN_VERSION: usize = 240000;

let bitcoin_version = client.version()?;
if bitcoin_version < MIN_VERSION {
bail!(
"Bitcoin Core {} or newer required, current version is {}",
Self::format_bitcoin_core_version(MIN_VERSION),
Self::format_bitcoin_core_version(bitcoin_version),
);
}

if !create {
let wallet_info = client.get_wallet_info()?;

if !(wallet_info.wallet_name == "ord" || wallet_info.wallet_name.starts_with("ord-")) {
bail!("wallet commands may only be used on mainnet with a wallet named `ord` or whose name starts with `ord-`");
}

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 !(wallet_info.wallet_name == "ord" || wallet_info.wallet_name.starts_with("ord-")) {
bail!("`{command}` may only be used on mainnet with a wallet named `ord` or whose name starts with `ord-`");
if tr != 2 || descriptors.len() != 2 + rawtr {
bail!("this does not appear to be an ord wallet, create one with `ord wallet create`");
}
}

Ok(client)
Expand Down
5 changes: 4 additions & 1 deletion src/subcommand/preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ impl Preview {

let rpc_client = options.bitcoin_rpc_client()?;

super::wallet::create::run(options.clone())?;
super::wallet::create::Create::run(
&super::wallet::create::Create { name: "ord".into() },
options.clone(),
)?;

let address = rpc_client.get_new_address(None, None)?;

Expand Down
10 changes: 5 additions & 5 deletions src/subcommand/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub(crate) enum Wallet {
#[clap(about = "Get wallet balance")]
Balance,
#[clap(about = "Create a new wallet")]
Create,
Create(create::Create),
#[clap(about = "Create an inscription")]
Inscribe(inscribe::Inscribe),
#[clap(about = "List wallet inscriptions")]
Expand All @@ -37,7 +37,7 @@ impl Wallet {
pub(crate) fn run(self, options: Options) -> Result {
match self {
Self::Balance => balance::run(options),
Self::Create => create::run(options),
Self::Create(create) => create.run(options),
Self::Inscribe(inscribe) => inscribe.run(options),
Self::Inscriptions => inscriptions::run(options),
Self::Receive(receive) => receive.run(options),
Expand All @@ -64,7 +64,7 @@ fn get_unspent_output_ranges(
}

fn get_unspent_outputs(options: &Options) -> Result<BTreeMap<OutPoint, Amount>> {
let client = options.bitcoin_rpc_client()?;
let client = options.bitcoin_rpc_client_for_wallet_command(false)?;

let mut utxos = BTreeMap::new();

Expand Down Expand Up @@ -97,13 +97,13 @@ fn get_unspent_outputs(options: &Options) -> Result<BTreeMap<OutPoint, Amount>>
}

fn get_change_addresses(options: &Options, n: usize) -> Result<Vec<Address>> {
let client = options.bitcoin_rpc_client()?;
let client = options.bitcoin_rpc_client_for_wallet_command(false)?;

let mut addresses = Vec::new();
for _ in 0..n {
addresses.push(
client
.call("getrawchangeaddress", &[])
.call("getrawchangeaddress", &["bech32m".into()])
.context("could not get change addresses from wallet")?,
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/subcommand/wallet/balance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pub(crate) fn run(options: Options) -> Result {
println!(
"{}",
options
.bitcoin_rpc_client()?
.bitcoin_rpc_client_for_wallet_command(false)?
.get_balances()?
.mine
.trusted
Expand Down
101 changes: 96 additions & 5 deletions src/subcommand/wallet/create.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,99 @@
use super::*;
use {
super::*,
bitcoin::secp256k1::{rand::RngCore, All, Secp256k1},
bitcoin::{
util::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey, Fingerprint},
Network,
},
bitcoincore_rpc::bitcoincore_rpc_json::{ImportDescriptors, Timestamp},
miniscript::descriptor::{Descriptor, DescriptorSecretKey, DescriptorXKey, Wildcard},
};

#[derive(Debug, Parser)]
pub(crate) struct Create {
#[clap(long, default_value = "ord", help = "Create wallet with <NAME>")]
pub(crate) name: String,
}

impl Create {
pub(crate) fn run(&self, options: Options) -> Result {
if !(self.name == "ord" || self.name.starts_with("ord-")) {
bail!("`ord wallet create` may only be used with a wallet named `ord` or whose name starts with `ord-`");
}

let client = options.bitcoin_rpc_client_for_wallet_command(true)?;

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

let secp = bitcoin::secp256k1::Secp256k1::new();
let mut seed = [0; 32];
bitcoin::secp256k1::rand::thread_rng().fill_bytes(&mut seed);

let master_private_key = ExtendedPrivKey::new_master(options.chain().network(), &seed)?;

let fingerprint = master_private_key.fingerprint(&secp);

let derivation_path = DerivationPath::master()
.child(ChildNumber::Hardened { index: 86 })
.child(ChildNumber::Hardened {
index: u32::from(options.chain().network() != Network::Bitcoin),
})
.child(ChildNumber::Hardened { index: 0 });

let derived_private_key = master_private_key.derive_priv(&secp, &derivation_path)?;

derive_and_import_descriptor(
&client,
&secp,
(fingerprint, derivation_path.clone()),
derived_private_key,
false,
)?;

derive_and_import_descriptor(
&client,
&secp,
(fingerprint, derivation_path),
derived_private_key,
true,
)?;

Ok(())
}
}

fn derive_and_import_descriptor(
client: &Client,
secp: &Secp256k1<All>,
origin: (Fingerprint, DerivationPath),
derived_private_key: ExtendedPrivKey,
change: bool,
) -> Result {
let secret_key = DescriptorSecretKey::XPrv(DescriptorXKey {
origin: Some(origin),
xkey: derived_private_key,
derivation_path: DerivationPath::master().child(ChildNumber::Normal {
index: change.into(),
}),
wildcard: Wildcard::Unhardened,
});

let public_key = secret_key.to_public(secp)?;

let mut key_map = std::collections::HashMap::new();
key_map.insert(public_key.clone(), secret_key);

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

client.import_descriptors(ImportDescriptors {
descriptor: desc.to_string_with_secret(&key_map),
timestamp: Timestamp::Now,
active: Some(true),
range: None,
next_index: None,
internal: Some(!change),
label: None,
})?;

pub(crate) fn run(options: Options) -> Result {
options
.bitcoin_rpc_client()?
.create_wallet("ord", None, None, None, None)?;
Ok(())
}
51 changes: 11 additions & 40 deletions src/subcommand/wallet/inscribe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,11 @@ use {
util::taproot::{LeafVersion, TapLeafHash, TaprootBuilder},
PackedLockTime, SchnorrSighashType, Witness,
},
bitcoincore_rpc::bitcoincore_rpc_json::{ImportDescriptors, Timestamp},
bitcoincore_rpc::Client,
serde_json::json,
std::collections::BTreeSet,
};

const MIN_BITCOIN_VERSION: usize = 240000;

fn format_bitcoin_core_version(version: usize) -> String {
format!(
"{}.{}.{}",
version / 10000,
version % 10000 / 100,
version % 100
)
}

#[derive(Debug, Parser)]
pub(crate) struct Inscribe {
#[clap(long, help = "Inscribe <SATPOINT>")]
Expand All @@ -39,16 +28,7 @@ pub(crate) struct Inscribe {

impl Inscribe {
pub(crate) fn run(self, options: Options) -> Result {
let client = options.bitcoin_rpc_client_for_wallet_command("ord wallet inscribe")?;

let bitcoin_version = client.version()?;
if bitcoin_version < MIN_BITCOIN_VERSION {
bail!(
"Bitcoin Core {} or newer required, current version is {}",
format_bitcoin_core_version(MIN_BITCOIN_VERSION),
format_bitcoin_core_version(bitcoin_version),
);
}
let client = options.bitcoin_rpc_client_for_wallet_command(false)?;

let inscription = Inscription::from_file(options.chain(), &self.file)?;

Expand Down Expand Up @@ -260,24 +240,15 @@ impl Inscribe {

let info = client.get_descriptor_info(&format!("rawtr({})", recovery_private_key.to_wif()))?;

let params = json!([
{
"desc": format!("rawtr({})#{}", recovery_private_key.to_wif(), info.checksum),
"active": false,
"timestamp": "now",
"internal": false,
"label": format!("commit tx recovery key")
}
]);

#[derive(Deserialize)]
struct ImportDescriptorsResult {
success: bool,
}

let response: Vec<ImportDescriptorsResult> = client
.call("importdescriptors", &[params])
.context("could not import commit tx recovery key")?;
let response = client.import_descriptors(ImportDescriptors {
descriptor: format!("rawtr({})#{}", recovery_private_key.to_wif(), info.checksum),
timestamp: Timestamp::Now,
active: Some(false),
range: None,
next_index: None,
internal: Some(false),
label: Some("commit tx recovery key".to_string()),
})?;

for result in response {
if !result.success {
Expand Down
4 changes: 2 additions & 2 deletions src/subcommand/wallet/receive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ pub(crate) struct Receive {
impl Receive {
pub(crate) fn run(self, options: Options) -> Result {
let address = options
.bitcoin_rpc_client_for_wallet_command("ord wallet receive")?
.get_new_address(None, None)?;
.bitcoin_rpc_client_for_wallet_command(false)?
.get_new_address(None, Some(bitcoincore_rpc::json::AddressType::Bech32m))?;

if self.cardinal {
println!("{}", address);
Expand Down
2 changes: 1 addition & 1 deletion src/subcommand/wallet/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub(crate) struct Send {

impl Send {
pub(crate) fn run(self, options: Options) -> Result {
let client = options.bitcoin_rpc_client_for_wallet_command("ord wallet send")?;
let client = options.bitcoin_rpc_client_for_wallet_command(false)?;

if !self.cardinal && !self.address.is_ordinal() {
bail!("refusing to send to cardinal adddress, which may be from wallet without sat control; the `--cardinal` flag bypasses this check");
Expand Down
Loading

0 comments on commit 311d5de

Please sign in to comment.