From 94585a2ca2bfd511d56e931e12e2b893b16614e5 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Thu, 16 Nov 2023 10:53:44 -0800 Subject: [PATCH] Add inscription charms (#2681) --- src/charm.rs | 66 +++++ src/index/entry.rs | 6 +- src/index/updater/inscription_updater.rs | 148 ++++++------ src/inscription.rs | 12 - src/lib.rs | 2 + src/sat.rs | 20 ++ src/subcommand/server.rs | 294 +++++++++++++++++++++-- src/templates/inscription.rs | 43 +--- templates/inscription.html | 10 + 9 files changed, 457 insertions(+), 144 deletions(-) create mode 100644 src/charm.rs diff --git a/src/charm.rs b/src/charm.rs new file mode 100644 index 0000000000..99865b211e --- /dev/null +++ b/src/charm.rs @@ -0,0 +1,66 @@ +#[derive(Copy, Clone)] +pub(crate) enum Charm { + Cursed, + Epic, + Legendary, + Lost, + Nineball, + Rare, + Reinscription, + Unbound, + Uncommon, +} + +impl Charm { + pub(crate) const ALL: [Charm; 9] = [ + Charm::Uncommon, + Charm::Rare, + Charm::Epic, + Charm::Legendary, + Charm::Nineball, + Charm::Reinscription, + Charm::Cursed, + Charm::Unbound, + Charm::Lost, + ]; + + fn flag(self) -> u16 { + 1 << self as u16 + } + + pub(crate) fn set(self, charms: &mut u16) { + *charms |= self.flag(); + } + + pub(crate) fn is_set(self, charms: u16) -> bool { + charms & self.flag() != 0 + } + + pub(crate) fn icon(self) -> &'static str { + match self { + Charm::Cursed => "👹", + Charm::Epic => "đŸĒģ", + Charm::Legendary => "🌝", + Charm::Lost => "🤔", + Charm::Nineball => "9ī¸âƒŖ", + Charm::Rare => "đŸ§ŋ", + Charm::Reinscription => "â™ģī¸", + Charm::Unbound => "🔓", + Charm::Uncommon => "🌱", + } + } + + pub(crate) fn title(self) -> &'static str { + match self { + Charm::Cursed => "cursed", + Charm::Epic => "epic", + Charm::Legendary => "legendary", + Charm::Lost => "lost", + Charm::Nineball => "nineball", + Charm::Rare => "rare", + Charm::Reinscription => "reinscription", + Charm::Unbound => "unbound", + Charm::Uncommon => "uncommon", + } + } +} diff --git a/src/index/entry.rs b/src/index/entry.rs index d3a8b0d2c0..59e8934b17 100644 --- a/src/index/entry.rs +++ b/src/index/entry.rs @@ -139,6 +139,7 @@ impl Entry for RuneId { #[derive(Debug)] pub(crate) struct InscriptionEntry { + pub(crate) charms: u16, pub(crate) fee: u64, pub(crate) height: u64, pub(crate) inscription_number: i64, @@ -149,6 +150,7 @@ pub(crate) struct InscriptionEntry { } pub(crate) type InscriptionEntryValue = ( + u16, // charms u64, // fee u64, // height i64, // inscription number @@ -162,9 +164,10 @@ impl Entry for InscriptionEntry { type Value = InscriptionEntryValue; fn load( - (fee, height, inscription_number, parent, sat, sequence_number, timestamp): InscriptionEntryValue, + (charms, fee, height, inscription_number, parent, sat, sequence_number, timestamp): InscriptionEntryValue, ) -> Self { Self { + charms, fee, height, inscription_number, @@ -181,6 +184,7 @@ impl Entry for InscriptionEntry { fn store(self) -> Self::Value { ( + self.charms, self.fee, self.height, self.inscription_number, diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index 0b20a9ebec..15a6afef3a 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -1,4 +1,16 @@ -use {super::*, inscription::Curse}; +use super::*; + +#[derive(Debug, PartialEq, Copy, Clone)] +enum Curse { + DuplicateField, + IncompleteField, + NotAtOffsetZero, + NotInFirstInput, + Pointer, + Pushnum, + Reinscription, + UnrecognizedEvenField, +} #[derive(Debug, Clone)] pub(super) struct Flotsam { @@ -15,6 +27,7 @@ enum Origin { hidden: bool, parent: Option, pointer: Option, + reinscription: bool, unbound: bool, }, Old { @@ -123,6 +136,8 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { index: id_counter, }; + let reinscription = inscribed_offsets.contains_key(&offset); + let curse = if inscription.payload.unrecognized_even_field { Some(Curse::UnrecognizedEvenField) } else if inscription.payload.duplicate_field { @@ -137,22 +152,12 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { Some(Curse::Pointer) } else if inscription.pushnum { Some(Curse::Pushnum) - } else if inscribed_offsets.contains_key(&offset) { - let seq_num = self.next_sequence_number; - - let sat = Self::calculate_sat(input_sat_ranges, offset); - - log::info!("processing reinscription {inscription_id} on sat {:?}: sequence number {seq_num}, inscribed offsets {:?}", sat, inscribed_offsets); - + } else if reinscription { Some(Curse::Reinscription) } else { None }; - if curse.is_some() { - log::info!("found cursed inscription {inscription_id}: {:?}", curse); - } - let cursed = if let Some(Curse::Reinscription) = curse { let first_reinscription = inscribed_offsets .get(&offset) @@ -172,8 +177,6 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { }) .unwrap_or(false); - log::info!("{inscription_id}: is first reinscription: {first_reinscription}, initial inscription is cursed: {initial_inscription_is_cursed}"); - !(initial_inscription_is_cursed && first_reinscription) } else { curse.is_some() @@ -181,19 +184,11 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { let unbound = current_input_value == 0 || curse == Some(Curse::UnrecognizedEvenField); - if curse.is_some() || unbound { - log::info!( - "indexing inscription {inscription_id} with curse {:?} as cursed {} and unbound {}", - curse, - cursed, - unbound - ); - } - floating_inscriptions.push(Flotsam { inscription_id, offset, origin: Origin::New { + reinscription, cursed, fee: 0, hidden: inscription.payload.hidden(), @@ -229,40 +224,16 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { // still have to normalize over inscription size let total_output_value = tx.output.iter().map(|txout| txout.value).sum::(); - let mut floating_inscriptions = floating_inscriptions - .into_iter() - .map(|flotsam| { - if let Flotsam { - inscription_id, - offset, - origin: - Origin::New { - cursed, - fee: _, - hidden, - parent, - pointer, - unbound, - }, - } = flotsam - { - Flotsam { - inscription_id, - offset, - origin: Origin::New { - cursed, - fee: (total_input_value - total_output_value) / u64::from(id_counter), - hidden, - parent, - pointer, - unbound, - }, - } - } else { - flotsam - } - }) - .collect::>(); + + for flotsam in &mut floating_inscriptions { + if let Flotsam { + origin: Origin::New { ref mut fee, .. }, + .. + } = flotsam + { + *fee = (total_input_value - total_output_value) / u64::from(id_counter); + } + } let is_coinbase = tx .input @@ -361,20 +332,19 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { input_sat_ranges: Option<&VecDeque<(u64, u64)>>, input_offset: u64, ) -> Option { - let mut sat = None; - if let Some(input_sat_ranges) = input_sat_ranges { - let mut offset = 0; - for (start, end) in input_sat_ranges { - let size = end - start; - if offset + size > input_offset { - let n = start + input_offset - offset; - sat = Some(Sat(n)); - break; - } - offset += size; + let input_sat_ranges = input_sat_ranges?; + + let mut offset = 0; + for (start, end) in input_sat_ranges { + let size = end - start; + if offset + size > input_offset { + let n = start + input_offset - offset; + return Some(Sat(n)); } + offset += size; } - sat + + unreachable!() } fn update_inscription_location( @@ -393,10 +363,11 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { Origin::New { cursed, fee, + hidden, parent, + pointer: _, + reinscription, unbound, - hidden, - .. } => { let inscription_number = if cursed { let number: i64 = self.cursed_inscription_count.try_into().unwrap(); @@ -428,6 +399,38 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { Self::calculate_sat(input_sat_ranges, flotsam.offset) }; + let mut charms = 0; + + if cursed { + Charm::Cursed.set(&mut charms); + } + + if reinscription { + Charm::Reinscription.set(&mut charms); + } + + if let Some(sat) = sat { + if sat.nineball() { + Charm::Nineball.set(&mut charms); + } + + match sat.rarity() { + Rarity::Common | Rarity::Mythic => {} + Rarity::Uncommon => Charm::Uncommon.set(&mut charms), + Rarity::Rare => Charm::Rare.set(&mut charms), + Rarity::Epic => Charm::Epic.set(&mut charms), + Rarity::Legendary => Charm::Legendary.set(&mut charms), + } + } + + if new_satpoint.outpoint == OutPoint::null() { + Charm::Lost.set(&mut charms); + } + + if unbound { + Charm::Unbound.set(&mut charms); + } + if let Some(Sat(n)) = sat { self.sat_to_inscription_id.insert(&n, &inscription_id)?; } @@ -435,12 +438,13 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { self.id_to_entry.insert( &inscription_id, &InscriptionEntry { + charms, fee, height: self.height, inscription_number, - sequence_number, parent, sat, + sequence_number, timestamp: self.timestamp, } .store(), diff --git a/src/inscription.rs b/src/inscription.rs index ce6c072628..0158cff4fb 100644 --- a/src/inscription.rs +++ b/src/inscription.rs @@ -11,18 +11,6 @@ use { std::str, }; -#[derive(Debug, PartialEq, Clone)] -pub(crate) enum Curse { - DuplicateField, - IncompleteField, - NotAtOffsetZero, - NotInFirstInput, - Pointer, - Pushnum, - Reinscription, - UnrecognizedEvenField, -} - #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Eq, Default)] pub struct Inscription { pub body: Option>, diff --git a/src/lib.rs b/src/lib.rs index c1aca40a89..a760a92038 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,7 @@ use { self::{ arguments::Arguments, blocktime::Blocktime, + charm::Charm, config::Config, decimal::Decimal, degree::Degree, @@ -108,6 +109,7 @@ macro_rules! tprintln { mod arguments; mod blocktime; mod chain; +mod charm; mod config; mod decimal; mod degree; diff --git a/src/sat.rs b/src/sat.rs index 3d9ed465c9..eab9fc48fc 100644 --- a/src/sat.rs +++ b/src/sat.rs @@ -20,6 +20,10 @@ impl Sat { self.epoch().starting_height() + self.epoch_position() / self.epoch().subsidy() } + pub(crate) fn nineball(self) -> bool { + self.n() >= 50 * COIN_VALUE * 9 && self.n() < 50 * COIN_VALUE * 10 + } + pub(crate) fn cycle(self) -> u64 { Epoch::from(self).0 / CYCLE_EPOCHS } @@ -591,6 +595,7 @@ mod tests { #[test] fn percentile_round_trip() { + #[track_caller] fn case(n: u64) { let expected = Sat(n); let actual = expected.percentile().parse::().unwrap(); @@ -607,6 +612,7 @@ mod tests { #[test] fn is_common() { + #[track_caller] fn case(n: u64) { assert_eq!(Sat(n).is_common(), Sat(n).rarity() == Rarity::Common); } @@ -620,4 +626,18 @@ mod tests { case(2067187500000000); case(2067187500000000 + 1); } + + #[test] + fn nineball() { + for height in 0..10 { + let sat = Sat(height * 50 * COIN_VALUE); + assert_eq!( + sat.nineball(), + sat.height() == 9, + "nineball: {} height: {}", + sat.nineball(), + sat.height() + ); + } + } } diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 8242f27245..631cf651d9 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -1137,7 +1137,8 @@ impl Server { .get_inscription_satpoint_by_id(inscription_id)? .ok_or_not_found(|| format!("inscription {inscription_id}"))?; - let output = if satpoint.outpoint == unbound_outpoint() { + let output = if satpoint.outpoint == unbound_outpoint() || satpoint.outpoint == OutPoint::null() + { None } else { Some( @@ -1164,28 +1165,39 @@ impl Server { let rune = index.get_rune_by_inscription_id(inscription_id)?; + let mut charms = entry.charms; + + if satpoint.outpoint == OutPoint::null() { + Charm::Lost.set(&mut charms); + } + Ok(if accept_json.0 { - Json(InscriptionJson::new( - page_config.chain, - children, - entry.fee, - entry.height, - inscription, + Json(InscriptionJson { inscription_id, - entry.parent, - next, - entry.inscription_number, - output, - previous, - entry.sat, + children, + inscription_number: entry.inscription_number, + genesis_height: entry.height, + parent: entry.parent, + genesis_fee: entry.fee, + output_value: output.as_ref().map(|o| o.value), + address: output + .as_ref() + .and_then(|o| page_config.chain.address_from_script(&o.script_pubkey).ok()) + .map(|address| address.to_string()), + sat: entry.sat, satpoint, - timestamp(entry.timestamp), + content_type: inscription.content_type().map(|s| s.to_string()), + content_length: inscription.content_length(), + timestamp: timestamp(entry.timestamp).timestamp(), + previous, + next, rune, - )) + }) .into_response() } else { InscriptionHtml { chain: page_config.chain, + charms, children, genesis_fee: entry.fee, genesis_height: entry.height, @@ -2236,8 +2248,7 @@ mod tests { format!( ".*
id
-
{inscription_id}
-
preview
.*
output
+
{inscription_id}
.*
output
0000000000000000000000000000000000000000000000000000000000000000:0
.*" ), ); @@ -3907,4 +3918,253 @@ mod tests { } ); } + + #[test] + fn charm_cursed() { + let server = TestServer::new_with_regtest(); + + server.mine_blocks(2); + + let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[ + (1, 0, 0, Witness::default()), + (2, 0, 0, inscription("text/plain", "cursed").to_witness()), + ], + outputs: 2, + ..Default::default() + }); + + let id = InscriptionId { txid, index: 0 }; + + server.mine_blocks(1); + + server.assert_response_regex( + format!("/inscription/{id}"), + StatusCode::OK, + format!( + ".*

Inscription -1

.* +
+
id
+
{id}
+
charms
+
+ 👹 +
+ .* +
+.* +" + ), + ); + } + + #[test] + fn charm_uncommon() { + let server = TestServer::new_with_regtest_with_index_sats(); + + server.mine_blocks(2); + + let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())], + ..Default::default() + }); + + let id = InscriptionId { txid, index: 0 }; + + server.mine_blocks(1); + + server.assert_response_regex( + format!("/inscription/{id}"), + StatusCode::OK, + format!( + ".*

Inscription 0

.* +
+
id
+
{id}
+
charms
+
+ 🌱 +
+ .* +
+.* +" + ), + ); + } + + #[test] + fn charm_nineball() { + let server = TestServer::new_with_regtest_with_index_sats(); + + server.mine_blocks(9); + + let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(9, 0, 0, inscription("text/plain", "foo").to_witness())], + ..Default::default() + }); + + let id = InscriptionId { txid, index: 0 }; + + server.mine_blocks(1); + + server.assert_response_regex( + format!("/inscription/{id}"), + StatusCode::OK, + format!( + ".*

Inscription 0

.* +
+
id
+
{id}
+
charms
+
+ 🌱 + 9ī¸âƒŖ +
+ .* +
+.* +" + ), + ); + } + + #[test] + fn charm_reinscription() { + let server = TestServer::new_with_regtest(); + + server.mine_blocks(1); + + server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())], + ..Default::default() + }); + + server.mine_blocks(1); + + let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 1, 0, inscription("text/plain", "bar").to_witness())], + ..Default::default() + }); + + server.mine_blocks(1); + + let id = InscriptionId { txid, index: 0 }; + + server.assert_response_regex( + format!("/inscription/{id}"), + StatusCode::OK, + format!( + ".*

Inscription -1

.* +
+
id
+
{id}
+
charms
+
+ â™ģī¸ + 👹 +
+ .* +
+.* +" + ), + ); + } + + #[test] + fn charm_unbound() { + let server = TestServer::new_with_regtest(); + + server.mine_blocks(1); + + let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, envelope(&[b"ord", &[128], &[0]]))], + ..Default::default() + }); + + server.mine_blocks(1); + + let id = InscriptionId { txid, index: 0 }; + + server.assert_response_regex( + format!("/inscription/{id}"), + StatusCode::OK, + format!( + ".*

Inscription -1

.* +
+
id
+
{id}
+
charms
+
+ 👹 + 🔓 +
+ .* +
+.* +" + ), + ); + } + + #[test] + fn charm_lost() { + let server = TestServer::new_with_regtest(); + + server.mine_blocks(1); + + let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())], + ..Default::default() + }); + + let id = InscriptionId { txid, index: 0 }; + + server.mine_blocks(1); + + server.assert_response_regex( + format!("/inscription/{id}"), + StatusCode::OK, + format!( + ".*

Inscription 0

.* +
+
id
+
{id}
+
output value
+
5000000000
+ .* +
+.* +" + ), + ); + + server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 1, 0, Default::default())], + fee: 50 * COIN_VALUE, + ..Default::default() + }); + + server.mine_blocks_with_subsidy(1, 0); + + server.assert_response_regex( + format!("/inscription/{id}"), + StatusCode::OK, + format!( + ".*

Inscription 0

.* +
+
id
+
{id}
+
charms
+
+ 🤔 +
+ .* +
+.* +" + ), + ); + } } diff --git a/src/templates/inscription.rs b/src/templates/inscription.rs index ce5e4f9011..9066449375 100644 --- a/src/templates/inscription.rs +++ b/src/templates/inscription.rs @@ -17,6 +17,7 @@ pub(crate) struct InscriptionHtml { pub(crate) sat: Option, pub(crate) satpoint: SatPoint, pub(crate) timestamp: DateTime, + pub(crate) charms: u16, } #[derive(Debug, PartialEq, Serialize, Deserialize)] @@ -39,48 +40,6 @@ pub struct InscriptionJson { pub timestamp: i64, } -impl InscriptionJson { - pub fn new( - chain: Chain, - children: Vec, - genesis_fee: u64, - genesis_height: u64, - inscription: Inscription, - inscription_id: InscriptionId, - parent: Option, - next: Option, - inscription_number: i64, - output: Option, - previous: Option, - sat: Option, - satpoint: SatPoint, - timestamp: DateTime, - rune: Option, - ) -> Self { - Self { - inscription_id, - children, - inscription_number, - genesis_height, - parent, - genesis_fee, - output_value: output.as_ref().map(|o| o.value), - address: output - .as_ref() - .and_then(|o| chain.address_from_script(&o.script_pubkey).ok()) - .map(|address| address.to_string()), - sat, - satpoint, - content_type: inscription.content_type().map(|s| s.to_string()), - content_length: inscription.content_length(), - timestamp: timestamp.timestamp(), - previous, - next, - rune, - } - } -} - impl PageContent for InscriptionHtml { fn title(&self) -> String { format!("Inscription {}", self.inscription_number) diff --git a/templates/inscription.html b/templates/inscription.html index b543d4f8e6..118f43857c 100644 --- a/templates/inscription.html +++ b/templates/inscription.html @@ -38,6 +38,16 @@

Inscription {{ self.inscription_number }}

parent
{{ parent }}
%% } +%% if self.charms != 0 { +
charms
+
+%% for charm in Charm::ALL { +%% if charm.is_set(self.charms) { + {{charm.icon()}} +%% } +%% } +
+%% } %% if let Some(output) = &self.output { %% if let Ok(address) = self.chain.address_from_script(&output.script_pubkey ) {
address