diff --git a/src/api.rs b/src/api.rs index a6810f97cd..a1b9c5127b 100644 --- a/src/api.rs +++ b/src/api.rs @@ -88,7 +88,7 @@ pub struct Inscription { pub id: InscriptionId, pub next: Option, pub number: i32, - pub parent: Option, + pub parents: Vec, pub previous: Option, pub rune: Option, pub sat: Option, diff --git a/src/index.rs b/src/index.rs index 39aed46b65..5caaf1439b 100644 --- a/src/index.rs +++ b/src/index.rs @@ -46,7 +46,7 @@ mod updater; #[cfg(test)] pub(crate) mod testing; -const SCHEMA_VERSION: u64 = 18; +const SCHEMA_VERSION: u64 = 19; macro_rules! define_table { ($name:ident, $key:ty, $value:ty) => { @@ -171,7 +171,7 @@ pub(crate) struct TransactionInfo { pub(crate) struct InscriptionInfo { pub(crate) children: Vec, pub(crate) entry: InscriptionEntry, - pub(crate) parent: Option, + pub(crate) parents: Vec, pub(crate) output: Option, pub(crate) satpoint: SatPoint, pub(crate) inscription: Inscription, @@ -1070,10 +1070,10 @@ impl Index { } #[cfg(test)] - pub(crate) fn get_parent_by_inscription_id( + pub(crate) fn get_parents_by_inscription_id( &self, inscription_id: InscriptionId, - ) -> InscriptionId { + ) -> Vec { let rtx = self.database.begin_read().unwrap(); let sequence_number = rtx @@ -1088,25 +1088,28 @@ impl Index { .open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY) .unwrap(); - let parent_sequence_number = InscriptionEntry::load( + let parent_sequences = InscriptionEntry::load( sequence_number_to_inscription_entry .get(sequence_number) .unwrap() .unwrap() .value(), ) - .parent - .unwrap(); + .parents; - let entry = InscriptionEntry::load( - sequence_number_to_inscription_entry - .get(parent_sequence_number) - .unwrap() - .unwrap() - .value(), - ); - - entry.id + parent_sequences + .into_iter() + .map(|parent_sequence_number| { + InscriptionEntry::load( + sequence_number_to_inscription_entry + .get(parent_sequence_number) + .unwrap() + .unwrap() + .value(), + ) + .id + }) + .collect() } pub(crate) fn get_children_by_sequence_number_paginated( @@ -1144,6 +1147,37 @@ impl Index { Ok((children, more)) } + pub(crate) fn get_parents_by_sequence_number_paginated( + &self, + parent_sequence_numbers: Vec, + page_index: usize, + ) -> Result<(Vec, bool)> { + const PAGE_SIZE: usize = 100; + let rtx = self.database.begin_read()?; + + let sequence_number_to_entry = rtx.open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY)?; + + let mut parents = parent_sequence_numbers + .iter() + .skip(page_index * PAGE_SIZE) + .take(PAGE_SIZE.saturating_add(1)) + .map(|sequence_number| { + sequence_number_to_entry + .get(sequence_number) + .map(|entry| InscriptionEntry::load(entry.unwrap().value()).id) + .map_err(|err| err.into()) + }) + .collect::>>()?; + + let more_parents = parents.len() > PAGE_SIZE; + + if more_parents { + parents.pop(); + } + + Ok((parents, more_parents)) + } + pub(crate) fn get_etching(&self, txid: Txid) -> Result> { let rtx = self.database.begin_read()?; @@ -1812,18 +1846,22 @@ impl Index { None }; - let parent = match entry.parent { - Some(parent) => Some( - InscriptionEntry::load( - sequence_number_to_inscription_entry - .get(parent)? - .unwrap() - .value(), + let parents = entry + .parents + .iter() + .take(4) + .map(|parent| { + Ok( + InscriptionEntry::load( + sequence_number_to_inscription_entry + .get(parent)? + .unwrap() + .value(), + ) + .id, ) - .id, - ), - None => None, - }; + }) + .collect::>>()?; let mut charms = entry.charms; @@ -1834,7 +1872,7 @@ impl Index { Ok(Some(InscriptionInfo { children, entry, - parent, + parents, output, satpoint, inscription, @@ -4140,7 +4178,7 @@ mod tests { for context in Context::configurations() { context.mine_blocks(1); - let mut inscription_ids = vec![]; + let mut inscription_ids = Vec::new(); for i in 1..=21 { let txid = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[( @@ -4387,8 +4425,8 @@ mod tests { .get_inscription_entry(inscription_id) .unwrap() .unwrap() - .parent - .is_none()); + .parents + .is_empty()); } } @@ -4417,7 +4455,7 @@ mod tests { Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.value()), + parents: vec![parent_inscription_id.value()], ..Default::default() } .to_witness(), @@ -4434,8 +4472,8 @@ mod tests { .get_inscription_entry(inscription_id) .unwrap() .unwrap() - .parent - .is_none()); + .parents + .is_empty()); } } @@ -4464,7 +4502,7 @@ mod tests { Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.value()), + parents: vec![parent_inscription_id.value()], ..Default::default() } .to_witness(), @@ -4477,8 +4515,8 @@ mod tests { let inscription_id = InscriptionId { txid, index: 0 }; assert_eq!( - context.index.get_parent_by_inscription_id(inscription_id), - parent_inscription_id + context.index.get_parents_by_inscription_id(inscription_id), + vec![parent_inscription_id] ); assert_eq!( @@ -4491,6 +4529,367 @@ mod tests { } } + #[test] + fn inscription_with_two_parent_tags_and_parents_has_parent_entries() { + for context in Context::configurations() { + context.mine_blocks(2); + + let parent_txid_a = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], + ..Default::default() + }); + let parent_txid_b = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0, inscription("text/plain", "world").to_witness())], + ..Default::default() + }); + + context.mine_blocks(1); + + let parent_inscription_id_a = InscriptionId { + txid: parent_txid_a, + index: 0, + }; + let parent_inscription_id_b = InscriptionId { + txid: parent_txid_b, + index: 0, + }; + + let multi_parent_inscription = Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + parents: vec![ + parent_inscription_id_a.value(), + parent_inscription_id_b.value(), + ], + ..Default::default() + }; + let multi_parent_witness = multi_parent_inscription.to_witness(); + + let revelation_input = (3, 1, 0, multi_parent_witness); + + let parent_b_input = (3, 2, 0, Witness::new()); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[revelation_input, parent_b_input], + ..Default::default() + }); + + context.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + + assert_eq!( + context.index.get_parents_by_inscription_id(inscription_id), + vec![parent_inscription_id_a, parent_inscription_id_b] + ); + + assert_eq!( + context + .index + .get_children_by_inscription_id(parent_inscription_id_a) + .unwrap(), + vec![inscription_id] + ); + assert_eq!( + context + .index + .get_children_by_inscription_id(parent_inscription_id_b) + .unwrap(), + vec![inscription_id] + ); + } + } + + #[test] + fn inscription_with_repeated_parent_tags_and_parents_has_singular_parent_entry() { + for context in Context::configurations() { + context.mine_blocks(1); + + let parent_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], + ..Default::default() + }); + + context.mine_blocks(1); + + let parent_inscription_id = InscriptionId { + txid: parent_txid, + index: 0, + }; + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[( + 2, + 1, + 0, + Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + parents: vec![parent_inscription_id.value(), parent_inscription_id.value()], + ..Default::default() + } + .to_witness(), + )], + ..Default::default() + }); + + context.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + + assert_eq!( + context.index.get_parents_by_inscription_id(inscription_id), + vec![parent_inscription_id] + ); + + assert_eq!( + context + .index + .get_children_by_inscription_id(parent_inscription_id) + .unwrap(), + vec![inscription_id] + ); + } + } + + #[test] + fn inscription_with_distinct_parent_tag_encodings_for_same_parent_has_singular_parent_entry() { + for context in Context::configurations() { + context.mine_blocks(1); + + let parent_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], + ..Default::default() + }); + + context.mine_blocks(1); + + let parent_inscription_id = InscriptionId { + txid: parent_txid, + index: 0, + }; + + let trailing_zero_inscription_id: Vec = parent_inscription_id + .value() + .into_iter() + .chain(vec![0, 0, 0, 0]) + .collect(); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[( + 2, + 1, + 0, + Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + parents: vec![parent_inscription_id.value(), trailing_zero_inscription_id], + ..Default::default() + } + .to_witness(), + )], + ..Default::default() + }); + + context.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + + assert_eq!( + context.index.get_parents_by_inscription_id(inscription_id), + vec![parent_inscription_id] + ); + + assert_eq!( + context + .index + .get_children_by_inscription_id(parent_inscription_id) + .unwrap(), + vec![inscription_id] + ); + } + } + + #[test] + fn inscription_with_three_parent_tags_and_two_parents_has_two_parent_entries() { + for context in Context::configurations() { + context.mine_blocks(3); + + let parent_txid_a = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], + ..Default::default() + }); + let parent_txid_b = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0, inscription("text/plain", "world").to_witness())], + ..Default::default() + }); + let parent_txid_c = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(3, 0, 0, inscription("text/plain", "wazzup").to_witness())], + ..Default::default() + }); + + context.mine_blocks(1); + + let parent_inscription_id_a = InscriptionId { + txid: parent_txid_a, + index: 0, + }; + let parent_inscription_id_b = InscriptionId { + txid: parent_txid_b, + index: 0, + }; + let parent_inscription_id_c = InscriptionId { + txid: parent_txid_c, + index: 0, + }; + + let multi_parent_inscription = Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + parents: vec![ + parent_inscription_id_a.value(), + parent_inscription_id_b.value(), + parent_inscription_id_c.value(), + ], + ..Default::default() + }; + let multi_parent_witness = multi_parent_inscription.to_witness(); + + let revealing_parent_a_input = (4, 1, 0, multi_parent_witness); + + let parent_c_input = (4, 3, 0, Witness::new()); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[revealing_parent_a_input, parent_c_input], + ..Default::default() + }); + + context.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + + assert_eq!( + context.index.get_parents_by_inscription_id(inscription_id), + vec![parent_inscription_id_a, parent_inscription_id_c] + ); + + assert_eq!( + context + .index + .get_children_by_inscription_id(parent_inscription_id_a) + .unwrap(), + vec![inscription_id] + ); + assert_eq!( + context + .index + .get_children_by_inscription_id(parent_inscription_id_b) + .unwrap(), + Vec::new() + ); + assert_eq!( + context + .index + .get_children_by_inscription_id(parent_inscription_id_c) + .unwrap(), + vec![inscription_id] + ); + } + } + + #[test] + fn inscription_with_valid_and_malformed_parent_tags_only_lists_valid_entries() { + for context in Context::configurations() { + context.mine_blocks(3); + + let parent_txid_a = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], + ..Default::default() + }); + let parent_txid_b = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0, inscription("text/plain", "world").to_witness())], + ..Default::default() + }); + let parent_txid_c = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(3, 0, 0, inscription("text/plain", "wazzup").to_witness())], + ..Default::default() + }); + + context.mine_blocks(1); + + let parent_inscription_id_a = InscriptionId { + txid: parent_txid_a, + index: 0, + }; + let parent_inscription_id_b = InscriptionId { + txid: parent_txid_b, + index: 0, + }; + let parent_inscription_id_c = InscriptionId { + txid: parent_txid_c, + index: 0, + }; + + let malformed_inscription_id_b = parent_inscription_id_b + .value() + .into_iter() + .chain(iter::once(0)) + .collect(); + + let multi_parent_inscription = Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + parents: vec![ + parent_inscription_id_a.value(), + malformed_inscription_id_b, + parent_inscription_id_c.value(), + ], + ..Default::default() + }; + let multi_parent_witness = multi_parent_inscription.to_witness(); + + let revealing_parent_a_input = (4, 1, 0, multi_parent_witness); + let parent_b_input = (4, 2, 0, Witness::new()); + let parent_c_input = (4, 3, 0, Witness::new()); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[revealing_parent_a_input, parent_b_input, parent_c_input], + ..Default::default() + }); + + context.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + + assert_eq!( + context.index.get_parents_by_inscription_id(inscription_id), + vec![parent_inscription_id_a, parent_inscription_id_c] + ); + + assert_eq!( + context + .index + .get_children_by_inscription_id(parent_inscription_id_a) + .unwrap(), + vec![inscription_id] + ); + assert_eq!( + context + .index + .get_children_by_inscription_id(parent_inscription_id_b) + .unwrap(), + Vec::new() + ); + assert_eq!( + context + .index + .get_children_by_inscription_id(parent_inscription_id_c) + .unwrap(), + vec![inscription_id] + ); + } + } + #[test] fn parents_can_be_in_preceding_input() { for context in Context::configurations() { @@ -4518,7 +4917,7 @@ mod tests { Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.value()), + parents: vec![parent_inscription_id.value()], ..Default::default() } .to_witness(), @@ -4532,8 +4931,8 @@ mod tests { let inscription_id = InscriptionId { txid, index: 0 }; assert_eq!( - context.index.get_parent_by_inscription_id(inscription_id), - parent_inscription_id + context.index.get_parents_by_inscription_id(inscription_id), + vec![parent_inscription_id] ); assert_eq!( @@ -4572,7 +4971,7 @@ mod tests { Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.value()), + parents: vec![parent_inscription_id.value()], ..Default::default() } .to_witness(), @@ -4587,8 +4986,8 @@ mod tests { let inscription_id = InscriptionId { txid, index: 0 }; assert_eq!( - context.index.get_parent_by_inscription_id(inscription_id), - parent_inscription_id + context.index.get_parents_by_inscription_id(inscription_id), + vec![parent_inscription_id] ); assert_eq!( @@ -4626,13 +5025,11 @@ mod tests { Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some( - parent_inscription_id - .value() - .into_iter() - .chain(iter::once(0)) - .collect(), - ), + parents: vec![parent_inscription_id + .value() + .into_iter() + .chain(iter::once(0)) + .collect()], ..Default::default() } .to_witness(), @@ -4649,8 +5046,8 @@ mod tests { .get_inscription_entry(inscription_id) .unwrap() .unwrap() - .parent - .is_none()); + .parents + .is_empty()); } } @@ -4805,7 +5202,7 @@ mod tests { let child_inscription = Inscription { content_type: Some("text/plain".into()), body: Some("pointer-child".into()), - parent: Some(parent_inscription_id.value()), + parents: vec![parent_inscription_id.value()], pointer: Some(0u64.to_le_bytes().to_vec()), ..Default::default() }; @@ -4842,8 +5239,8 @@ mod tests { assert_eq!( context .index - .get_parent_by_inscription_id(child_inscription_id), - parent_inscription_id + .get_parents_by_inscription_id(child_inscription_id), + vec![parent_inscription_id] ); assert_eq!( @@ -5723,7 +6120,7 @@ mod tests { sequence_number: 0, block_height: 2, charms: expected_charms, - parent_inscription_id: None + parent_inscription_ids: Vec::new(), } ); diff --git a/src/index/entry.rs b/src/index/entry.rs index 91923082fe..d7adb0f314 100644 --- a/src/index/entry.rs +++ b/src/index/entry.rs @@ -192,14 +192,14 @@ impl Entry for RuneId { } } -#[derive(Debug)] +#[derive(Debug, Eq, PartialEq, Clone)] pub(crate) struct InscriptionEntry { pub(crate) charms: u16, pub(crate) fee: u64, pub(crate) height: u32, pub(crate) id: InscriptionId, pub(crate) inscription_number: i32, - pub(crate) parent: Option, + pub(crate) parents: Vec, pub(crate) sat: Option, pub(crate) sequence_number: u32, pub(crate) timestamp: u32, @@ -211,7 +211,7 @@ pub(crate) type InscriptionEntryValue = ( u32, // height InscriptionIdValue, // inscription id i32, // inscription number - Option, // parent + Vec, // parents Option, // sat u32, // sequence number u32, // timestamp @@ -228,7 +228,7 @@ impl Entry for InscriptionEntry { height, id, inscription_number, - parent, + parents, sat, sequence_number, timestamp, @@ -240,7 +240,7 @@ impl Entry for InscriptionEntry { height, id: InscriptionId::load(id), inscription_number, - parent, + parents, sat: sat.map(Sat), sequence_number, timestamp, @@ -254,7 +254,7 @@ impl Entry for InscriptionEntry { self.height, self.id.store(), self.inscription_number, - self.parent, + self.parents, self.sat.map(Sat::n), self.sequence_number, self.timestamp, @@ -397,6 +397,30 @@ impl Entry for Txid { mod tests { use super::*; + #[test] + fn inscription_entry() { + let id = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdefi0" + .parse::() + .unwrap(); + + let entry = InscriptionEntry { + charms: 0, + fee: 1, + height: 2, + id, + inscription_number: 3, + parents: vec![4, 5, 6], + sat: Some(Sat(7)), + sequence_number: 8, + timestamp: 9, + }; + + let value = (0, 1, 2, id.store(), 3, vec![4, 5, 6], Some(7), 8, 9); + + assert_eq!(entry.clone().store(), value); + assert_eq!(InscriptionEntry::load(value), entry); + } + #[test] fn inscription_id_entry() { let inscription_id = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdefi0" diff --git a/src/index/event.rs b/src/index/event.rs index c650691158..997a70ebb4 100644 --- a/src/index/event.rs +++ b/src/index/event.rs @@ -7,7 +7,7 @@ pub enum Event { charms: u16, inscription_id: InscriptionId, location: Option, - parent_inscription_id: Option, + parent_inscription_ids: Vec, sequence_number: u32, }, InscriptionTransferred { diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index ee10151f1e..4dd5be7e82 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -26,7 +26,7 @@ enum Origin { cursed: bool, fee: u64, hidden: bool, - parent: Option, + parents: Vec, pointer: Option, reinscription: bool, unbound: bool, @@ -215,7 +215,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { cursed: curse.is_some() && !jubilant, fee: 0, hidden: inscription.payload.hidden(), - parent: inscription.payload.parent(), + parents: inscription.payload.parents(), pointer: inscription.payload.pointer(), reinscription: inscribed_offsets.get(&offset).is_some(), unbound: current_input_value == 0 @@ -253,15 +253,16 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { for flotsam in &mut floating_inscriptions { if let Flotsam { - origin: Origin::New { parent, .. }, + origin: Origin::New { + parents: purported_parents, + .. + }, .. } = flotsam { - if let Some(purported_parent) = parent { - if !potential_parents.contains(purported_parent) { - *parent = None; - } - } + let mut seen = HashSet::new(); + purported_parents + .retain(|parent| seen.insert(*parent) && potential_parents.contains(parent)); } } @@ -423,7 +424,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { cursed, fee, hidden, - parent, + parents, pointer: _, reinscription, unbound, @@ -496,21 +497,22 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { self.sat_to_sequence_number.insert(&n, &sequence_number)?; } - let parent_sequence_number = match parent { - Some(parent_id) => { + let parent_sequence_numbers = parents + .iter() + .map(|parent| { let parent_sequence_number = self .id_to_sequence_number - .get(&parent_id.store())? + .get(&parent.store())? .unwrap() .value(); + self .sequence_number_to_children .insert(parent_sequence_number, sequence_number)?; - Some(parent_sequence_number) - } - None => None, - }; + Ok(parent_sequence_number) + }) + .collect::>>()?; if let Some(sender) = self.event_sender { sender.blocking_send(Event::InscriptionCreated { @@ -518,7 +520,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { charms, inscription_id, location: (!unbound).then_some(new_satpoint), - parent_inscription_id: parent, + parent_inscription_ids: parents, sequence_number, })?; } @@ -531,7 +533,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { height: self.height, id: inscription_id, inscription_number, - parent: parent_sequence_number, + parents: parent_sequence_numbers, sat, sequence_number, timestamp: self.timestamp, diff --git a/src/inscriptions/envelope.rs b/src/inscriptions/envelope.rs index 0ef863c831..2f739edbf1 100644 --- a/src/inscriptions/envelope.rs +++ b/src/inscriptions/envelope.rs @@ -48,13 +48,13 @@ impl From for ParsedEnvelope { let duplicate_field = fields.iter().any(|(_key, values)| values.len() > 1); - let content_encoding = Tag::ContentEncoding.remove_field(&mut fields); - let content_type = Tag::ContentType.remove_field(&mut fields); - let delegate = Tag::Delegate.remove_field(&mut fields); - let metadata = Tag::Metadata.remove_field(&mut fields); - let metaprotocol = Tag::Metaprotocol.remove_field(&mut fields); - let parent = Tag::Parent.remove_field(&mut fields); - let pointer = Tag::Pointer.remove_field(&mut fields); + let content_encoding = Tag::ContentEncoding.take(&mut fields); + let content_type = Tag::ContentType.take(&mut fields); + let delegate = Tag::Delegate.take(&mut fields); + let metadata = Tag::Metadata.take(&mut fields); + let metaprotocol = Tag::Metaprotocol.take(&mut fields); + let parents = Tag::Parent.take_array(&mut fields); + let pointer = Tag::Pointer.take(&mut fields); let unrecognized_even_field = fields .keys() @@ -76,7 +76,7 @@ impl From for ParsedEnvelope { incomplete_field, metadata, metaprotocol, - parent, + parents, pointer, unrecognized_even_field, }, @@ -855,7 +855,7 @@ mod tests { parse(&[envelope(&[&PROTOCOL_ID, Tag::Metadata.bytes(), &[]])]), vec![ParsedEnvelope { payload: Inscription { - metadata: Some(vec![]), + metadata: Some(Vec::new()), ..Default::default() }, ..Default::default() diff --git a/src/inscriptions/inscription.rs b/src/inscriptions/inscription.rs index 057f34f76e..39179693a8 100644 --- a/src/inscriptions/inscription.rs +++ b/src/inscriptions/inscription.rs @@ -21,7 +21,7 @@ pub struct Inscription { pub incomplete_field: bool, pub metadata: Option>, pub metaprotocol: Option>, - pub parent: Option>, + pub parents: Vec>, pub pointer: Option>, pub unrecognized_even_field: bool, } @@ -42,7 +42,7 @@ impl Inscription { delegate: Option, metadata: Option>, metaprotocol: Option, - parent: Option, + parents: Vec, path: impl AsRef, pointer: Option, ) -> Result { @@ -102,7 +102,7 @@ impl Inscription { delegate: delegate.map(|delegate| delegate.value()), metadata, metaprotocol: metaprotocol.map(|metaprotocol| metaprotocol.into_bytes()), - parent: parent.map(|parent| parent.value()), + parents: parents.iter().map(|parent| parent.value()).collect(), pointer: pointer.map(Self::pointer_value), ..Default::default() }) @@ -127,13 +127,13 @@ impl Inscription { .push_opcode(opcodes::all::OP_IF) .push_slice(envelope::PROTOCOL_ID); - Tag::ContentType.encode(&mut builder, &self.content_type); - Tag::ContentEncoding.encode(&mut builder, &self.content_encoding); - Tag::Metaprotocol.encode(&mut builder, &self.metaprotocol); - Tag::Parent.encode(&mut builder, &self.parent); - Tag::Delegate.encode(&mut builder, &self.delegate); - Tag::Pointer.encode(&mut builder, &self.pointer); - Tag::Metadata.encode(&mut builder, &self.metadata); + Tag::ContentType.append(&mut builder, &self.content_type); + Tag::ContentEncoding.append(&mut builder, &self.content_encoding); + Tag::Metaprotocol.append(&mut builder, &self.metaprotocol); + Tag::Parent.append_array(&mut builder, &self.parents); + Tag::Delegate.append(&mut builder, &self.delegate); + Tag::Pointer.append(&mut builder, &self.pointer); + Tag::Metadata.append(&mut builder, &self.metadata); if let Some(body) = &self.body { builder = builder.push_slice(envelope::BODY_TAG); @@ -168,7 +168,7 @@ impl Inscription { Inscription::append_batch_reveal_script_to_builder(inscriptions, builder).into_script() } - fn inscription_id_field(field: &Option>) -> Option { + fn inscription_id_field(field: Option<&[u8]>) -> Option { let value = field.as_ref()?; if value.len() < Txid::LEN { @@ -236,7 +236,7 @@ impl Inscription { } pub(crate) fn delegate(&self) -> Option { - Self::inscription_id_field(&self.delegate) + Self::inscription_id_field(self.delegate.as_deref()) } pub(crate) fn metadata(&self) -> Option { @@ -247,8 +247,12 @@ impl Inscription { str::from_utf8(self.metaprotocol.as_ref()?).ok() } - pub(crate) fn parent(&self) -> Option { - Self::inscription_id_field(&self.parent) + pub(crate) fn parents(&self) -> Vec { + self + .parents + .iter() + .filter_map(|parent| Self::inscription_id_field(Some(parent))) + .collect() } pub(crate) fn pointer(&self) -> Option { @@ -421,31 +425,31 @@ mod tests { #[test] fn inscription_with_no_parent_field_has_no_parent() { assert!(Inscription { - parent: None, + parents: Vec::new(), ..Default::default() } - .parent() - .is_none()); + .parents() + .is_empty()); } #[test] fn inscription_with_parent_field_shorter_than_txid_length_has_no_parent() { assert!(Inscription { - parent: Some(vec![]), + parents: vec![Vec::new()], ..Default::default() } - .parent() - .is_none()); + .parents() + .is_empty()); } #[test] fn inscription_with_parent_field_longer_than_txid_and_index_has_no_parent() { assert!(Inscription { - parent: Some(vec![1; 37]), + parents: vec![vec![1; 37]], ..Default::default() } - .parent() - .is_none()); + .parents() + .is_empty()); } #[test] @@ -454,12 +458,12 @@ mod tests { parent[35] = 0; - assert!(Inscription { - parent: Some(parent), + assert!(!Inscription { + parents: vec![parent], ..Default::default() } - .parent() - .is_some()); + .parents() + .is_empty()); } #[test] @@ -469,11 +473,11 @@ mod tests { parent[34] = 0; assert!(Inscription { - parent: Some(parent), + parents: vec![parent], ..Default::default() } - .parent() - .is_none()); + .parents() + .is_empty()); } #[test] @@ -500,19 +504,19 @@ mod tests { fn inscription_parent_txid_is_deserialized_correctly() { assert_eq!( Inscription { - parent: Some(vec![ + parents: vec![vec![ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, - ]), + ]], ..Default::default() } - .parent() - .unwrap() - .txid, - "1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100" - .parse() - .unwrap() + .parents(), + [ + "1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100i0" + .parse() + .unwrap() + ], ); } @@ -520,13 +524,15 @@ mod tests { fn inscription_parent_with_zero_byte_index_field_is_deserialized_correctly() { assert_eq!( Inscription { - parent: Some(vec![1; 32]), + parents: vec![vec![1; 32]], ..Default::default() } - .parent() - .unwrap() - .index, - 0 + .parents(), + [ + "0101010101010101010101010101010101010101010101010101010101010101i0" + .parse() + .unwrap() + ], ); } @@ -534,17 +540,19 @@ mod tests { fn inscription_parent_with_one_byte_index_field_is_deserialized_correctly() { assert_eq!( Inscription { - parent: Some(vec![ + parents: vec![vec![ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01 - ]), + ]], ..Default::default() } - .parent() - .unwrap() - .index, - 1 + .parents(), + [ + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffi1" + .parse() + .unwrap() + ], ); } @@ -552,17 +560,19 @@ mod tests { fn inscription_parent_with_two_byte_index_field_is_deserialized_correctly() { assert_eq!( Inscription { - parent: Some(vec![ + parents: vec![vec![ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0x02 - ]), + ]], ..Default::default() } - .parent() - .unwrap() - .index, - 0x0201, + .parents(), + [ + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffi513" + .parse() + .unwrap() + ], ); } @@ -570,17 +580,19 @@ mod tests { fn inscription_parent_with_three_byte_index_field_is_deserialized_correctly() { assert_eq!( Inscription { - parent: Some(vec![ + parents: vec![vec![ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0x02, 0x03 - ]), + ]], ..Default::default() } - .parent() - .unwrap() - .index, - 0x030201, + .parents(), + [ + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffi197121" + .parse() + .unwrap() + ], ); } @@ -588,17 +600,49 @@ mod tests { fn inscription_parent_with_four_byte_index_field_is_deserialized_correctly() { assert_eq!( Inscription { - parent: Some(vec![ + parents: vec![vec![ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0x02, 0x03, 0x04, - ]), + ]], ..Default::default() } - .parent() - .unwrap() - .index, - 0x04030201, + .parents(), + [ + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffi67305985" + .parse() + .unwrap() + ], + ); + } + + #[test] + fn inscription_parent_returns_multiple_parents() { + assert_eq!( + Inscription { + parents: vec![ + vec![ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x01, 0x02, 0x03, 0x04, + ], + vec![ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x00, 0x02, 0x03, 0x04, + ] + ], + ..Default::default() + } + .parents(), + [ + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffi67305985" + .parse() + .unwrap(), + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffi67305984" + .parse() + .unwrap() + ], ); } @@ -732,7 +776,7 @@ mod tests { None, None, None, - None, + Vec::new(), file.path(), None, ) @@ -746,7 +790,7 @@ mod tests { None, None, None, - None, + Vec::new(), file.path(), Some(0), ) @@ -760,7 +804,7 @@ mod tests { None, None, None, - None, + Vec::new(), file.path(), Some(1), ) @@ -774,7 +818,7 @@ mod tests { None, None, None, - None, + Vec::new(), file.path(), Some(256), ) diff --git a/src/inscriptions/tag.rs b/src/inscriptions/tag.rs index 6f186c13f2..34602c50cb 100644 --- a/src/inscriptions/tag.rs +++ b/src/inscriptions/tag.rs @@ -19,7 +19,7 @@ pub(crate) enum Tag { } impl Tag { - fn is_chunked(self) -> bool { + fn chunked(self) -> bool { matches!(self, Self::Metadata) } @@ -39,12 +39,12 @@ impl Tag { } } - pub(crate) fn encode(self, builder: &mut script::Builder, value: &Option>) { + pub(crate) fn append(self, builder: &mut script::Builder, value: &Option>) { if let Some(value) = value { let mut tmp = script::Builder::new(); mem::swap(&mut tmp, builder); - if self.is_chunked() { + if self.chunked() { for chunk in value.chunks(MAX_SCRIPT_ELEMENT_SIZE) { tmp = tmp .push_slice::<&script::PushBytes>(self.bytes().try_into().unwrap()) @@ -60,8 +60,21 @@ impl Tag { } } - pub(crate) fn remove_field(self, fields: &mut BTreeMap<&[u8], Vec<&[u8]>>) -> Option> { - if self.is_chunked() { + pub(crate) fn append_array(self, builder: &mut script::Builder, values: &Vec>) { + let mut tmp = script::Builder::new(); + mem::swap(&mut tmp, builder); + + for value in values { + tmp = tmp + .push_slice::<&script::PushBytes>(self.bytes().try_into().unwrap()) + .push_slice::<&script::PushBytes>(value.as_slice().try_into().unwrap()); + } + + mem::swap(&mut tmp, builder); + } + + pub(crate) fn take(self, fields: &mut BTreeMap<&[u8], Vec<&[u8]>>) -> Option> { + if self.chunked() { let value = fields.remove(self.bytes())?; if value.is_empty() { @@ -85,4 +98,13 @@ impl Tag { } } } + + pub(crate) fn take_array(self, fields: &mut BTreeMap<&[u8], Vec<&[u8]>>) -> Vec> { + fields + .remove(self.bytes()) + .unwrap_or_default() + .into_iter() + .map(|v| v.to_vec()) + .collect() + } } diff --git a/src/subcommand/decode.rs b/src/subcommand/decode.rs index f0562a126a..cb8581e469 100644 --- a/src/subcommand/decode.rs +++ b/src/subcommand/decode.rs @@ -26,8 +26,8 @@ pub struct CompactInscription { pub metadata: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub metaprotocol: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub parent: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub parents: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub pointer: Option, #[serde(default, skip_serializing_if = "std::ops::Not::not")] @@ -45,7 +45,7 @@ impl TryFrom for CompactInscription { .transpose()?, content_type: inscription.content_type().map(str::to_string), metaprotocol: inscription.metaprotocol().map(str::to_string), - parent: inscription.parent(), + parents: inscription.parents(), pointer: inscription.pointer(), body: inscription.body.map(hex::encode), duplicate_field: inscription.duplicate_field, diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index a24a2f1d0d..1ac3f2b6ac 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -8,9 +8,10 @@ use { crate::templates::{ BlockHtml, BlocksHtml, ChildrenHtml, ClockSvg, CollectionsHtml, HomeHtml, InputHtml, InscriptionHtml, InscriptionsBlockHtml, InscriptionsHtml, OutputHtml, PageContent, PageHtml, - PreviewAudioHtml, PreviewCodeHtml, PreviewFontHtml, PreviewImageHtml, PreviewMarkdownHtml, - PreviewModelHtml, PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml, PreviewVideoHtml, - RangeHtml, RareTxt, RuneBalancesHtml, RuneHtml, RunesHtml, SatHtml, TransactionHtml, + ParentsHtml, PreviewAudioHtml, PreviewCodeHtml, PreviewFontHtml, PreviewImageHtml, + PreviewMarkdownHtml, PreviewModelHtml, PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml, + PreviewVideoHtml, RangeHtml, RareTxt, RuneBalancesHtml, RuneHtml, RunesHtml, SatHtml, + TransactionHtml, }, axum::{ body, @@ -215,6 +216,11 @@ impl Server { .route("/install.sh", get(Self::install_script)) .route("/ordinal/:sat", get(Self::ordinal)) .route("/output/:output", get(Self::output)) + .route("/parents/:inscription_id", get(Self::parents)) + .route( + "/parents/:inscription_id/:page", + get(Self::parents_paginated), + ) .route("/preview/:inscription_id", get(Self::preview)) .route("/r/blockhash", get(Self::block_hash_json)) .route( @@ -1492,7 +1498,7 @@ impl Server { id: info.entry.id, next: info.next, number: info.entry.inscription_number, - parent: info.parent, + parents: info.parents, previous: info.previous, rune: info.rune, sat: info.entry.sat, @@ -1513,7 +1519,7 @@ impl Server { number: info.entry.inscription_number, next: info.next, output: info.output, - parent: info.parent, + parents: info.parents, previous: info.previous, rune: info.rune, sat: info.entry.sat, @@ -1734,6 +1740,49 @@ impl Server { }) } + async fn parents( + Extension(server_config): Extension>, + Extension(index): Extension>, + Path(inscription_id): Path, + ) -> ServerResult { + Self::parents_paginated( + Extension(server_config), + Extension(index), + Path((inscription_id, 0)), + ) + .await + } + + async fn parents_paginated( + Extension(server_config): Extension>, + Extension(index): Extension>, + Path((id, page)): Path<(InscriptionId, usize)>, + ) -> ServerResult { + task::block_in_place(|| { + let child = index + .get_inscription_entry(id)? + .ok_or_not_found(|| format!("inscription {id}"))?; + + let (parents, more) = index.get_parents_by_sequence_number_paginated(child.parents, page)?; + + let prev_page = page.checked_sub(1); + + let next_page = more.then_some(page + 1); + + Ok( + ParentsHtml { + id, + number: child.inscription_number, + parents, + prev_page, + next_page, + } + .page(server_config) + .into_response(), + ) + }) + } + async fn sat_inscriptions( Extension(index): Extension>, Path(sat): Path, @@ -4543,7 +4592,7 @@ mod tests { Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_id.value()), + parents: vec![parent_id.value()], ..Default::default() } .to_witness(), @@ -4661,7 +4710,7 @@ next Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.value()), + parents: vec![parent_inscription_id.value()], ..Default::default() } .to_witness(), @@ -4678,7 +4727,7 @@ next server.assert_response_regex( format!("/inscription/{inscription_id}"), StatusCode::OK, - format!(".*Inscription 1.*
parent
.*
.**.*"), + format!(".*Inscription 1.*
parents
.*
.**.*"), ); server.assert_response_regex( format!("/inscription/{parent_inscription_id}"), @@ -4689,8 +4738,8 @@ next assert_eq!( server .get_json::(format!("/inscription/{inscription_id}")) - .parent, - Some(parent_inscription_id), + .parents, + vec![parent_inscription_id], ); assert_eq!( @@ -4733,7 +4782,7 @@ next Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.value()), + parents: vec![parent_inscription_id.value()], ..Default::default() } .to_witness(), @@ -4780,7 +4829,7 @@ next Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.value()), + parents: vec![parent_inscription_id.value()], ..Default::default() } .to_witness(), @@ -4792,7 +4841,7 @@ next Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.value()), + parents: vec![parent_inscription_id.value()], ..Default::default() } .to_witness(), @@ -4804,7 +4853,7 @@ next Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.value()), + parents: vec![parent_inscription_id.value()], ..Default::default() } .to_witness(), @@ -4816,7 +4865,7 @@ next Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.value()), + parents: vec![parent_inscription_id.value()], ..Default::default() } .to_witness(), @@ -4828,7 +4877,7 @@ next Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.value()), + parents: vec![parent_inscription_id.value()], ..Default::default() } .to_witness(), @@ -4856,6 +4905,136 @@ next ); } + #[test] + fn inscription_with_parent_page() { + let server = TestServer::builder().chain(Chain::Regtest).build(); + server.mine_blocks(2); + + let parent_a_txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], + ..Default::default() + }); + + let parent_b_txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0, inscription("text/plain", "hello").to_witness())], + ..Default::default() + }); + + server.mine_blocks(1); + + let parent_a_inscription_id = InscriptionId { + txid: parent_a_txid, + index: 0, + }; + + let parent_b_inscription_id = InscriptionId { + txid: parent_b_txid, + index: 0, + }; + + let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[ + ( + 3, + 0, + 0, + Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + parents: vec![ + parent_a_inscription_id.value(), + parent_b_inscription_id.value(), + ], + ..Default::default() + } + .to_witness(), + ), + (3, 1, 0, Default::default()), + (3, 2, 0, Default::default()), + ], + ..Default::default() + }); + + server.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + + server.assert_response_regex( + format!("/parents/{inscription_id}"), + StatusCode::OK, + format!(".*Inscription -1 Parents.*

Inscription -1 Parents

.*
.*.*"), + ); + } + + #[test] + fn inscription_parent_page_pagination() { + let server = TestServer::builder().chain(Chain::Regtest).build(); + + server.mine_blocks(1); + + let mut parent_ids = Vec::new(); + let mut inputs = Vec::new(); + for i in 0..101 { + parent_ids.push( + InscriptionId { + txid: server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(i + 1, 0, 0, inscription("text/plain", "hello").to_witness())], + ..Default::default() + }), + index: 0, + } + .value(), + ); + + inputs.push((i + 2, 1, 0, Witness::default())); + + server.mine_blocks(1); + } + + inputs.insert( + 0, + ( + 101, + 0, + 0, + Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + parents: parent_ids, + ..Default::default() + } + .to_witness(), + ), + ); + + let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &inputs, + ..Default::default() + }); + + server.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + + server.assert_response_regex( + format!("/parents/{inscription_id}"), + StatusCode::OK, + format!(".*Inscription -1 Parents.*

Inscription -1 Parents

.*
(.*.*){{100}}.*"), + ); + + server.assert_response_regex( + format!("/parents/{inscription_id}/1"), + StatusCode::OK, + format!(".*Inscription -1 Parents.*

Inscription -1 Parents

.*
(.*.*){{1}}.*"), + ); + + server.assert_response_regex( + format!("/inscription/{inscription_id}"), + StatusCode::OK, + ".*Inscription -1.*

Inscription -1

.*
(.*.*){4}.*", + ); + } + #[test] fn inscription_number_endpoint() { let server = TestServer::builder().chain(Chain::Regtest).build(); @@ -5377,7 +5556,7 @@ next assert_eq!( server.get_json::("/r/sat/5000000000"), api::SatInscriptions { - ids: vec![], + ids: Vec::new(), page: 0, more: false } @@ -5499,7 +5678,7 @@ next builder = Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.value()), + parents: vec![parent_inscription_id.value()], unrecognized_even_field: false, ..Default::default() } diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 6940603cb8..c8066de812 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -109,7 +109,7 @@ impl Inscribe { self.delegate, metadata, self.metaprotocol, - self.parent, + self.parent.into_iter().collect(), file, None, )?]; @@ -478,7 +478,7 @@ mod tests { inscriptions.insert(parent_info.location, vec![parent_inscription]); let child_inscription = InscriptionTemplate { - parent: Some(parent_inscription), + parents: vec![parent_inscription], ..Default::default() } .into(); @@ -820,17 +820,17 @@ inscriptions: let inscriptions = vec![ InscriptionTemplate { - parent: Some(parent), + parents: vec![parent], ..Default::default() } .into(), InscriptionTemplate { - parent: Some(parent), + parents: vec![parent], ..Default::default() } .into(), InscriptionTemplate { - parent: Some(parent), + parents: vec![parent], ..Default::default() } .into(), @@ -928,17 +928,17 @@ inscriptions: let inscriptions = vec![ InscriptionTemplate { - parent: Some(parent), + parents: vec![parent], pointer: Some(10_000), } .into(), InscriptionTemplate { - parent: Some(parent), + parents: vec![parent], pointer: Some(11_111), } .into(), InscriptionTemplate { - parent: Some(parent), + parents: vec![parent], pointer: Some(13_3333), } .into(), @@ -1044,17 +1044,17 @@ inscriptions: let inscriptions = vec![ InscriptionTemplate { - parent: Some(parent), + parents: vec![parent], ..Default::default() } .into(), InscriptionTemplate { - parent: Some(parent), + parents: vec![parent], ..Default::default() } .into(), InscriptionTemplate { - parent: Some(parent), + parents: vec![parent], ..Default::default() } .into(), @@ -1120,17 +1120,17 @@ inscriptions: let inscriptions = vec![ InscriptionTemplate { - parent: Some(parent), + parents: vec![parent], ..Default::default() } .into(), InscriptionTemplate { - parent: Some(parent), + parents: vec![parent], ..Default::default() } .into(), InscriptionTemplate { - parent: Some(parent), + parents: vec![parent], ..Default::default() } .into(), @@ -1290,17 +1290,17 @@ inscriptions: let inscriptions = vec![ InscriptionTemplate { - parent: Some(parent), + parents: vec![parent], ..Default::default() } .into(), InscriptionTemplate { - parent: Some(parent), + parents: vec![parent], ..Default::default() } .into(), InscriptionTemplate { - parent: Some(parent), + parents: vec![parent], ..Default::default() } .into(), @@ -1334,18 +1334,16 @@ inscriptions: .unwrap(); assert_eq!( - parent, + vec![parent], ParsedEnvelope::from_transaction(&reveal_tx)[0] .payload - .parent() - .unwrap() + .parents(), ); assert_eq!( - parent, + vec![parent], ParsedEnvelope::from_transaction(&reveal_tx)[1] .payload - .parent() - .unwrap() + .parents(), ); let sig_vbytes = 17; diff --git a/src/templates.rs b/src/templates.rs index 68da58efef..897bd5e9c1 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -14,6 +14,7 @@ pub(crate) use { inscriptions_block::InscriptionsBlockHtml, metadata::MetadataHtml, output::OutputHtml, + parents::ParentsHtml, preview::{ PreviewAudioHtml, PreviewCodeHtml, PreviewFontHtml, PreviewImageHtml, PreviewMarkdownHtml, PreviewModelHtml, PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml, PreviewVideoHtml, @@ -42,6 +43,7 @@ pub mod inscriptions; mod inscriptions_block; mod metadata; pub mod output; +mod parents; mod preview; mod range; mod rare; diff --git a/src/templates/inscription.rs b/src/templates/inscription.rs index 1a3984475f..d0def05caf 100644 --- a/src/templates/inscription.rs +++ b/src/templates/inscription.rs @@ -12,7 +12,7 @@ pub(crate) struct InscriptionHtml { pub(crate) number: i32, pub(crate) next: Option, pub(crate) output: Option, - pub(crate) parent: Option, + pub(crate) parents: Vec, pub(crate) previous: Option, pub(crate) rune: Option, pub(crate) sat: Option, @@ -209,7 +209,7 @@ mod tests { fn with_parent() { assert_regex_match!( InscriptionHtml { - parent: Some(inscription_id(2)), + parents: vec![inscription_id(2)], fee: 1, inscription: inscription("text/plain;charset=utf-8", "HELLOWORLD"), id: inscription_id(1), @@ -225,14 +225,17 @@ mod tests {
-
id
-
1{64}i1
-
parent
+
parents
+
+ all +
+
id
+
1{64}i1
preview
link
content
@@ -258,7 +261,7 @@ mod tests {
ethereum teleburn address
0xa1DfBd1C519B9323FD7Fd8e498Ac16c2E502F059
- " +" .unindent() ); } diff --git a/src/templates/parents.rs b/src/templates/parents.rs new file mode 100644 index 0000000000..e142c126d4 --- /dev/null +++ b/src/templates/parents.rs @@ -0,0 +1,71 @@ +use super::*; + +#[derive(Boilerplate)] +pub(crate) struct ParentsHtml { + pub(crate) id: InscriptionId, + pub(crate) number: i32, + pub(crate) parents: Vec, + pub(crate) prev_page: Option, + pub(crate) next_page: Option, +} + +impl PageContent for ParentsHtml { + fn title(&self) -> String { + format!("Inscription {} Parents", self.number) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn without_prev_and_next() { + assert_regex_match!( + ParentsHtml { + id: inscription_id(1), + number: 0, + parents: vec![inscription_id(2), inscription_id(3)], + prev_page: None, + next_page: None, + }, + " +

Inscription 0 Parents

+
+ + +
+ .* + prev + next + .* + " + .unindent() + ); + } + + #[test] + fn with_prev_and_next() { + assert_regex_match!( + ParentsHtml { + id: inscription_id(1), + number: 0, + parents: vec![inscription_id(2), inscription_id(3)], + next_page: Some(3), + prev_page: Some(1), + }, + " +

Inscription 0 Parents

+
+ + +
+ .* + + + .* + " + .unindent() + ); + } +} diff --git a/src/test.rs b/src/test.rs index 5e90c2c453..98a96e80f4 100644 --- a/src/test.rs +++ b/src/test.rs @@ -103,14 +103,14 @@ pub(crate) fn tx_out(value: u64, address: Address) -> TxOut { #[derive(Default, Debug)] pub(crate) struct InscriptionTemplate { - pub(crate) parent: Option, + pub(crate) parents: Vec, pub(crate) pointer: Option, } impl From for Inscription { fn from(template: InscriptionTemplate) -> Self { Self { - parent: template.parent.map(|id| id.value()), + parents: template.parents.into_iter().map(|id| id.value()).collect(), pointer: template.pointer.map(Inscription::pointer_value), ..Default::default() } diff --git a/src/wallet/inscribe/batch.rs b/src/wallet/inscribe/batch.rs index 1c27df1669..6221ac4d40 100644 --- a/src/wallet/inscribe/batch.rs +++ b/src/wallet/inscribe/batch.rs @@ -220,10 +220,9 @@ impl Batch { change: [Address; 2], ) -> Result<(Transaction, Transaction, TweakedKeyPair, u64)> { if let Some(parent_info) = &self.parent_info { - assert!(self - .inscriptions - .iter() - .all(|inscription| inscription.parent().unwrap() == parent_info.id)) + for inscription in &self.inscriptions { + assert_eq!(inscription.parents(), vec![parent_info.id]); + } } match self.mode { diff --git a/src/wallet/inscribe/batch_file.rs b/src/wallet/inscribe/batch_file.rs index d7472f6417..7cfbe4b841 100644 --- a/src/wallet/inscribe/batch_file.rs +++ b/src/wallet/inscribe/batch_file.rs @@ -134,7 +134,7 @@ impl Batchfile { entry.delegate, entry.metadata()?, entry.metaprotocol.clone(), - self.parent, + self.parent.into_iter().collect(), &entry.file, Some(pointer), )?); diff --git a/templates/inscription.html b/templates/inscription.html index 005940ae81..2e54991985 100644 --- a/templates/inscription.html +++ b/templates/inscription.html @@ -13,6 +13,19 @@

Inscription {{ self.number }}

%% }
+%% if !&self.parents.is_empty() { +
parents
+
+
+%% for parent in &self.parents { + {{Iframe::thumbnail(*parent)}} +%% } +
+
+ all +
+
+%% } %% if !self.children.is_empty() {
children
@@ -28,14 +41,6 @@

Inscription {{ self.number }}

%% }
id
{{ self.id }}
-%% if let Some(parent) = &self.parent { -
parent
-
-
- {{Iframe::thumbnail(*parent)}} -
-
-%% } %% if self.charms != 0 {
charms
diff --git a/templates/parents.html b/templates/parents.html new file mode 100644 index 0000000000..9612de657a --- /dev/null +++ b/templates/parents.html @@ -0,0 +1,22 @@ +

Inscription {{ self.number }} Parents

+%% if self.parents.is_empty() { +

No parents

+%% } else { +
+%% for id in &self.parents { + {{ Iframe::thumbnail(*id) }} +%% } +
+
+%% if let Some(prev_page) = &self.prev_page { + +%% } else { +prev +%% } +%% if let Some(next_page) = &self.next_page { + +%% } else { +next +%% } +
+%% } diff --git a/tests/decode.rs b/tests/decode.rs index d27d855d8b..b3500a47f3 100644 --- a/tests/decode.rs +++ b/tests/decode.rs @@ -136,7 +136,7 @@ fn compact() { incomplete_field: false, metadata: None, metaprotocol: None, - parent: None, + parents: Vec::new(), pointer: None, unrecognized_even_field: false, }], diff --git a/tests/json_api.rs b/tests/json_api.rs index 612f18481e..b41188dfab 100644 --- a/tests/json_api.rs +++ b/tests/json_api.rs @@ -30,7 +30,7 @@ fn get_sat_without_sat_index() { percentile: "100%".into(), satpoint: None, timestamp: 0, - inscriptions: vec![], + inscriptions: Vec::new(), } ) } @@ -163,7 +163,7 @@ fn get_inscription() { number: 0, next: None, value: Some(10000), - parent: None, + parents: Vec::new(), previous: None, rune: None, sat: Some(Sat(50 * COIN_VALUE)), @@ -389,7 +389,7 @@ fn get_block() { .unwrap(), best_height: 1, height: 0, - inscriptions: vec![], + inscriptions: Vec::new(), } ); } diff --git a/tests/wallet/inscribe.rs b/tests/wallet/inscribe.rs index a6e864f862..e0efd95879 100644 --- a/tests/wallet/inscribe.rs +++ b/tests/wallet/inscribe.rs @@ -657,7 +657,7 @@ fn inscribe_with_parent_inscription_and_fee_rate() { ord_rpc_server.assert_response_regex( format!("/inscription/{}", child_output.inscriptions[0].id), format!( - ".*
parent
.*.*", + ".*
parents
.*
.*", child_output.parent.unwrap() ), ); @@ -1050,12 +1050,12 @@ fn batch_inscribe_with_multiple_inscriptions_with_parent() { ord_rpc_server.assert_response_regex( format!("/inscription/{}", output.inscriptions[0].id), - r".*
parent
\s*
.*
.*", + r".*
parents
\s*
.*
.*", ); ord_rpc_server.assert_response_regex( format!("/inscription/{}", output.inscriptions[1].id), - r".*
parent
\s*
.*
.*", + r".*
parents
\s*
.*
.*", ); let request = ord_rpc_server.request(format!("/content/{}", output.inscriptions[2].id)); @@ -1282,7 +1282,7 @@ fn batch_in_separate_outputs_with_parent() { ord_rpc_server.assert_response_regex( format!("/inscription/{}", output.inscriptions[0].id), format!( - r".*
parent
\s*
.*{parent_id}.*
.*
value
.*
10000
.*.*
location
.*
{}:0
.*", + r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
10000
.*.*
location
.*
{}:0
.*", output_1 ), ); @@ -1290,7 +1290,7 @@ fn batch_in_separate_outputs_with_parent() { ord_rpc_server.assert_response_regex( format!("/inscription/{}", output.inscriptions[1].id), format!( - r".*
parent
\s*
.*{parent_id}.*
.*
value
.*
10000
.*.*
location
.*
{}:0
.*", + r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
10000
.*.*
location
.*
{}:0
.*", output_2 ), ); @@ -1298,7 +1298,7 @@ fn batch_in_separate_outputs_with_parent() { ord_rpc_server.assert_response_regex( format!("/inscription/{}", output.inscriptions[2].id), format!( - r".*
parent
\s*
.*{parent_id}.*
.*
value
.*
10000
.*.*
location
.*
{}:0
.*", + r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
10000
.*.*
location
.*
{}:0
.*", output_3 ), ); @@ -1360,7 +1360,7 @@ fn batch_in_separate_outputs_with_parent_and_non_default_postage() { ord_rpc_server.assert_response_regex( format!("/inscription/{}", output.inscriptions[0].id), format!( - r".*
parent
\s*
.*{parent_id}.*
.*
value
.*
777
.*.*
location
.*
{}:0
.*", + r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
777
.*.*
location
.*
{}:0
.*", output_1 ), ); @@ -1368,7 +1368,7 @@ fn batch_in_separate_outputs_with_parent_and_non_default_postage() { ord_rpc_server.assert_response_regex( format!("/inscription/{}", output.inscriptions[1].id), format!( - r".*
parent
\s*
.*{parent_id}.*
.*
value
.*
777
.*.*
location
.*
{}:0
.*", + r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
777
.*.*
location
.*
{}:0
.*", output_2 ), ); @@ -1376,7 +1376,7 @@ fn batch_in_separate_outputs_with_parent_and_non_default_postage() { ord_rpc_server.assert_response_regex( format!("/inscription/{}", output.inscriptions[2].id), format!( - r".*
parent
\s*
.*{parent_id}.*
.*
value
.*
777
.*.*
location
.*
{}:0
.*", + r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
777
.*.*
location
.*
{}:0
.*", output_3 ), ); @@ -2407,7 +2407,7 @@ inscriptions: ord_rpc_server.assert_response_regex( format!("/inscription/{}", inscription_1.id), - format!(r".*
parent
\s*
.*{parent_id}.*
.*
value
.*
{}
.*
sat
.*
.*{}.*
.*
location
.*
{}
.*", + format!(r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
{}
.*
sat
.*
.*{}.*
.*
location
.*
{}
.*", 50 * COIN_VALUE, sat_1, inscription_1.location, @@ -2416,7 +2416,7 @@ inscriptions: ord_rpc_server.assert_response_regex( format!("/inscription/{}", inscription_2.id), - format!(r".*
parent
\s*
.*{parent_id}.*
.*
value
.*
{}
.*
sat
.*
.*{}.*
.*
location
.*
{}
.*", + format!(r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
{}
.*
sat
.*
.*{}.*
.*
location
.*
{}
.*", 50 * COIN_VALUE, sat_2, inscription_2.location @@ -2425,7 +2425,7 @@ inscriptions: ord_rpc_server.assert_response_regex( format!("/inscription/{}", inscription_3.id), - format!(r".*
parent
\s*
.*{parent_id}.*
.*
value
.*
{}
.*
sat
.*
.*{}.*
.*
location
.*
{}
.*", + format!(r".*
parents
\s*
.*{parent_id}.*
.*
value
.*
{}
.*
sat
.*
.*{}.*
.*
location
.*
{}
.*", 50 * COIN_VALUE, sat_3, inscription_3.location