diff --git a/src/main.rs b/src/main.rs index da4ca54681..60b4c5eedc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ use { options::Options, ordinal::Ordinal, purse::Purse, + rarity::Rarity, sat_point::SatPoint, subcommand::Subcommand, }, @@ -85,6 +86,7 @@ mod nft; mod options; mod ordinal; mod purse; +mod rarity; mod sat_point; mod subcommand; diff --git a/src/ordinal.rs b/src/ordinal.rs index 83d6ca2583..dee240036e 100644 --- a/src/ordinal.rs +++ b/src/ordinal.rs @@ -44,27 +44,8 @@ impl Ordinal { format!("{}.{}", self.height(), self.third()) } - pub(crate) fn rarity(self) -> &'static str { - let Degree { - hour, - minute, - second, - third, - } = self.degree(); - - if hour == 0 && minute == 0 && second == 0 && third == 0 { - "mythic" - } else if minute == 0 && second == 0 && third == 0 { - "legendary" - } else if minute == 0 && third == 0 { - "epic" - } else if second == 0 && third == 0 { - "rare" - } else if third == 0 { - "uncommon" - } else { - "common" - } + pub(crate) fn rarity(self) -> Rarity { + self.into() } pub(crate) fn name(self) -> String { @@ -498,28 +479,6 @@ mod tests { assert_eq!(Ordinal(2067187500000000 + 1).cycle(), 1); } - #[test] - fn rarity() { - assert_eq!(Ordinal(0).rarity(), "mythic"); - assert_eq!(Ordinal(1).rarity(), "common"); - - assert_eq!(Ordinal(50 * 100_000_000 - 1).rarity(), "common"); - assert_eq!(Ordinal(50 * 100_000_000).rarity(), "uncommon"); - assert_eq!(Ordinal(50 * 100_000_000 + 1).rarity(), "common"); - - assert_eq!(Ordinal(50 * 100_000_000 * 2016 - 1).rarity(), "common"); - assert_eq!(Ordinal(50 * 100_000_000 * 2016).rarity(), "rare"); - assert_eq!(Ordinal(50 * 100_000_000 * 2016 + 1).rarity(), "common"); - - assert_eq!(Ordinal(50 * 100_000_000 * 210000 - 1).rarity(), "common"); - assert_eq!(Ordinal(50 * 100_000_000 * 210000).rarity(), "epic"); - assert_eq!(Ordinal(50 * 100_000_000 * 210000 + 1).rarity(), "common"); - - assert_eq!(Ordinal(2067187500000000 - 1).rarity(), "common"); - assert_eq!(Ordinal(2067187500000000).rarity(), "legendary"); - assert_eq!(Ordinal(2067187500000000 + 1).rarity(), "common"); - } - #[test] fn third() { assert_eq!(Ordinal(0).third(), 0); diff --git a/src/rarity.rs b/src/rarity.rs new file mode 100644 index 0000000000..a4bdc9b7eb --- /dev/null +++ b/src/rarity.rs @@ -0,0 +1,92 @@ +use super::*; + +#[derive(Debug, PartialEq, PartialOrd)] +pub(crate) enum Rarity { + Common, + Uncommon, + Rare, + Epic, + Legendary, + Mythic, +} + +impl Display for Rarity { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Self::Common => "common", + Self::Uncommon => "uncommon", + Self::Rare => "rare", + Self::Epic => "epic", + Self::Legendary => "legendary", + Self::Mythic => "mythic", + } + ) + } +} + +impl From for Rarity { + fn from(ordinal: Ordinal) -> Self { + let Degree { + hour, + minute, + second, + third, + } = ordinal.degree(); + + if hour == 0 && minute == 0 && second == 0 && third == 0 { + Self::Mythic + } else if minute == 0 && second == 0 && third == 0 { + Self::Legendary + } else if minute == 0 && third == 0 { + Self::Epic + } else if second == 0 && third == 0 { + Self::Rare + } else if third == 0 { + Self::Uncommon + } else { + Self::Common + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rarity() { + assert_eq!(Ordinal(0).rarity(), Rarity::Mythic); + assert_eq!(Ordinal(1).rarity(), Rarity::Common); + + assert_eq!(Ordinal(50 * 100_000_000 - 1).rarity(), Rarity::Common); + assert_eq!(Ordinal(50 * 100_000_000).rarity(), Rarity::Uncommon); + assert_eq!(Ordinal(50 * 100_000_000 + 1).rarity(), Rarity::Common); + + assert_eq!( + Ordinal(50 * 100_000_000 * 2016 - 1).rarity(), + Rarity::Common + ); + assert_eq!(Ordinal(50 * 100_000_000 * 2016).rarity(), Rarity::Rare); + assert_eq!( + Ordinal(50 * 100_000_000 * 2016 + 1).rarity(), + Rarity::Common + ); + + assert_eq!( + Ordinal(50 * 100_000_000 * 210000 - 1).rarity(), + Rarity::Common + ); + assert_eq!(Ordinal(50 * 100_000_000 * 210000).rarity(), Rarity::Epic); + assert_eq!( + Ordinal(50 * 100_000_000 * 210000 + 1).rarity(), + Rarity::Common + ); + + assert_eq!(Ordinal(2067187500000000 - 1).rarity(), Rarity::Common); + assert_eq!(Ordinal(2067187500000000).rarity(), Rarity::Legendary); + assert_eq!(Ordinal(2067187500000000 + 1).rarity(), Rarity::Common); + } +} diff --git a/src/subcommand/server/templates/home.rs b/src/subcommand/server/templates/home.rs index 2b3052f78f..ea7bcd15af 100644 --- a/src/subcommand/server/templates/home.rs +++ b/src/subcommand/server/templates/home.rs @@ -3,7 +3,7 @@ use super::*; #[derive(Display)] pub(crate) struct HomeHtml { last: u64, - blocks: Vec<(&'static str, BlockHash)>, + blocks: Vec<(Rarity, BlockHash)>, } impl HomeHtml { diff --git a/src/subcommand/wallet.rs b/src/subcommand/wallet.rs index 65ebb570df..cfcb304a18 100644 --- a/src/subcommand/wallet.rs +++ b/src/subcommand/wallet.rs @@ -2,6 +2,7 @@ use super::*; mod balance; mod fund; +mod identify; mod init; mod send; mod utxos; @@ -10,6 +11,7 @@ mod utxos; pub(crate) enum Wallet { Balance, Fund, + Identify, Init, Send(send::Send), Utxos, @@ -20,6 +22,7 @@ impl Wallet { match self { Self::Balance => balance::run(options), Self::Fund => fund::run(options), + Self::Identify => identify::run(options), Self::Init => init::run(options), Self::Send(send) => send.run(options), Self::Utxos => utxos::run(options), diff --git a/src/subcommand/wallet/identify.rs b/src/subcommand/wallet/identify.rs new file mode 100644 index 0000000000..9ae17e6f24 --- /dev/null +++ b/src/subcommand/wallet/identify.rs @@ -0,0 +1,39 @@ +use super::*; + +pub(crate) fn run(options: Options) -> Result { + let index = Index::index(&options)?; + + let mut ordinals = Purse::load(&options)? + .wallet + .list_unspent()? + .into_iter() + .map(|utxo| { + index.list(utxo.outpoint).and_then(|list| match list { + Some(List::Unspent(ranges)) => Ok( + ranges + .into_iter() + .map(|(start, _end)| Ordinal(start)) + .filter(|ordinal| ordinal.rarity() > Rarity::Common) + .map(|ordinal| (ordinal, utxo.outpoint)) + .collect(), + ), + Some(List::Spent(txid)) => Err(anyhow!( + "UTXO {} unspent in wallet but spent in index by transaction {txid}", + utxo.outpoint + )), + None => Ok(Vec::new()), + }) + }) + .collect::>, _>>()? + .into_iter() + .flatten() + .collect::>(); + + ordinals.sort_by(|(ordinal_a, _), (ordinal_b, _)| ordinal_a.cmp(ordinal_b)); + + for (ordinal, outpoint) in ordinals { + println!("{ordinal} {} {outpoint}", ordinal.rarity()); + } + + Ok(()) +} diff --git a/tests/wallet.rs b/tests/wallet.rs index 43d6e78e81..83dfd6a634 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -189,6 +189,190 @@ fn balance() { .run() } +#[test] +fn identify_single_ordinal() { + let state = Test::new() + .command("--network regtest wallet init") + .expected_status(0) + .expected_stderr("Wallet initialized.\n") + .output() + .state; + + let output = Test::with_state(state) + .command("--network regtest wallet fund") + .stdout_regex("^bcrt1.*\n") + .output(); + + output + .state + .client + .generate_to_address( + 1, + &Address::from_str( + output + .stdout + .strip_suffix('\n') + .ok_or("Failed to strip suffix") + .unwrap(), + ) + .unwrap(), + ) + .unwrap(); + + Test::with_state(output.state) + .command("--network regtest wallet identify") + .expected_status(0) + .stdout_regex("5000000000 uncommon [[:xdigit:]]{64}:[[:digit:]]\n") + .run() +} + +#[test] +fn identify_multiple_ordinals() { + let state = Test::new() + .command("--network regtest wallet init") + .expected_status(0) + .expected_stderr("Wallet initialized.\n") + .output() + .state; + + let output = Test::with_state(state) + .command("--network regtest wallet fund") + .stdout_regex("^bcrt1.*\n") + .output(); + + output + .state + .client + .generate_to_address( + 5, + &Address::from_str( + output + .stdout + .strip_suffix('\n') + .ok_or("Failed to strip suffix") + .unwrap(), + ) + .unwrap(), + ) + .unwrap(); + + Test::with_state(output.state) + .command("--network regtest wallet identify") + .expected_status(0) + .stdout_regex( + " + 5000000000 uncommon [[:xdigit:]]{64}:[[:digit:]] + 10000000000 uncommon [[:xdigit:]]{64}:[[:digit:]] + 15000000000 uncommon [[:xdigit:]]{64}:[[:digit:]] + 20000000000 uncommon [[:xdigit:]]{64}:[[:digit:]] + 25000000000 uncommon [[:xdigit:]]{64}:[[:digit:]] + " + .unindent() + .trim_start(), + ) + .run() +} + +#[test] +fn identify_sent_ordinal() { + let state = Test::new() + .command("--network regtest wallet init") + .expected_status(0) + .expected_stderr("Wallet initialized.\n") + .output() + .state; + + let output = Test::with_state(state) + .command("--network regtest wallet fund") + .stdout_regex("^bcrt1.*\n") + .output(); + + let from_address = Address::from_str( + output + .stdout + .strip_suffix('\n') + .ok_or("Failed to strip suffix") + .unwrap(), + ) + .unwrap(); + + output + .state + .client + .generate_to_address(1, &from_address) + .unwrap(); + + output + .state + .client + .generate_to_address( + 100, + &Address::from_str("bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw").unwrap(), + ) + .unwrap(); + + let output = Test::with_state(output.state) + .command("--network regtest wallet utxos") + .expected_status(0) + .stdout_regex("[[:xdigit:]]{64}:[[:digit:]] 5000000000\n") + .output(); + + let output = Test::with_state(output.state) + .command("--network regtest wallet identify") + .expected_status(0) + .stdout_regex("5000000000 uncommon [[:xdigit:]]{64}:[[:digit:]]\n") + .output(); + + let wallet = Wallet::new( + Bip84( + ( + Mnemonic::parse("book fit fly ketchup also elevator scout mind edit fatal where rookie") + .unwrap(), + None, + ), + KeychainKind::External, + ), + None, + Network::Regtest, + MemoryDatabase::new(), + ) + .unwrap(); + + let to_address = wallet.get_address(AddressIndex::LastUnused).unwrap(); + + let state = Test::with_state(output.state) + .command(&format!( + "--network regtest wallet send --address {to_address} --ordinal 5000000000", + )) + .expected_status(0) + .stdout_regex(format!( + "Sent ordinal 5000000000 to address {to_address}: [[:xdigit:]]{{64}}\n" + )) + .output() + .state; + + wallet + .sync(&state.blockchain, SyncOptions::default()) + .unwrap(); + + state.client.generate_to_address(1, &to_address).unwrap(); + + let output = Test::with_state(state) + .command(&format!( + "--network regtest list {}", + wallet.list_unspent().unwrap().first().unwrap().outpoint + )) + .expected_status(0) + .expected_stdout("[5000000000,9999999780)\n") + .output(); + + Test::with_state(output.state) + .command("--network regtest wallet identify") + .expected_status(0) + .expected_stdout("") + .run() +} + #[test] fn send_owned_ordinal() { let state = Test::new()