From 43ddf5bae851a86e33c23bd9c2f690b91a22148e Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Wed, 14 Feb 2024 15:14:43 -0800 Subject: [PATCH 1/8] Add command to export BIP-329 wallet output labels --- src/inscriptions/inscription_id.rs | 2 +- src/subcommand/wallet.rs | 8 +- src/subcommand/wallet/label.rs | 71 ++++++++++++ src/subcommand/wallet/sats.rs | 166 +++++++++++++++-------------- src/wallet.rs | 11 +- 5 files changed, 175 insertions(+), 83 deletions(-) create mode 100644 src/subcommand/wallet/label.rs diff --git a/src/inscriptions/inscription_id.rs b/src/inscriptions/inscription_id.rs index 684a2f9183..91252129c6 100644 --- a/src/inscriptions/inscription_id.rs +++ b/src/inscriptions/inscription_id.rs @@ -1,6 +1,6 @@ use super::*; -#[derive(Debug, PartialEq, Copy, Clone, Hash, Eq)] +#[derive(Debug, PartialEq, Copy, Clone, Hash, Eq, Ord, PartialOrd)] pub struct InscriptionId { pub txid: Txid, pub index: u32, diff --git a/src/subcommand/wallet.rs b/src/subcommand/wallet.rs index e8a78669c5..1a4823d3ec 100644 --- a/src/subcommand/wallet.rs +++ b/src/subcommand/wallet.rs @@ -15,6 +15,7 @@ pub mod dump; pub mod etch; pub mod inscribe; pub mod inscriptions; +pub mod label; pub mod outputs; pub mod receive; pub mod restore; @@ -53,6 +54,8 @@ pub(crate) enum Subcommand { Inscribe(inscribe::Inscribe), #[command(about = "List wallet inscriptions")] Inscriptions, + #[command(about = "Label wallet outputs")] + Label, #[command(about = "Generate receive address")] Receive, #[command(about = "Restore wallet")] @@ -80,18 +83,19 @@ impl WalletCommand { match self.subcommand { Subcommand::Balance => balance::run(wallet), + Subcommand::Cardinals => cardinals::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), + Subcommand::Label => label::run(wallet), + Subcommand::Outputs => outputs::run(wallet), Subcommand::Receive => receive::run(wallet), Subcommand::Restore(restore) => restore.run(wallet), Subcommand::Sats(sats) => sats.run(wallet), Subcommand::Send(send) => send.run(wallet), Subcommand::Transactions(transactions) => transactions.run(wallet), - Subcommand::Outputs => outputs::run(wallet), - Subcommand::Cardinals => cardinals::run(wallet), } } } diff --git a/src/subcommand/wallet/label.rs b/src/subcommand/wallet/label.rs new file mode 100644 index 0000000000..01359f10c6 --- /dev/null +++ b/src/subcommand/wallet/label.rs @@ -0,0 +1,71 @@ +use super::*; + +#[derive(Serialize)] +struct Label { + first_sat: SatLabel, + inscriptions: BTreeMap>, +} + +#[derive(Serialize)] +struct SatLabel { + name: String, + number: u64, + rarity: Rarity, +} + +#[derive(Serialize)] +struct Line { + label: String, + r#ref: String, + r#type: String, +} + +pub(crate) fn run(wallet: Wallet) -> SubcommandResult { + let mut lines: Vec = Vec::new(); + + let sat_ranges = wallet.get_output_sat_ranges()?; + + let mut inscriptions_by_output: BTreeMap>> = + BTreeMap::new(); + + for (satpoint, inscriptions) in wallet.get_inscriptions()? { + inscriptions_by_output + .entry(satpoint.outpoint) + .or_default() + .insert(satpoint.offset, inscriptions); + } + + for (output, ranges) in sat_ranges { + let sat = Sat(ranges[0].0); + let mut inscriptions = BTreeMap::>::new(); + + if let Some(output_inscriptions) = inscriptions_by_output.get(&output) { + for (&offset, offset_inscriptions) in output_inscriptions { + inscriptions + .entry(offset) + .or_default() + .extend(offset_inscriptions); + } + } + + lines.push(Line { + label: serde_json::to_string(&Label { + first_sat: SatLabel { + name: sat.name(), + number: sat.n(), + rarity: sat.rarity(), + }, + inscriptions, + })?, + r#ref: output.to_string(), + r#type: "output".into(), + }); + } + + for line in lines { + serde_json::to_writer(io::stdout(), &line)?; + println!(); + } + + Ok(None) +} diff --git a/src/subcommand/wallet/sats.rs b/src/subcommand/wallet/sats.rs index 4e86533698..91f7ad974c 100644 --- a/src/subcommand/wallet/sats.rs +++ b/src/subcommand/wallet/sats.rs @@ -11,8 +11,8 @@ pub(crate) struct Sats { #[derive(Serialize, Deserialize)] pub struct OutputTsv { - pub sat: String, - pub output: OutPoint, + pub found: BTreeMap, + pub lost: BTreeSet, } #[derive(Serialize, Deserialize)] @@ -30,24 +30,25 @@ impl Sats { "sats requires index created with `--index-sats` flag" ); - let utxos = wallet.get_output_sat_ranges()?; + let haystacks = wallet.get_output_sat_ranges()?; if let Some(path) = &self.tsv { - let mut output = Vec::new(); - for (outpoint, sat) in sats_from_tsv( - utxos, - &fs::read_to_string(path) - .with_context(|| format!("I/O error reading `{}`", path.display()))?, - )? { - output.push(OutputTsv { - sat: sat.into(), - output: outpoint, - }); - } - Ok(Some(Box::new(output))) + let tsv = fs::read_to_string(path) + .with_context(|| format!("I/O error reading `{}`", path.display()))?; + + let needles = Self::needles(&tsv)?; + + let found = Self::find(&needles, &haystacks); + + let lost = needles + .into_iter() + .filter_map(|(_sat, value)| (!found.contains_key(value)).then(|| value.into())) + .collect(); + + Ok(Some(Box::new(OutputTsv { found, lost }))) } else { let mut output = Vec::new(); - for (outpoint, sat, offset, rarity) in rare_sats(utxos) { + for (outpoint, sat, offset, rarity) in Self::rare_sats(haystacks) { output.push(OutputRare { sat, output: outpoint, @@ -58,80 +59,89 @@ impl Sats { Ok(Some(Box::new(output))) } } -} -fn rare_sats(utxos: Vec<(OutPoint, Vec<(u64, u64)>)>) -> Vec<(OutPoint, Sat, u64, Rarity)> { - utxos - .into_iter() - .flat_map(|(outpoint, sat_ranges)| { + fn find<'a>( + needles: &[(Sat, &'a str)], + ranges: &[(OutPoint, Vec<(u64, u64)>)], + ) -> BTreeMap { + let mut haystacks = Vec::new(); + + for (outpoint, ranges) in ranges { let mut offset = 0; - sat_ranges.into_iter().filter_map(move |(start, end)| { - let sat = Sat(start); - let rarity = sat.rarity(); - let start_offset = offset; + for (start, end) in ranges { + haystacks.push((start, end, offset, outpoint)); offset += end - start; - if rarity > Rarity::Common { - Some((outpoint, sat, start_offset, rarity)) - } else { - None - } - }) - }) - .collect() -} - -fn sats_from_tsv( - utxos: Vec<(OutPoint, Vec<(u64, u64)>)>, - tsv: &str, -) -> Result> { - let mut needles = Vec::new(); - for (i, line) in tsv.lines().enumerate() { - if line.is_empty() || line.starts_with('#') { - continue; + } } - if let Some(value) = line.split('\t').next() { - let sat = Sat::from_str(value).map_err(|err| { - anyhow!( - "failed to parse sat from string \"{value}\" on line {}: {err}", - i + 1, - ) - })?; + let mut i = 0; + let mut j = 0; + let mut results = BTreeMap::new(); + while i < needles.len() && j < haystacks.len() { + let (needle, value) = needles[i]; + let (&start, &end, offset, outpoint) = haystacks[j]; + + if needle >= start && needle < end { + results.insert( + value.into(), + SatPoint { + outpoint: *outpoint, + offset: offset + needle.0 - start, + }, + ); + } - needles.push((sat, value)); + if needle >= end { + j += 1; + } else { + i += 1; + } } + + results } - needles.sort(); - let mut haystacks = utxos - .into_iter() - .flat_map(|(outpoint, ranges)| { - ranges - .into_iter() - .map(move |(start, end)| (start, end, outpoint)) - }) - .collect::>(); - haystacks.sort(); - - let mut i = 0; - let mut j = 0; - let mut results = Vec::new(); - while i < needles.len() && j < haystacks.len() { - let (needle, value) = needles[i]; - let (start, end, outpoint) = haystacks[j]; - - if needle >= start && needle < end { - results.push((outpoint, value)); - } + fn needles(tsv: &str) -> Result> { + let mut needles = tsv + .lines() + .enumerate() + .filter(|(_i, line)| !line.starts_with('#')) + .filter_map(|(i, line)| { + line.split('\t').next().map(|value| { + Sat::from_str(value).map(|sat| (sat, value)).map_err(|err| { + anyhow!( + "failed to parse sat from string \"{value}\" on line {}: {err}", + i + 1, + ) + }) + }) + }) + .collect::>>()?; - if needle >= end { - j += 1; - } else { - i += 1; - } + needles.sort(); + + Ok(needles) } - Ok(results) + fn rare_sats(haystacks: Vec<(OutPoint, Vec<(u64, u64)>)>) -> Vec<(OutPoint, Sat, u64, Rarity)> { + haystacks + .into_iter() + .flat_map(|(outpoint, sat_ranges)| { + let mut offset = 0; + sat_ranges.into_iter().filter_map(move |(start, end)| { + let sat = Sat(start); + let rarity = sat.rarity(); + let start_offset = offset; + offset += end - start; + if rarity > Rarity::Common { + Some((outpoint, sat, start_offset, rarity)) + } else { + None + } + }) + }) + .collect() + } } #[cfg(test)] diff --git a/src/wallet.rs b/src/wallet.rs index 03de5793a0..f51348d914 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -32,7 +32,11 @@ impl Wallet { client.load_wallet(&self.name)?; } - self.check_descriptors(client.list_descriptors(None)?.descriptors)?; + let info = client.get_wallet_info()?; + + if info.private_keys_enabled { + self.check_descriptors(client.list_descriptors(None)?.descriptors)?; + } Ok(client) } @@ -82,7 +86,10 @@ impl Wallet { bail!("wallet failed get output: {}", response.text()?); } - let output_json: OutputJson = serde_json::from_str(&response.text()?)?; + let text = response.text()?; + + let output_json: OutputJson = serde_json::from_str(&text) + .with_context(|| anyhow!("Failed to deserialize response from ord: `{text}`"))?; if !output_json.indexed { bail!("output in wallet but not in ord server: {output}"); From 64e75be5314d74cdb27c9c4d7f9f5f80d6196bac Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 15 Apr 2024 18:03:48 -0700 Subject: [PATCH 2/8] Fix tests --- src/subcommand/wallet/sats.rs | 268 +++++++++++++++++----------------- tests/wallet.rs | 1 + tests/wallet/label.rs | 0 tests/wallet/sats.rs | 8 +- 4 files changed, 140 insertions(+), 137 deletions(-) create mode 100644 tests/wallet/label.rs diff --git a/src/subcommand/wallet/sats.rs b/src/subcommand/wallet/sats.rs index 6ee75b196e..585a40f8ef 100644 --- a/src/subcommand/wallet/sats.rs +++ b/src/subcommand/wallet/sats.rs @@ -151,7 +151,7 @@ mod tests { #[test] fn identify_no_rare_sats() { assert_eq!( - rare_sats(vec![( + Sats::rare_sats(vec![( outpoint(1), vec![(51 * COIN_VALUE, 100 * COIN_VALUE), (1234, 5678)], )]), @@ -162,7 +162,7 @@ mod tests { #[test] fn identify_one_rare_sat() { assert_eq!( - rare_sats(vec![( + Sats::rare_sats(vec![( outpoint(1), vec![(10, 80), (50 * COIN_VALUE, 100 * COIN_VALUE)], )]), @@ -173,7 +173,7 @@ mod tests { #[test] fn identify_two_rare_sats() { assert_eq!( - rare_sats(vec![( + Sats::rare_sats(vec![( outpoint(1), vec![(0, 100), (1050000000000000, 1150000000000000)], )]), @@ -187,7 +187,7 @@ mod tests { #[test] fn identify_rare_sats_in_different_outpoints() { assert_eq!( - rare_sats(vec![ + Sats::rare_sats(vec![ (outpoint(1), vec![(50 * COIN_VALUE, 55 * COIN_VALUE)]), (outpoint(2), vec![(100 * COIN_VALUE, 111 * COIN_VALUE)],), ]), @@ -198,134 +198,134 @@ mod tests { ) } - #[test] - fn identify_from_tsv_none() { - assert_eq!( - sats_from_tsv(vec![(outpoint(1), vec![(0, 1)])], "1\n").unwrap(), - Vec::new() - ) - } - - #[test] - fn identify_from_tsv_single() { - assert_eq!( - sats_from_tsv(vec![(outpoint(1), vec![(0, 1)])], "0\n").unwrap(), - vec![(outpoint(1), "0"),] - ) - } - - #[test] - fn identify_from_tsv_two_in_one_range() { - assert_eq!( - sats_from_tsv(vec![(outpoint(1), vec![(0, 2)])], "0\n1\n").unwrap(), - vec![(outpoint(1), "0"), (outpoint(1), "1"),] - ) - } - - #[test] - fn identify_from_tsv_out_of_order_tsv() { - assert_eq!( - sats_from_tsv(vec![(outpoint(1), vec![(0, 2)])], "1\n0\n").unwrap(), - vec![(outpoint(1), "0"), (outpoint(1), "1"),] - ) - } - - #[test] - fn identify_from_tsv_out_of_order_ranges() { - assert_eq!( - sats_from_tsv(vec![(outpoint(1), vec![(1, 2), (0, 1)])], "1\n0\n").unwrap(), - vec![(outpoint(1), "0"), (outpoint(1), "1"),] - ) - } - - #[test] - fn identify_from_tsv_two_in_two_ranges() { - assert_eq!( - sats_from_tsv(vec![(outpoint(1), vec![(0, 1), (1, 2)])], "0\n1\n").unwrap(), - vec![(outpoint(1), "0"), (outpoint(1), "1"),] - ) - } - - #[test] - fn identify_from_tsv_two_in_two_outputs() { - assert_eq!( - sats_from_tsv( - vec![(outpoint(1), vec![(0, 1)]), (outpoint(2), vec![(1, 2)])], - "0\n1\n" - ) - .unwrap(), - vec![(outpoint(1), "0"), (outpoint(2), "1"),] - ) - } - - #[test] - fn identify_from_tsv_ignores_extra_columns() { - assert_eq!( - sats_from_tsv(vec![(outpoint(1), vec![(0, 1)])], "0\t===\n").unwrap(), - vec![(outpoint(1), "0"),] - ) - } - - #[test] - fn identify_from_tsv_ignores_empty_lines() { - assert_eq!( - sats_from_tsv(vec![(outpoint(1), vec![(0, 1)])], "0\n\n\n").unwrap(), - vec![(outpoint(1), "0"),] - ) - } - - #[test] - fn identify_from_tsv_ignores_comments() { - assert_eq!( - sats_from_tsv(vec![(outpoint(1), vec![(0, 1)])], "0\n#===\n").unwrap(), - vec![(outpoint(1), "0"),] - ) - } - - #[test] - fn parse_error_reports_line_and_value() { - assert_eq!( - sats_from_tsv(vec![(outpoint(1), vec![(0, 1)])], "0\n===\n") - .unwrap_err() - .to_string(), - "failed to parse sat from string \"===\" on line 2: failed to parse sat `===`: invalid integer: invalid digit found in string", - ) - } - - #[test] - fn identify_from_tsv_is_fast() { - let mut start = 0; - let mut utxos = Vec::new(); - let mut results = Vec::new(); - for i in 0..16 { - let mut ranges = Vec::new(); - let outpoint = outpoint(i); - for _ in 0..100 { - let end = start + 50 * COIN_VALUE; - ranges.push((start, end)); - for j in 0..50 { - results.push((outpoint, start + j * COIN_VALUE)); - } - start = end; - } - utxos.push((outpoint, ranges)); - } - - let mut tsv = String::new(); - for i in 0..start / COIN_VALUE { - writeln!(tsv, "{}", i * COIN_VALUE).expect("writing to string should succeed"); - } - - let start = Instant::now(); - assert_eq!( - sats_from_tsv(utxos, &tsv) - .unwrap() - .into_iter() - .map(|(outpoint, s)| (outpoint, s.parse().unwrap())) - .collect::>(), - results - ); - - assert!(Instant::now() - start < Duration::from_secs(10)); - } + // #[test] + // fn identify_from_tsv_none() { + // assert_eq!( + // sats_from_tsv(vec![(outpoint(1), vec![(0, 1)])], "1\n").unwrap(), + // Vec::new() + // ) + // } + + // #[test] + // fn identify_from_tsv_single() { + // assert_eq!( + // sats_from_tsv(vec![(outpoint(1), vec![(0, 1)])], "0\n").unwrap(), + // vec![(outpoint(1), "0"),] + // ) + // } + + // #[test] + // fn identify_from_tsv_two_in_one_range() { + // assert_eq!( + // sats_from_tsv(vec![(outpoint(1), vec![(0, 2)])], "0\n1\n").unwrap(), + // vec![(outpoint(1), "0"), (outpoint(1), "1"),] + // ) + // } + + // #[test] + // fn identify_from_tsv_out_of_order_tsv() { + // assert_eq!( + // sats_from_tsv(vec![(outpoint(1), vec![(0, 2)])], "1\n0\n").unwrap(), + // vec![(outpoint(1), "0"), (outpoint(1), "1"),] + // ) + // } + + // #[test] + // fn identify_from_tsv_out_of_order_ranges() { + // assert_eq!( + // sats_from_tsv(vec![(outpoint(1), vec![(1, 2), (0, 1)])], "1\n0\n").unwrap(), + // vec![(outpoint(1), "0"), (outpoint(1), "1"),] + // ) + // } + + // #[test] + // fn identify_from_tsv_two_in_two_ranges() { + // assert_eq!( + // sats_from_tsv(vec![(outpoint(1), vec![(0, 1), (1, 2)])], "0\n1\n").unwrap(), + // vec![(outpoint(1), "0"), (outpoint(1), "1"),] + // ) + // } + + // #[test] + // fn identify_from_tsv_two_in_two_outputs() { + // assert_eq!( + // sats_from_tsv( + // vec![(outpoint(1), vec![(0, 1)]), (outpoint(2), vec![(1, 2)])], + // "0\n1\n" + // ) + // .unwrap(), + // vec![(outpoint(1), "0"), (outpoint(2), "1"),] + // ) + // } + + // #[test] + // fn identify_from_tsv_ignores_extra_columns() { + // assert_eq!( + // sats_from_tsv(vec![(outpoint(1), vec![(0, 1)])], "0\t===\n").unwrap(), + // vec![(outpoint(1), "0"),] + // ) + // } + + // #[test] + // fn identify_from_tsv_ignores_empty_lines() { + // assert_eq!( + // sats_from_tsv(vec![(outpoint(1), vec![(0, 1)])], "0\n\n\n").unwrap(), + // vec![(outpoint(1), "0"),] + // ) + // } + + // #[test] + // fn identify_from_tsv_ignores_comments() { + // assert_eq!( + // sats_from_tsv(vec![(outpoint(1), vec![(0, 1)])], "0\n#===\n").unwrap(), + // vec![(outpoint(1), "0"),] + // ) + // } + + // #[test] + // fn parse_error_reports_line_and_value() { + // assert_eq!( + // sats_from_tsv(vec![(outpoint(1), vec![(0, 1)])], "0\n===\n") + // .unwrap_err() + // .to_string(), + // "failed to parse sat from string \"===\" on line 2: failed to parse sat `===`: invalid integer: invalid digit found in string", + // ) + // } + + // #[test] + // fn identify_from_tsv_is_fast() { + // let mut start = 0; + // let mut utxos = Vec::new(); + // let mut results = Vec::new(); + // for i in 0..16 { + // let mut ranges = Vec::new(); + // let outpoint = outpoint(i); + // for _ in 0..100 { + // let end = start + 50 * COIN_VALUE; + // ranges.push((start, end)); + // for j in 0..50 { + // results.push((outpoint, start + j * COIN_VALUE)); + // } + // start = end; + // } + // utxos.push((outpoint, ranges)); + // } + + // let mut tsv = String::new(); + // for i in 0..start / COIN_VALUE { + // writeln!(tsv, "{}", i * COIN_VALUE).expect("writing to string should succeed"); + // } + + // let start = Instant::now(); + // assert_eq!( + // sats_from_tsv(utxos, &tsv) + // .unwrap() + // .into_iter() + // .map(|(outpoint, s)| (outpoint, s.parse().unwrap())) + // .collect::>(), + // results + // ); + + // assert!(Instant::now() - start < Duration::from_secs(10)); + // } } diff --git a/tests/wallet.rs b/tests/wallet.rs index 44dd41bb7c..d82edf11a8 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -8,6 +8,7 @@ mod create; mod dump; mod inscribe; mod inscriptions; +mod label; mod mint; mod outputs; mod receive; diff --git a/tests/wallet/label.rs b/tests/wallet/label.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/wallet/sats.rs b/tests/wallet/sats.rs index f25826fe65..44e8650bb5 100644 --- a/tests/wallet/sats.rs +++ b/tests/wallet/sats.rs @@ -52,10 +52,12 @@ fn sats_from_tsv_success() { .write("foo.tsv", "nvtcsezkbtg") .core(&core) .ord(&ord) - .run_and_deserialize_output::>(); + .run_and_deserialize_output::(); - assert_eq!(output[0].sat, "nvtcsezkbtg"); - assert_eq!(output[0].output.to_string(), format!("{second_coinbase}:0")); + assert_eq!( + output.found["nvtcsezkbtg"].to_string(), + format!("{second_coinbase}:0:1") + ); } #[test] From 2bb5cc0657fadb1911e7f82be47c94c5c8ef641f Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 15 Apr 2024 18:43:09 -0700 Subject: [PATCH 3/8] Fix tests --- src/subcommand/wallet/sats.rs | 240 +++++++++++++++------------------- 1 file changed, 109 insertions(+), 131 deletions(-) diff --git a/src/subcommand/wallet/sats.rs b/src/subcommand/wallet/sats.rs index 585a40f8ef..1951f1d918 100644 --- a/src/subcommand/wallet/sats.rs +++ b/src/subcommand/wallet/sats.rs @@ -74,6 +74,8 @@ impl Sats { } } + haystacks.sort_by_key(|(start, _, _, _)| *start); + let mut i = 0; let mut j = 0; let mut results = BTreeMap::new(); @@ -105,7 +107,7 @@ impl Sats { let mut needles = tsv .lines() .enumerate() - .filter(|(_i, line)| !line.starts_with('#')) + .filter(|(_i, line)| !line.starts_with('#') && !line.is_empty()) .filter_map(|(i, line)| { line.split('\t').next().map(|value| { Sat::from_str(value).map(|sat| (sat, value)).map_err(|err| { @@ -198,134 +200,110 @@ mod tests { ) } - // #[test] - // fn identify_from_tsv_none() { - // assert_eq!( - // sats_from_tsv(vec![(outpoint(1), vec![(0, 1)])], "1\n").unwrap(), - // Vec::new() - // ) - // } - - // #[test] - // fn identify_from_tsv_single() { - // assert_eq!( - // sats_from_tsv(vec![(outpoint(1), vec![(0, 1)])], "0\n").unwrap(), - // vec![(outpoint(1), "0"),] - // ) - // } - - // #[test] - // fn identify_from_tsv_two_in_one_range() { - // assert_eq!( - // sats_from_tsv(vec![(outpoint(1), vec![(0, 2)])], "0\n1\n").unwrap(), - // vec![(outpoint(1), "0"), (outpoint(1), "1"),] - // ) - // } - - // #[test] - // fn identify_from_tsv_out_of_order_tsv() { - // assert_eq!( - // sats_from_tsv(vec![(outpoint(1), vec![(0, 2)])], "1\n0\n").unwrap(), - // vec![(outpoint(1), "0"), (outpoint(1), "1"),] - // ) - // } - - // #[test] - // fn identify_from_tsv_out_of_order_ranges() { - // assert_eq!( - // sats_from_tsv(vec![(outpoint(1), vec![(1, 2), (0, 1)])], "1\n0\n").unwrap(), - // vec![(outpoint(1), "0"), (outpoint(1), "1"),] - // ) - // } - - // #[test] - // fn identify_from_tsv_two_in_two_ranges() { - // assert_eq!( - // sats_from_tsv(vec![(outpoint(1), vec![(0, 1), (1, 2)])], "0\n1\n").unwrap(), - // vec![(outpoint(1), "0"), (outpoint(1), "1"),] - // ) - // } - - // #[test] - // fn identify_from_tsv_two_in_two_outputs() { - // assert_eq!( - // sats_from_tsv( - // vec![(outpoint(1), vec![(0, 1)]), (outpoint(2), vec![(1, 2)])], - // "0\n1\n" - // ) - // .unwrap(), - // vec![(outpoint(1), "0"), (outpoint(2), "1"),] - // ) - // } - - // #[test] - // fn identify_from_tsv_ignores_extra_columns() { - // assert_eq!( - // sats_from_tsv(vec![(outpoint(1), vec![(0, 1)])], "0\t===\n").unwrap(), - // vec![(outpoint(1), "0"),] - // ) - // } - - // #[test] - // fn identify_from_tsv_ignores_empty_lines() { - // assert_eq!( - // sats_from_tsv(vec![(outpoint(1), vec![(0, 1)])], "0\n\n\n").unwrap(), - // vec![(outpoint(1), "0"),] - // ) - // } - - // #[test] - // fn identify_from_tsv_ignores_comments() { - // assert_eq!( - // sats_from_tsv(vec![(outpoint(1), vec![(0, 1)])], "0\n#===\n").unwrap(), - // vec![(outpoint(1), "0"),] - // ) - // } - - // #[test] - // fn parse_error_reports_line_and_value() { - // assert_eq!( - // sats_from_tsv(vec![(outpoint(1), vec![(0, 1)])], "0\n===\n") - // .unwrap_err() - // .to_string(), - // "failed to parse sat from string \"===\" on line 2: failed to parse sat `===`: invalid integer: invalid digit found in string", - // ) - // } - - // #[test] - // fn identify_from_tsv_is_fast() { - // let mut start = 0; - // let mut utxos = Vec::new(); - // let mut results = Vec::new(); - // for i in 0..16 { - // let mut ranges = Vec::new(); - // let outpoint = outpoint(i); - // for _ in 0..100 { - // let end = start + 50 * COIN_VALUE; - // ranges.push((start, end)); - // for j in 0..50 { - // results.push((outpoint, start + j * COIN_VALUE)); - // } - // start = end; - // } - // utxos.push((outpoint, ranges)); - // } - - // let mut tsv = String::new(); - // for i in 0..start / COIN_VALUE { - // writeln!(tsv, "{}", i * COIN_VALUE).expect("writing to string should succeed"); - // } - - // let start = Instant::now(); - // assert_eq!( - // sats_from_tsv(utxos, &tsv) - // .unwrap() - // .into_iter() - // .map(|(outpoint, s)| (outpoint, s.parse().unwrap())) - // .collect::>(), - // results - // ); - - // assert!(Instant::now() - start < Duration::from_secs(10)); - // } + #[track_caller] + fn case(tsv: &str, haystacks: &[(OutPoint, Vec<(u64, u64)>)], expected: &[(&str, SatPoint)]) { + assert_eq!( + Sats::find(&Sats::needles(&tsv).unwrap(), &haystacks), + expected + .iter() + .map(|(sat, satpoint)| (sat.to_string(), *satpoint)) + .collect() + ); + } + + #[test] + fn tsv() { + case("1\n", &[(outpoint(1), vec![(0, 1)])], &[]); + } + + #[test] + fn identify_from_tsv_single() { + case( + "0\n", + &[(outpoint(1), vec![(0, 1)])], + &[("0".into(), satpoint(1, 0))], + ); + } + + #[test] + fn identify_from_tsv_two_in_one_range() { + case( + "0\n1\n", + &[(outpoint(1), vec![(0, 2)])], + &[("0".into(), satpoint(1, 0)), ("1".into(), satpoint(1, 1))], + ); + } + + #[test] + fn identify_from_tsv_out_of_order_tsv() { + case( + "1\n0\n", + &[(outpoint(1), vec![(0, 2)])], + &[("0", satpoint(1, 0)), ("1", satpoint(1, 1))], + ); + } + + #[test] + fn identify_from_tsv_out_of_order_ranges() { + case( + "1\n0\n", + &[(outpoint(1), vec![(1, 2), (0, 1)])], + &[("0", satpoint(1, 1)), ("1", satpoint(1, 0))], + ); + } + + #[test] + fn identify_from_tsv_two_in_two_ranges() { + case( + "0\n1\n", + &[(outpoint(1), vec![(0, 1), (1, 2)])], + &[("0", satpoint(1, 0)), ("1", satpoint(1, 1))], + ) + } + + #[test] + fn identify_from_tsv_two_in_two_outputs() { + case( + "0\n1\n", + &[(outpoint(1), vec![(0, 1)]), (outpoint(2), vec![(1, 2)])], + &[("0", satpoint(1, 0)), ("1", satpoint(2, 0))], + ); + } + + #[test] + fn identify_from_tsv_ignores_extra_columns() { + case( + "0\t===\n", + &[(outpoint(1), vec![(0, 1)])], + &[("0", satpoint(1, 0))], + ); + } + + #[test] + fn identify_from_tsv_ignores_empty_lines() { + case( + "0\n\n\n", + &[(outpoint(1), vec![(0, 1)])], + &[("0", satpoint(1, 0))], + ); + } + + #[test] + fn identify_from_tsv_ignores_comments() { + case( + "0\n#===\n", + &[(outpoint(1), vec![(0, 1)])], + &[("0", satpoint(1, 0))], + ); + } + + #[test] + fn parse_error_reports_line_and_value() { + assert_eq!( + Sats::needles("0\n===\n") + .unwrap_err() + .to_string(), + "failed to parse sat from string \"===\" on line 2: failed to parse sat `===`: invalid integer: invalid digit found in string", + ); + } } From dfc248f2b75ded0cf5eb1ff3f4fbfaca4cb3eba3 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 15 Apr 2024 18:43:25 -0700 Subject: [PATCH 4/8] Revise --- src/subcommand/wallet/sats.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/subcommand/wallet/sats.rs b/src/subcommand/wallet/sats.rs index 1951f1d918..64d9f3e847 100644 --- a/src/subcommand/wallet/sats.rs +++ b/src/subcommand/wallet/sats.rs @@ -148,7 +148,7 @@ impl Sats { #[cfg(test)] mod tests { - use {super::*, std::fmt::Write}; + use super::*; #[test] fn identify_no_rare_sats() { From 64a8aa6983eeee0650011148f7a52e213e5323be Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 15 Apr 2024 19:00:32 -0700 Subject: [PATCH 5/8] Add test --- tests/wallet/label.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/wallet/label.rs b/tests/wallet/label.rs index e69de29bb2..65ce3127ac 100644 --- a/tests/wallet/label.rs +++ b/tests/wallet/label.rs @@ -0,0 +1,30 @@ +use super::*; + +#[test] +fn label() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(2); + + let (inscription, _reveal) = inscribe(&core, &ord); + + let output = CommandBuilder::new("wallet label") + .core(&core) + .ord(&ord) + .stdout_regex(".*") + .run_and_extract_stdout(); + + assert!( + output.contains(r#"\"name\":\"nvtcsezkbth\",\"number\":5000000000,\"rarity\":\"uncommon\""#) + ); + + assert!( + output.contains(r#"\"name\":\"nvtccadxgaz\",\"number\":10000000000,\"rarity\":\"uncommon\""#) + ); + + assert!(output.contains(&inscription.to_string())); +} From bf6e8e08a4886e76d94d9ee31c6b6ca7f775513b Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 15 Apr 2024 19:24:52 -0700 Subject: [PATCH 6/8] Only check descriptors if private keys are enabled --- src/wallet/wallet_constructor.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/wallet/wallet_constructor.rs b/src/wallet/wallet_constructor.rs index f485f6f8fb..1db7ee5c6b 100644 --- a/src/wallet/wallet_constructor.rs +++ b/src/wallet/wallet_constructor.rs @@ -54,7 +54,9 @@ impl WalletConstructor { client.load_wallet(&self.name)?; } - Wallet::check_descriptors(&self.name, client.list_descriptors(None)?.descriptors)?; + if client.get_wallet_info()?.private_keys_enabled { + Wallet::check_descriptors(&self.name, client.list_descriptors(None)?.descriptors)?; + } client }; From d9fd32e1301d67d1c88e068dc86efa341947774f Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 15 Apr 2024 19:29:36 -0700 Subject: [PATCH 7/8] Placate clippy --- src/subcommand/wallet/sats.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/subcommand/wallet/sats.rs b/src/subcommand/wallet/sats.rs index 64d9f3e847..7cdbe72c74 100644 --- a/src/subcommand/wallet/sats.rs +++ b/src/subcommand/wallet/sats.rs @@ -42,7 +42,8 @@ impl Sats { let lost = needles .into_iter() - .filter_map(|(_sat, value)| (!found.contains_key(value)).then(|| value.into())) + .filter(|(_sat, value)| !found.contains_key(*value)) + .map(|(_sat, value)| value.into()) .collect(); Ok(Some(Box::new(OutputTsv { found, lost }))) @@ -60,8 +61,8 @@ impl Sats { } } - fn find<'a>( - needles: &[(Sat, &'a str)], + fn find( + needles: &[(Sat, &str)], ranges: &[(OutPoint, Vec<(u64, u64)>)], ) -> BTreeMap { let mut haystacks = Vec::new(); @@ -203,7 +204,7 @@ mod tests { #[track_caller] fn case(tsv: &str, haystacks: &[(OutPoint, Vec<(u64, u64)>)], expected: &[(&str, SatPoint)]) { assert_eq!( - Sats::find(&Sats::needles(&tsv).unwrap(), &haystacks), + Sats::find(&Sats::needles(tsv).unwrap(), haystacks), expected .iter() .map(|(sat, satpoint)| (sat.to_string(), *satpoint)) @@ -221,7 +222,7 @@ mod tests { case( "0\n", &[(outpoint(1), vec![(0, 1)])], - &[("0".into(), satpoint(1, 0))], + &[("0", satpoint(1, 0))], ); } @@ -230,7 +231,7 @@ mod tests { case( "0\n1\n", &[(outpoint(1), vec![(0, 2)])], - &[("0".into(), satpoint(1, 0)), ("1".into(), satpoint(1, 1))], + &[("0", satpoint(1, 0)), ("1", satpoint(1, 1))], ); } From 9fbac1a9d603d94708db1cbff496d35095904e97 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Tue, 16 Apr 2024 11:04:16 -0700 Subject: [PATCH 8/8] Revise --- crates/mockcore/src/server.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/mockcore/src/server.rs b/crates/mockcore/src/server.rs index d034266106..bd62aab5b4 100644 --- a/crates/mockcore/src/server.rs +++ b/crates/mockcore/src/server.rs @@ -296,7 +296,7 @@ impl Api for Server { keypool_size: 0, keypool_size_hd_internal: 0, pay_tx_fee: Amount::from_sat(0), - private_keys_enabled: false, + private_keys_enabled: true, scanning: None, tx_count: 0, unconfirmed_balance: Amount::from_sat(0),