From 0ffc62b291905c5ae41b16bb9bbfc4347fc87715 Mon Sep 17 00:00:00 2001 From: raphjaph Date: Tue, 4 Oct 2022 01:06:23 +0200 Subject: [PATCH] Add `ord wallet list` command (#601) --- src/main.rs | 2 +- src/options.rs | 18 +++++++- src/subcommand/wallet.rs | 25 ++++++++++- src/subcommand/wallet/identify.rs | 51 +++++---------------- src/subcommand/wallet/list.rs | 73 +++++++++++++++++++++++++++++++ tests/wallet.rs | 18 +++++++- 6 files changed, 143 insertions(+), 44 deletions(-) create mode 100644 src/subcommand/wallet/list.rs diff --git a/src/main.rs b/src/main.rs index 67c3c458c0..c3521afa60 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_arguments, clippy::type_complexity)] use { self::{ diff --git a/src/options.rs b/src/options.rs index 0fa1ce4c88..7f77d4ff69 100644 --- a/src/options.rs +++ b/src/options.rs @@ -1,4 +1,8 @@ -use {super::*, clap::ValueEnum}; +use { + super::*, + bitcoincore_rpc::{Auth, Client}, + clap::ValueEnum, +}; #[derive(Debug, Parser)] pub(crate) struct Options { @@ -112,6 +116,18 @@ impl Options { Ok(path) } + + pub(crate) fn bitcoin_rpc_client(&self) -> Result { + let cookie_file = self.cookie_file()?; + let rpc_url = self.rpc_url(); + log::info!( + "Connecting to Bitcoin Core RPC server at {rpc_url} using credentials from `{}`", + cookie_file.display() + ); + + Client::new(&rpc_url, Auth::CookieFile(cookie_file)) + .context("Failed to connect to Bitcoin Core RPC at {rpc_url}") + } } #[cfg(test)] diff --git a/src/subcommand/wallet.rs b/src/subcommand/wallet.rs index e575fb304c..2de363f4de 100644 --- a/src/subcommand/wallet.rs +++ b/src/subcommand/wallet.rs @@ -1,16 +1,39 @@ -use super::*; +use {super::*, bitcoincore_rpc::RpcApi}; mod identify; +mod list; + +fn list_unspent(options: Options) -> Result)>> { + let index = Index::open(&options)?; + index.index()?; + + let client = options.bitcoin_rpc_client()?; + + client + .list_unspent(None, None, None, None, None)? + .iter() + .map(|utxo| { + let outpoint = OutPoint::new(utxo.txid, utxo.vout); + match index.list(outpoint)? { + Some(List::Unspent(ordinal_ranges)) => Ok((outpoint, ordinal_ranges)), + Some(List::Spent) => bail!("Output {outpoint} in wallet but is spent according to index"), + None => bail!("Ordinals index has not seen {outpoint}"), + } + }) + .collect() +} #[derive(Debug, Parser)] pub(crate) enum Wallet { Identify, + List, } impl Wallet { pub(crate) fn run(self, options: Options) -> Result<()> { match self { Self::Identify => identify::run(options), + Self::List => list::run(options), } } } diff --git a/src/subcommand/wallet/identify.rs b/src/subcommand/wallet/identify.rs index a6ece41616..f6c32c04dd 100644 --- a/src/subcommand/wallet/identify.rs +++ b/src/subcommand/wallet/identify.rs @@ -1,45 +1,16 @@ -use { - super::*, - bitcoincore_rpc::{Auth, Client, RpcApi}, -}; +use super::*; pub(crate) fn run(options: Options) -> Result { - let index = Index::open(&options)?; - index.index()?; + let utxos = list_unspent(options)?; - let cookie_file = options.cookie_file()?; - let rpc_url = options.rpc_url(); - log::info!( - "Connecting to Bitcoin Core RPC server at {rpc_url} using credentials from `{}`", - cookie_file.display() - ); - let client = Client::new(&rpc_url, Auth::CookieFile(cookie_file)) - .context("Failed to connect to Bitcoin Core RPC at {rpc_url}")?; - - let unspent = client.list_unspent(None, None, None, None, None)?; - - let mut utxos = Vec::new(); - for utxo in unspent { - let output = OutPoint::new(utxo.txid, utxo.vout); - match index.list(output)? { - Some(List::Unspent(ordinal_ranges)) => { - utxos.push((output, ordinal_ranges)); - } - Some(List::Spent) => { - bail!("Output {output} in wallet but is spent according to index") - } - None => bail!("Ordinals index has not seen {output}"), - } - } - - for (ordinal, output, offset, rarity) in identify(utxos) { - println!("{ordinal}\t{output}\t{offset}\t{rarity}"); + for (output, ordinal, offset, rarity) in identify(utxos) { + println!("{output}\t{ordinal}\t{offset}\t{rarity}"); } Ok(()) } -fn identify(utxos: Vec<(OutPoint, Vec<(u64, u64)>)>) -> Vec<(Ordinal, OutPoint, u64, Rarity)> { +fn identify(utxos: Vec<(OutPoint, Vec<(u64, u64)>)>) -> Vec<(OutPoint, Ordinal, u64, Rarity)> { utxos .into_iter() .flat_map(|(outpoint, ordinal_ranges)| { @@ -50,7 +21,7 @@ fn identify(utxos: Vec<(OutPoint, Vec<(u64, u64)>)>) -> Vec<(Ordinal, OutPoint, let start_offset = offset; offset += end - start; if rarity > Rarity::Common { - Some((ordinal, outpoint, start_offset, rarity)) + Some((outpoint, ordinal, start_offset, rarity)) } else { None } @@ -81,8 +52,8 @@ mod tests { assert_eq!( identify(utxos), vec![( - Ordinal(50 * COIN_VALUE), OutPoint::null(), + Ordinal(50 * COIN_VALUE), 70, Rarity::Uncommon )] @@ -98,10 +69,10 @@ mod tests { assert_eq!( identify(utxos), vec![ - (Ordinal(0), OutPoint::null(), 0, Rarity::Mythic), + (OutPoint::null(), Ordinal(0), 0, Rarity::Mythic), ( - Ordinal(1050000000000000), OutPoint::null(), + Ordinal(1050000000000000), 100, Rarity::Epic ) @@ -123,15 +94,15 @@ mod tests { identify(utxos), vec![ ( - Ordinal(50 * COIN_VALUE), OutPoint::null(), + Ordinal(50 * COIN_VALUE), 0, Rarity::Uncommon ), ( - Ordinal(100 * COIN_VALUE), OutPoint::from_str("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:5") .unwrap(), + Ordinal(100 * COIN_VALUE), 0, Rarity::Uncommon ) diff --git a/src/subcommand/wallet/list.rs b/src/subcommand/wallet/list.rs new file mode 100644 index 0000000000..6b337c6948 --- /dev/null +++ b/src/subcommand/wallet/list.rs @@ -0,0 +1,73 @@ +use super::*; + +pub(crate) fn run(options: Options) -> Result { + let utxos = list_unspent(options)?; + + for (output, start, size, rarity, name) in list(utxos) { + println!("{output}\t{start}\t{size}\t{rarity}\t{name}"); + } + + Ok(()) +} + +fn list(utxos: Vec<(OutPoint, Vec<(u64, u64)>)>) -> Vec<(OutPoint, u64, u64, Rarity, String)> { + utxos + .into_iter() + .flat_map(|(output, ordinal_ranges)| { + ordinal_ranges.into_iter().map(move |(start, end)| { + let ordinal = Ordinal(start); + let rarity = ordinal.rarity(); + let name = ordinal.name(); + let size = end - start; + (output, start, size, rarity, name) + }) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn list_ranges() { + let utxos = vec![ + ( + OutPoint::from_str("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:5") + .unwrap(), + vec![(50 * COIN_VALUE, 55 * COIN_VALUE)], + ), + ( + OutPoint::null(), + vec![(10, 100), (1050000000000000, 1150000000000000)], + ), + ]; + assert_eq!( + list(utxos), + vec![ + ( + OutPoint::from_str("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:5") + .unwrap(), + 50 * COIN_VALUE, + 5 * COIN_VALUE, + Rarity::Uncommon, + "nvtcsezkbth".to_string() + ), + ( + OutPoint::null(), + 10, + 90, + Rarity::Common, + "nvtdijuwxlf".to_string() + ), + ( + OutPoint::null(), + 1050000000000000, + 100000000000000, + Rarity::Epic, + "gkjbdrhkfqf".to_string() + ) + ] + ) + } +} diff --git a/tests/wallet.rs b/tests/wallet.rs index a519c01d56..3de68ecf32 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -12,8 +12,24 @@ fn identify() { .rpc_server(&rpc_server) .expected_stdout(format!( "{}\t{}\t0\tuncommon\n", + OutPoint::new(second_coinbase, 0), + 50 * COIN_VALUE, + )) + .run(); +} + +#[test] +fn list() { + let rpc_server = test_bitcoincore_rpc::spawn(); + let second_coinbase = rpc_server.mine_blocks(1)[0].txdata[0].txid(); + + CommandBuilder::new("wallet list") + .rpc_server(&rpc_server) + .expected_stdout(format!( + "{}\t{}\t{}\tuncommon\tnvtcsezkbth\n", + OutPoint::new(second_coinbase, 0), + 50 * COIN_VALUE, 50 * COIN_VALUE, - OutPoint::new(second_coinbase, 0) )) .run(); }