From 8e8449bcc55af275b79fff00077d4841528aa233 Mon Sep 17 00:00:00 2001 From: Wizard of Ord <134870335+devords@users.noreply.github.com> Date: Thu, 2 Nov 2023 17:48:15 -0500 Subject: [PATCH] Add `/children` with pagination (#2617) --- src/index.rs | 30 ++++ src/subcommand/server.rs | 224 ++++++++++++++++++++++++++-- src/templates.rs | 2 + src/templates/children.rs | 71 +++++++++ src/templates/inscription.rs | 66 +++++++- src/templates/inscriptions_block.rs | 4 +- static/index.css | 7 +- templates/children.html | 22 +++ templates/inscription.html | 23 +-- templates/inscriptions-block.html | 2 +- 10 files changed, 424 insertions(+), 27 deletions(-) create mode 100644 src/templates/children.rs create mode 100644 templates/children.html diff --git a/src/index.rs b/src/index.rs index 8657b89edf..b21109ac4b 100644 --- a/src/index.rs +++ b/src/index.rs @@ -778,6 +778,7 @@ impl Index { self.client.get_block(&hash).into_option() } + #[cfg(test)] pub(crate) fn get_children_by_inscription_id( &self, inscription_id: InscriptionId, @@ -795,6 +796,35 @@ impl Index { .collect() } + pub(crate) fn get_children_by_inscription_id_paginated( + &self, + inscription_id: InscriptionId, + page_size: usize, + page_index: usize, + ) -> Result<(Vec, bool)> { + let mut children = self + .database + .begin_read()? + .open_multimap_table(INSCRIPTION_ID_TO_CHILDREN)? + .get(&inscription_id.store())? + .skip(page_index * page_size) + .take(page_size + 1) + .map(|result| { + result + .map(|inscription_id| InscriptionId::load(*inscription_id.value())) + .map_err(|err| err.into()) + }) + .collect::>>()?; + + let more = children.len() > page_size; + + if more { + children.pop(); + } + + Ok((children, more)) + } + pub(crate) fn get_etching(&self, txid: Txid) -> Result> { Ok( self diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 5a32d63216..81d0b96fb0 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -9,9 +9,9 @@ use { page_config::PageConfig, runes::Rune, templates::{ - BlockHtml, BlockJson, ClockSvg, HomeHtml, InputHtml, InscriptionHtml, InscriptionJson, - InscriptionsBlockHtml, InscriptionsHtml, InscriptionsJson, OutputHtml, OutputJson, - PageContent, PageHtml, PreviewAudioHtml, PreviewCodeHtml, PreviewImageHtml, + BlockHtml, BlockJson, ChildrenHtml, ClockSvg, HomeHtml, InputHtml, InscriptionHtml, + InscriptionJson, InscriptionsBlockHtml, InscriptionsHtml, InscriptionsJson, OutputHtml, + OutputJson, PageContent, PageHtml, PreviewAudioHtml, PreviewCodeHtml, PreviewImageHtml, PreviewMarkdownHtml, PreviewModelHtml, PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml, PreviewVideoHtml, RangeHtml, RareTxt, RuneHtml, RunesHtml, SatHtml, SatJson, TransactionHtml, }, @@ -199,6 +199,11 @@ impl Server { .route("/feed.xml", get(Self::feed)) .route("/input/:block/:transaction/:input", get(Self::input)) .route("/inscription/:inscription_query", get(Self::inscription)) + .route("/children/:inscription_id", get(Self::children)) + .route( + "/children/:inscription_id/:page", + get(Self::children_paginated), + ) .route("/inscriptions", get(Self::inscriptions)) .route("/inscriptions/:from", get(Self::inscriptions_from)) .route("/inscriptions/:from/:n", get(Self::inscriptions_from_n)) @@ -207,7 +212,7 @@ impl Server { get(Self::inscriptions_in_block), ) .route( - "/inscriptions/block/:height/:page_index", + "/inscriptions/block/:height/:page", get(Self::inscriptions_in_block_from_page), ) .route("/install.sh", get(Self::install_script)) @@ -1140,7 +1145,8 @@ impl Server { let next = index.get_inscription_id_by_sequence_number(entry.sequence_number + 1)?; - let children = index.get_children_by_inscription_id(inscription_id)?; + let (children, _more_children) = + index.get_children_by_inscription_id_paginated(inscription_id, 4, 0)?; let rune = index.get_rune_by_inscription_id(inscription_id)?; @@ -1166,26 +1172,69 @@ impl Server { } else { InscriptionHtml { chain: page_config.chain, + children, genesis_fee: entry.fee, genesis_height: entry.height, - children, inscription, inscription_id, - next, inscription_number: entry.inscription_number, + next, output, parent: entry.parent, previous, + rune, sat: entry.sat, satpoint, timestamp: timestamp(entry.timestamp), - rune, } .page(page_config) .into_response() }) } + async fn children( + Extension(page_config): Extension>, + Extension(index): Extension>, + Path(inscription_id): Path, + ) -> ServerResult { + Self::children_paginated( + Extension(page_config), + Extension(index), + Path((inscription_id, 0)), + ) + .await + } + + async fn children_paginated( + Extension(page_config): Extension>, + Extension(index): Extension>, + Path((parent, page)): Path<(InscriptionId, usize)>, + ) -> ServerResult { + let parent_number = index + .get_inscription_entry(parent)? + .ok_or_not_found(|| format!("inscription {parent}"))? + .inscription_number; + + let (children, more_children) = + index.get_children_by_inscription_id_paginated(parent, 100, page)?; + + let prev_page = page.checked_sub(1); + + let next_page = more_children.then_some(page + 1); + + Ok( + ChildrenHtml { + parent, + parent_number, + children, + prev_page, + next_page, + } + .page(page_config) + .into_response(), + ) + } + async fn inscriptions( Extension(page_config): Extension>, Extension(index): Extension>, @@ -1212,7 +1261,7 @@ impl Server { async fn inscriptions_in_block_from_page( Extension(page_config): Extension>, Extension(index): Extension>, - Path((block_height, page_index)): Path<(u64, usize)>, + Path((block_height, page)): Path<(u64, usize)>, accept_json: AcceptJson, ) -> ServerResult { let inscriptions = index.get_inscriptions_in_block(block_height)?; @@ -1224,7 +1273,7 @@ impl Server { block_height, index.block_height()?.unwrap_or(Height(0)).n(), inscriptions, - page_index, + page, )? .page(page_config) .into_response() @@ -3238,6 +3287,161 @@ mod tests { ); } + #[test] + fn inscription_with_and_without_children_page() { + let server = TestServer::new_with_regtest(); + server.mine_blocks(1); + + let parent_txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], + ..Default::default() + }); + + server.mine_blocks(1); + + let parent_inscription_id = InscriptionId { + txid: parent_txid, + index: 0, + }; + + server.assert_response_regex( + format!("/children/{parent_inscription_id}"), + StatusCode::OK, + ".*

No children

.*", + ); + + let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[ + ( + 2, + 0, + 0, + Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + parent: Some(parent_inscription_id.parent_value()), + ..Default::default() + } + .to_witness(), + ), + (2, 1, 0, Default::default()), + ], + ..Default::default() + }); + + server.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + + server.assert_response_regex( + format!("/children/{parent_inscription_id}"), + StatusCode::OK, + format!(".*Inscription 0 Children.*

Inscription 0 Children

.*
.*.*"), + ); + } + + #[test] + fn inscriptions_page_shows_max_four_children() { + let server = TestServer::new_with_regtest(); + server.mine_blocks(1); + + let parent_txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], + ..Default::default() + }); + + server.mine_blocks(6); + + let parent_inscription_id = InscriptionId { + txid: parent_txid, + index: 0, + }; + + let _txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[ + ( + 2, + 0, + 0, + Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + parent: Some(parent_inscription_id.parent_value()), + ..Default::default() + } + .to_witness(), + ), + ( + 3, + 0, + 0, + Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + parent: Some(parent_inscription_id.parent_value()), + ..Default::default() + } + .to_witness(), + ), + ( + 4, + 0, + 0, + Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + parent: Some(parent_inscription_id.parent_value()), + ..Default::default() + } + .to_witness(), + ), + ( + 5, + 0, + 0, + Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + parent: Some(parent_inscription_id.parent_value()), + ..Default::default() + } + .to_witness(), + ), + ( + 6, + 0, + 0, + Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + parent: Some(parent_inscription_id.parent_value()), + ..Default::default() + } + .to_witness(), + ), + (2, 1, 0, Default::default()), + ], + ..Default::default() + }); + + server.mine_blocks(1); + + server.assert_response_regex( + format!("/inscription/{parent_inscription_id}"), + StatusCode::OK, + format!( + ".*Inscription 0.* +.*.* +.*.* +.*.* +.*.* +
+ all +
.*" + ), + ); + } + #[test] fn runes_are_displayed_on_runes_page() { let server = TestServer::new_with_regtest_with_index_runes(); diff --git a/src/templates.rs b/src/templates.rs index d170d28bcf..582e2a07c9 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -2,6 +2,7 @@ use {super::*, boilerplate::Boilerplate}; pub(crate) use { block::{BlockHtml, BlockJson}, + children::ChildrenHtml, clock::ClockSvg, home::HomeHtml, iframe::Iframe, @@ -25,6 +26,7 @@ pub(crate) use { }; pub mod block; +mod children; mod clock; mod home; mod iframe; diff --git a/src/templates/children.rs b/src/templates/children.rs new file mode 100644 index 0000000000..ce744a489f --- /dev/null +++ b/src/templates/children.rs @@ -0,0 +1,71 @@ +use super::*; + +#[derive(Boilerplate)] +pub(crate) struct ChildrenHtml { + pub(crate) parent: InscriptionId, + pub(crate) parent_number: i64, + pub(crate) children: Vec, + pub(crate) prev_page: Option, + pub(crate) next_page: Option, +} + +impl PageContent for ChildrenHtml { + fn title(&self) -> String { + format!("Inscription {} Children", self.parent_number) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn without_prev_and_next() { + assert_regex_match!( + ChildrenHtml { + parent: inscription_id(1), + parent_number: 0, + children: vec![inscription_id(2), inscription_id(3)], + prev_page: None, + next_page: None, + }, + " +

Inscription 0 Children

+
+ + +
+ .* + prev + next + .* + " + .unindent() + ); + } + + #[test] + fn with_prev_and_next() { + assert_regex_match!( + ChildrenHtml { + parent: inscription_id(1), + parent_number: 0, + children: vec![inscription_id(2), inscription_id(3)], + next_page: Some(3), + prev_page: Some(1), + }, + " +

Inscription 0 Children

+
+ + +
+ .* + + + .* + " + .unindent() + ); + } +} diff --git a/src/templates/inscription.rs b/src/templates/inscription.rs index 5c16f4244a..ce5e4f9011 100644 --- a/src/templates/inscription.rs +++ b/src/templates/inscription.rs @@ -340,6 +340,16 @@ mod tests {
+
children
+
+
+ + +
+
+ all +
+
id
1{64}i1
preview
@@ -366,13 +376,67 @@ mod tests {
0
ethereum teleburn address
0xa1DfBd1C519B9323FD7Fd8e498Ac16c2E502F059
+
+ " + .unindent() + ); + } + + #[test] + fn with_paginated_children() { + assert_regex_match!( + InscriptionHtml { + children: vec![inscription_id(2)], + genesis_fee: 1, + inscription: inscription("text/plain;charset=utf-8", "HELLOWORLD"), + inscription_id: inscription_id(1), + inscription_number: 1, + satpoint: satpoint(1, 0), + ..Default::default() + }, + " +

Inscription 1

+
+
+ +
+
+
children
- +
+
+ all
+
id
+
1{64}i1
+
preview
+
link
+
content
+
link
+
content length
+
10 bytes
+
content type
+
text/plain;charset=utf-8
+
timestamp
+
+
genesis height
+
0
+
genesis fee
+
1
+
genesis transaction
+
1{64}
+
location
+
1{64}:1:0
+
output
+
1{64}:1
+
offset
+
0
+
ethereum teleburn address
+
0xa1DfBd1C519B9323FD7Fd8e498Ac16c2E502F059
" .unindent() diff --git a/src/templates/inscriptions_block.rs b/src/templates/inscriptions_block.rs index b5d7907088..e32d7592c6 100644 --- a/src/templates/inscriptions_block.rs +++ b/src/templates/inscriptions_block.rs @@ -72,7 +72,7 @@ mod tests { next_page: None, }, " -

Inscriptions in Block 21

+

Inscriptions in Block 21

@@ -98,7 +98,7 @@ mod tests { prev_page: Some(1), }, " -

Inscriptions in Block 21

+

Inscriptions in Block 21

diff --git a/static/index.css b/static/index.css index 7a7a1dd4fd..ff4056ba43 100644 --- a/static/index.css +++ b/static/index.css @@ -34,6 +34,10 @@ h1 { width: 100%; } +h1 a { + color: var(--light-fg); +} + iframe { aspect-ratio: 1 / 1; border: none; @@ -124,9 +128,6 @@ ol, ul { margin: 1%; } -.light-fg a { - color: var(--light-fg); -} .center { text-align: center; } diff --git a/templates/children.html b/templates/children.html new file mode 100644 index 0000000000..e82c98e140 --- /dev/null +++ b/templates/children.html @@ -0,0 +1,22 @@ +

Inscription {{ self.parent_number }} Children

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

No children

+%% } else { +
+%% for id in &self.children { + {{ 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/templates/inscription.html b/templates/inscription.html index cf1d23df5a..b543d4f8e6 100644 --- a/templates/inscription.html +++ b/templates/inscription.html @@ -13,6 +13,19 @@

Inscription {{ self.inscription_number }}

%% }
+%% if !self.children.is_empty() { +
children
+
+
+%% for id in &self.children { + {{Iframe::thumbnail(*id)}} +%% } +
+
+ all +
+
+%% }
id
{{ self.inscription_id }}
%% if let Some(metadata) = self.inscription.metadata() { @@ -69,16 +82,6 @@

Inscription {{ self.inscription_number }}

{{ self.satpoint.offset }}
ethereum teleburn address
{{ teleburn::Ethereum::from(self.inscription_id) }}
-%% if !self.children.is_empty() { -
children
-
-
-%% for id in &self.children { - {{Iframe::thumbnail(*id)}} -%% } -
-
-%% } %% if let Some(rune) = self.rune {
rune
{{ rune }}
diff --git a/templates/inscriptions-block.html b/templates/inscriptions-block.html index 8be648b1e2..cfd5ebfadf 100644 --- a/templates/inscriptions-block.html +++ b/templates/inscriptions-block.html @@ -1,4 +1,4 @@ -

Inscriptions in Block {{ &self.block }}

+

Inscriptions in Block {{ &self.block }}

%% for id in &self.inscriptions { {{ Iframe::thumbnail(*id) }}