From 02fd883941533ffab4275ba7e378aa1c4c66fd76 Mon Sep 17 00:00:00 2001 From: Ordinally Date: Fri, 11 Aug 2023 19:31:02 +0200 Subject: [PATCH 01/13] Move check if JSON API is enabled into AcceptJson. This allows returning StatusCode::NOT_ACCEPTABLE early, and from a single place. Implement JSON endpoint for /inscription --- src/chain.rs | 2 +- src/index.rs | 13 +-- src/inscription.rs | 2 +- src/subcommand/server.rs | 124 ++++++++++++++++++--------- src/subcommand/server/accept_json.rs | 27 ++++-- src/templates.rs | 2 +- src/templates/inscription.rs | 52 +++++++++++ 7 files changed, 165 insertions(+), 57 deletions(-) diff --git a/src/chain.rs b/src/chain.rs index 1f72115bbb..2d100d6dde 100644 --- a/src/chain.rs +++ b/src/chain.rs @@ -2,7 +2,7 @@ use {super::*, clap::ValueEnum}; #[derive(Default, ValueEnum, Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] -pub(crate) enum Chain { +pub enum Chain { #[default] #[clap(alias("main"))] Mainnet, diff --git a/src/index.rs b/src/index.rs index f366f3c899..f493c9f0c1 100644 --- a/src/index.rs +++ b/src/index.rs @@ -877,7 +877,7 @@ impl Index { &self, n: usize, from: Option, - ) -> Result<(Vec, Option, Option)> { + ) -> Result<(Vec, i64, Option, Option)> { let rtx = self.database.begin_read()?; let inscription_number_to_inscription_id = @@ -917,7 +917,7 @@ impl Index { .flat_map(|result| result.map(|(_number, id)| Entry::load(*id.value()))) .collect(); - Ok((inscriptions, prev, next)) + Ok((inscriptions, latest, prev, next)) } pub(crate) fn get_feed_inscriptions(&self, n: usize) -> Result> { @@ -2447,7 +2447,7 @@ mod tests { context.mine_blocks(1); - let (inscriptions, prev, next) = context + let (inscriptions, _, prev, next) = context .index .get_latest_inscriptions_with_prev_and_next(100, None) .unwrap(); @@ -2476,15 +2476,16 @@ mod tests { ids.reverse(); - let (inscriptions, prev, next) = context + let (inscriptions, latest, prev, next) = context .index .get_latest_inscriptions_with_prev_and_next(100, None) .unwrap(); assert_eq!(inscriptions, &ids[..100]); + assert_eq!(latest, 102); assert_eq!(prev, Some(2)); assert_eq!(next, None); - let (inscriptions, prev, next) = context + let (inscriptions, _latest, prev, next) = context .index .get_latest_inscriptions_with_prev_and_next(100, Some(101)) .unwrap(); @@ -2492,7 +2493,7 @@ mod tests { assert_eq!(prev, Some(1)); assert_eq!(next, Some(102)); - let (inscriptions, prev, next) = context + let (inscriptions, _latest, prev, next) = context .index .get_latest_inscriptions_with_prev_and_next(100, Some(0)) .unwrap(); diff --git a/src/inscription.rs b/src/inscription.rs index e647999fc6..5506c5f361 100644 --- a/src/inscription.rs +++ b/src/inscription.rs @@ -23,7 +23,7 @@ pub(crate) enum Curse { } #[derive(Debug, PartialEq, Clone)] -pub(crate) struct Inscription { +pub struct Inscription { body: Option>, content_type: Option>, } diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 8b1b1476b8..98db2196b4 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use { self::{ accept_json::AcceptJson, @@ -7,9 +9,10 @@ use { super::*, crate::page_config::PageConfig, crate::templates::{ - BlockHtml, ClockSvg, HomeHtml, InputHtml, InscriptionHtml, InscriptionsHtml, OutputHtml, - PageContent, PageHtml, PreviewAudioHtml, PreviewImageHtml, PreviewPdfHtml, PreviewTextHtml, - PreviewUnknownHtml, PreviewVideoHtml, RangeHtml, RareTxt, SatHtml, SatJson, TransactionHtml, + inscription::InscriptionJson, BlockHtml, ClockSvg, HomeHtml, InputHtml, InscriptionHtml, + InscriptionsHtml, OutputHtml, PageContent, PageHtml, PreviewAudioHtml, PreviewImageHtml, + PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml, PreviewVideoHtml, RangeHtml, RareTxt, + SatHtml, SatJson, TransactionHtml, }, axum::{ body, @@ -40,6 +43,11 @@ use { mod accept_json; mod error; +#[derive(Clone)] +pub struct ServerState { + pub is_json_api_enabled: bool, +} + enum BlockQuery { Height(u64), Hash(BlockHash), @@ -148,6 +156,10 @@ impl Server { domain: acme_domains.first().cloned(), }); + let server_state = Arc::new(ServerState { + is_json_api_enabled: index.is_json_api_enabled(), + }); + let router = Router::new() .route("/", get(Self::home)) .route("/block/:query", get(Self::block)) @@ -166,6 +178,7 @@ impl Server { .route("/inscription/:inscription_id", get(Self::inscription)) .route("/inscriptions", get(Self::inscriptions)) .route("/inscriptions/:from", get(Self::inscriptions_from)) + .route("/inscriptions/:from/:n", get(Self::inscriptions_from_n)) .route("/install.sh", get(Self::install_script)) .route("/ordinal/:sat", get(Self::ordinal)) .route("/output/:output", get(Self::output)) @@ -194,7 +207,8 @@ impl Server { .allow_methods([http::Method::GET]) .allow_origin(Any), ) - .layer(CompressionLayer::new()); + .layer(CompressionLayer::new()) + .with_state(server_state); match (self.http_port(), self.https_port()) { (Some(http_port), None) => { @@ -390,27 +404,23 @@ impl Server { let blocktime = index.block_time(sat.height())?; let inscriptions = index.get_inscription_ids_by_sat(sat)?; Ok(if accept_json.0 { - if index.is_json_api_enabled() { - Json(SatJson { - number: sat.0, - decimal: sat.decimal().to_string(), - degree: sat.degree().to_string(), - name: sat.name(), - block: sat.height().0, - cycle: sat.cycle(), - epoch: sat.epoch().0, - period: sat.period(), - offset: sat.third(), - rarity: sat.rarity(), - percentile: sat.percentile(), - satpoint, - timestamp: blocktime.timestamp().to_string(), - inscriptions, - }) - .into_response() - } else { - StatusCode::NOT_ACCEPTABLE.into_response() - } + Json(SatJson { + number: sat.0, + decimal: sat.decimal().to_string(), + degree: sat.degree().to_string(), + name: sat.name(), + block: sat.height().0, + cycle: sat.cycle(), + epoch: sat.epoch().0, + period: sat.period(), + offset: sat.third(), + rarity: sat.rarity(), + percentile: sat.percentile(), + satpoint, + timestamp: blocktime.timestamp().to_string(), + inscriptions, + }) + .into_response() } else { SatHtml { sat, @@ -902,7 +912,8 @@ impl Server { Extension(page_config): Extension>, Extension(index): Extension>, Path(inscription_id): Path, - ) -> ServerResult> { + accept_json: AcceptJson, + ) -> ServerResult { let entry = index .get_inscription_entry(inscription_id)? .ok_or_not_found(|| format!("inscription {inscription_id}"))?; @@ -933,7 +944,23 @@ impl Server { let next = index.get_inscription_id_by_inscription_number(entry.number + 1)?; - Ok( + Ok(if accept_json.0 { + axum::Json(InscriptionJson::new( + page_config.chain, + entry.fee, + entry.height, + inscription, + inscription_id, + next, + entry.number, + output, + previous, + entry.sat, + satpoint, + timestamp(entry.timestamp), + )) + .into_response() + } else { InscriptionHtml { chain: page_config.chain, genesis_fee: entry.fee, @@ -948,39 +975,58 @@ impl Server { satpoint, timestamp: timestamp(entry.timestamp), } - .page(page_config, index.has_sat_index()?), - ) + .page(page_config, index.has_sat_index()?) + .into_response() + }) } async fn inscriptions( Extension(page_config): Extension>, Extension(index): Extension>, - ) -> ServerResult> { - Self::inscriptions_inner(page_config, index, None).await + accept_json: AcceptJson, + ) -> ServerResult { + Self::inscriptions_inner(page_config, index, None, 100, accept_json).await } async fn inscriptions_from( Extension(page_config): Extension>, Extension(index): Extension>, Path(from): Path, - ) -> ServerResult> { - Self::inscriptions_inner(page_config, index, Some(from)).await + accept_json: AcceptJson, + ) -> ServerResult { + Self::inscriptions_inner(page_config, index, Some(from), 100, accept_json).await + } + + async fn inscriptions_from_n( + Extension(page_config): Extension>, + Extension(index): Extension>, + Path((from, n)): Path<(i64, usize)>, + accept_json: AcceptJson, + ) -> ServerResult { + Self::inscriptions_inner(page_config, index, Some(from), n, accept_json).await } async fn inscriptions_inner( page_config: Arc, index: Arc, from: Option, - ) -> ServerResult> { - let (inscriptions, prev, next) = index.get_latest_inscriptions_with_prev_and_next(100, from)?; - Ok( + n: usize, + accept_json: AcceptJson, + ) -> ServerResult { + let (inscriptions, highest, prev, next) = + index.get_latest_inscriptions_with_prev_and_next(n, from)?; + Ok(if accept_json.0 { + axum::Json(serde_json::json!({"inscriptions": inscriptions, "next": next, "prev": prev, "highest":highest})) + .into_response() + } else { InscriptionsHtml { inscriptions, next, prev, } - .page(page_config, index.has_sat_index()?), - ) + .page(page_config, index.has_sat_index()?) + .into_response() + }) } async fn redirect_http_to_https( @@ -2034,7 +2080,7 @@ mod tests { fn commits_are_tracked() { let server = TestServer::new(); - thread::sleep(Duration::from_millis(25)); + thread::sleep(Duration::from_millis(100)); assert_eq!(server.index.statistic(crate::index::Statistic::Commits), 3); let info = server.index.info().unwrap(); diff --git a/src/subcommand/server/accept_json.rs b/src/subcommand/server/accept_json.rs index e01edf29c2..c5faacdad2 100644 --- a/src/subcommand/server/accept_json.rs +++ b/src/subcommand/server/accept_json.rs @@ -1,24 +1,33 @@ use super::*; +use axum::extract::FromRef; pub(crate) struct AcceptJson(pub(crate) bool); #[async_trait::async_trait] impl axum::extract::FromRequestParts for AcceptJson where + Arc: FromRef, S: Send + Sync, { - type Rejection = (); + type Rejection = (StatusCode, &'static str); async fn from_request_parts( parts: &mut http::request::Parts, - _state: &S, + state: &S, ) -> Result { - Ok(Self( - parts - .headers - .get("accept") - .map(|value| value == "application/json") - .unwrap_or_default(), - )) + let state = Arc::from_ref(state); + let json_api_enabled = state.is_json_api_enabled; + let json_header = parts + .headers + .get("accept") + .map(|value| value == "application/json") + .unwrap_or_default(); + if json_header && json_api_enabled { + Ok(Self(true)) + } else if json_header && !json_api_enabled { + Err((StatusCode::NOT_ACCEPTABLE, "JSON API disabled")) + } else { + Ok(Self(false)) + } } } diff --git a/src/templates.rs b/src/templates.rs index e4f008f6a8..74886809ba 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -25,7 +25,7 @@ mod clock; mod home; mod iframe; mod input; -mod inscription; +pub mod inscription; mod inscriptions; mod output; mod preview; diff --git a/src/templates/inscription.rs b/src/templates/inscription.rs index c19e5bf4b2..e438b18f30 100644 --- a/src/templates/inscription.rs +++ b/src/templates/inscription.rs @@ -16,6 +16,58 @@ pub(crate) struct InscriptionHtml { pub(crate) timestamp: DateTime, } +#[derive(Debug, PartialEq, Serialize)] +pub struct InscriptionJson { + inscription_id: InscriptionId, + number: i64, + genesis_height: u64, + genesis_fee: u64, + output_value: Option, + address: Option
, + sat: Option, + satpoint: SatPoint, + content_type: Option, + content_length: Option, + timestamp: i64, + previous: Option, + next: Option, +} + +impl InscriptionJson { + pub fn new( + chain: Chain, + genesis_fee: u64, + genesis_height: u64, + inscription: Inscription, + inscription_id: InscriptionId, + next: Option, + number: i64, + output: Option, + previous: Option, + sat: Option, + satpoint: SatPoint, + timestamp: DateTime, + ) -> Self { + Self { + inscription_id, + number, + genesis_height, + 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()), + sat, + satpoint, + content_type: inscription.content_type().map(|s| s.to_string()), + content_length: inscription.content_length(), + timestamp: timestamp.timestamp(), + previous, + next, + } + } +} + impl PageContent for InscriptionHtml { fn title(&self) -> String { format!("Inscription {}", self.number) From f597d69cf14a56619390e7ca96fd66c79001b3ad Mon Sep 17 00:00:00 2001 From: Ordinally Date: Sat, 12 Aug 2023 19:23:41 +0200 Subject: [PATCH 02/13] - Return outpoint for all inscribed sats. - Added corresponding test inscribing on a common sat. - Change timestamp format to actual (i64) timestamp for their universality, compactness, ease of computation and language and platform agnosticism. --- src/subcommand/server.rs | 13 ++++-- src/templates/sat.rs | 2 +- tests/json_api.rs | 92 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 100 insertions(+), 7 deletions(-) diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 98db2196b4..4d8157ee32 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -400,9 +400,16 @@ impl Server { Path(DeserializeFromStr(sat)): Path>, accept_json: AcceptJson, ) -> ServerResult { - let satpoint = index.rare_sat_satpoint(sat)?; - let blocktime = index.block_time(sat.height())?; let inscriptions = index.get_inscription_ids_by_sat(sat)?; + let satpoint = index.rare_sat_satpoint(sat)?.or_else(|| { + inscriptions.first().and_then(|&first_inscription_id| { + index + .get_inscription_satpoint_by_id(first_inscription_id) + .ok() + .flatten() + }) + }); + let blocktime = index.block_time(sat.height())?; Ok(if accept_json.0 { Json(SatJson { number: sat.0, @@ -417,7 +424,7 @@ impl Server { rarity: sat.rarity(), percentile: sat.percentile(), satpoint, - timestamp: blocktime.timestamp().to_string(), + timestamp: blocktime.timestamp().timestamp(), inscriptions, }) .into_response() diff --git a/src/templates/sat.rs b/src/templates/sat.rs index c42b839750..c79c717749 100644 --- a/src/templates/sat.rs +++ b/src/templates/sat.rs @@ -22,7 +22,7 @@ pub struct SatJson { pub rarity: Rarity, pub percentile: String, pub satpoint: Option, - pub timestamp: String, + pub timestamp: i64, pub inscriptions: Vec, } diff --git a/tests/json_api.rs b/tests/json_api.rs index 3cbba92188..5911800a9d 100644 --- a/tests/json_api.rs +++ b/tests/json_api.rs @@ -15,7 +15,7 @@ fn get_sat_without_sat_index() { let mut sat_json: SatJson = serde_json::from_str(&response.text().unwrap()).unwrap(); // this is a hack to ignore the timestamp, since it changes for every request - sat_json.timestamp = "".into(); + sat_json.timestamp = 0; pretty_assert_eq!( sat_json, @@ -32,7 +32,7 @@ fn get_sat_without_sat_index() { rarity: Rarity::Uncommon, percentile: "100%".into(), satpoint: None, - timestamp: "".into(), + timestamp: 0, inscriptions: vec![], } ) @@ -69,7 +69,93 @@ fn get_sat_with_inscription_and_sat_index() { rarity: Rarity::Uncommon, percentile: "0.00023809523835714296%".into(), satpoint: Some(SatPoint::from_str(&format!("{}:{}:{}", reveal, 0, 0)).unwrap()), - timestamp: "1970-01-01 00:00:01 UTC".into(), + timestamp: 1, + inscriptions: vec![inscription_id], + } + ) +} + +#[test] +fn get_inscription() { + let rpc_server = test_bitcoincore_rpc::spawn(); + + create_wallet(&rpc_server); + + let Inscribe { reveal, .. } = inscribe(&rpc_server); + let inscription_id = InscriptionId::from(reveal); + + let response = TestServer::spawn_with_args(&rpc_server, &["--index-sats", "--enable-json-api"]) + .json_request(format!("/sat/{}", 50 * COIN_VALUE)); + + assert_eq!(response.status(), StatusCode::OK); + + let sat_json: SatJson = serde_json::from_str(&response.text().unwrap()).unwrap(); + + pretty_assert_eq!( + sat_json, + SatJson { + number: 50 * COIN_VALUE, + decimal: "1.0".into(), + degree: "0°1′1″0‴".into(), + name: "nvtcsezkbth".into(), + block: 1, + cycle: 0, + epoch: 0, + period: 0, + offset: 0, + rarity: Rarity::Uncommon, + percentile: "0.00023809523835714296%".into(), + satpoint: Some(SatPoint::from_str(&format!("{}:{}:{}", reveal, 0, 0)).unwrap()), + timestamp: 1, + inscriptions: vec![inscription_id], + } + ) +} + +#[test] +fn get_inscription_on_common_sat_and_not_first() { + let rpc_server = test_bitcoincore_rpc::spawn(); + + create_wallet(&rpc_server); + + inscribe(&rpc_server); + + let txid = rpc_server.mine_blocks(1)[0].txdata[0].txid(); + + let Inscribe { reveal, .. } = CommandBuilder::new(format!( + "wallet inscribe --satpoint {} --fee-rate 1 foo.txt", + format!("{}:0:1", txid) + )) + .write("foo.txt", "FOO") + .rpc_server(&rpc_server) + .run_and_check_output(); + + rpc_server.mine_blocks(1); + let inscription_id = InscriptionId::from(reveal); + + let response = TestServer::spawn_with_args(&rpc_server, &["--index-sats", "--enable-json-api"]) + .json_request(format!("/sat/{}", 3 * 50 * COIN_VALUE + 1)); + + assert_eq!(response.status(), StatusCode::OK); + + let sat_json: SatJson = serde_json::from_str(&response.text().unwrap()).unwrap(); + + pretty_assert_eq!( + sat_json, + SatJson { + number: 3 * 50 * COIN_VALUE + 1, + decimal: "3.1".into(), + degree: "0°3′3″1‴".into(), + name: "nvtblvikkiq".into(), + block: 3, + cycle: 0, + epoch: 0, + period: 0, + offset: 1, + rarity: Rarity::Common, + percentile: "0.000714285715119048%".into(), + satpoint: Some(SatPoint::from_str(&format!("{}:{}:{}", reveal, 0, 0)).unwrap()), + timestamp: 3, inscriptions: vec![inscription_id], } ) From 220df0c84445c0f38b85dac60e97fc7b8a03af1e Mon Sep 17 00:00:00 2001 From: Ordinally Date: Sat, 12 Aug 2023 19:31:06 +0200 Subject: [PATCH 03/13] Clippy --- tests/json_api.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/json_api.rs b/tests/json_api.rs index 5911800a9d..1bb4a10c36 100644 --- a/tests/json_api.rs +++ b/tests/json_api.rs @@ -123,8 +123,8 @@ fn get_inscription_on_common_sat_and_not_first() { let txid = rpc_server.mine_blocks(1)[0].txdata[0].txid(); let Inscribe { reveal, .. } = CommandBuilder::new(format!( - "wallet inscribe --satpoint {} --fee-rate 1 foo.txt", - format!("{}:0:1", txid) + "wallet inscribe --satpoint {}:0:1 --fee-rate 1 foo.txt", + txid )) .write("foo.txt", "FOO") .rpc_server(&rpc_server) From 0e4ca132598e30e719cc64f2dc41e906364ff657 Mon Sep 17 00:00:00 2001 From: Ordinally Date: Sat, 12 Aug 2023 22:03:52 +0200 Subject: [PATCH 04/13] - Create `InscriptionsJson` struct for result of `/inscriptions` route. - Add route `/inscriptions/block` to retrieve all inscriptions made in a given block. This makes it easy to retrieve new inscriptions as blocks are mined. --- Cargo.lock | 10 ++++++ Cargo.toml | 1 + src/index.rs | 61 +++++++++++++++++++++++++++-------- src/subcommand/server.rs | 35 ++++++++++++++++++-- src/templates.rs | 4 +-- src/templates/inscriptions.rs | 27 ++++++++++++++++ 6 files changed, 119 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0089ed5dda..30f559314a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1563,6 +1563,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -2024,6 +2033,7 @@ dependencies = [ "http", "hyper", "indicatif", + "itertools", "lazy_static", "log", "mime", diff --git a/Cargo.toml b/Cargo.toml index eb4e6b6498..e852c4aa04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ html-escaper = "0.2.0" http = "0.2.6" hyper = { version = "0.14.24", features = ["http1", "client"] } indicatif = "0.17.1" +itertools = "0.11.0" lazy_static = "1.4.0" log = "0.4.14" mime = "0.3.16" diff --git a/src/index.rs b/src/index.rs index f493c9f0c1..c8b2bff7a4 100644 --- a/src/index.rs +++ b/src/index.rs @@ -13,6 +13,7 @@ use { bitcoincore_rpc::{json::GetBlockHeaderResult, Client}, chrono::SubsecRound, indicatif::{ProgressBar, ProgressStyle}, + itertools::Itertools, log::log_enabled, redb::{ Database, MultimapTable, MultimapTableDefinition, ReadableMultimapTable, ReadableTable, Table, @@ -877,19 +878,23 @@ impl Index { &self, n: usize, from: Option, - ) -> Result<(Vec, i64, Option, Option)> { + ) -> Result<(Vec, Option, Option, i64, i64)> { let rtx = self.database.begin_read()?; let inscription_number_to_inscription_id = rtx.open_table(INSCRIPTION_NUMBER_TO_INSCRIPTION_ID)?; - let latest = match inscription_number_to_inscription_id.iter()?.next_back() { + let highest = match inscription_number_to_inscription_id.iter()?.next_back() { Some(Ok((number, _id))) => number.value(), - Some(Err(_)) => return Ok(Default::default()), - None => return Ok(Default::default()), + Some(Err(_)) | None => return Ok(Default::default()), }; - let from = from.unwrap_or(latest); + let lowest = match inscription_number_to_inscription_id.iter()?.next() { + Some(Ok((number, _id))) => number.value(), + Some(Err(_)) | None => return Ok(Default::default()), + }; + + let from = from.unwrap_or(highest); let prev = if let Some(prev) = from.checked_sub(n.try_into()?) { inscription_number_to_inscription_id @@ -899,12 +904,12 @@ impl Index { None }; - let next = if from < latest { + let next = if from < highest { Some( from .checked_add(n.try_into()?) - .unwrap_or(latest) - .min(latest), + .unwrap_or(highest) + .min(highest), ) } else { None @@ -917,7 +922,34 @@ impl Index { .flat_map(|result| result.map(|(_number, id)| Entry::load(*id.value()))) .collect(); - Ok((inscriptions, latest, prev, next)) + Ok((inscriptions, prev, next, lowest, highest)) + } + + pub(crate) fn get_inscriptions_from_block( + &self, + block_height: u64, + ) -> Result> { + let block_inscriptions = self + .database + .begin_read()? + .open_table(INSCRIPTION_ID_TO_INSCRIPTION_ENTRY)? + .iter()? + .filter_map(|result| match result { + Ok((key, entry_value)) => { + let entry = InscriptionEntry::load(entry_value.value()); + if entry.height == block_height { + Some((InscriptionId::load(*key.value()), entry.number)) + } else { + None + } + } + Err(_) => None, + }) + .sorted_by_key(|&(_id, number)| number) + .map(|(id, _)| id) + .collect(); + + Ok(block_inscriptions) } pub(crate) fn get_feed_inscriptions(&self, n: usize) -> Result> { @@ -2447,7 +2479,7 @@ mod tests { context.mine_blocks(1); - let (inscriptions, _, prev, next) = context + let (inscriptions, prev, next, _, _) = context .index .get_latest_inscriptions_with_prev_and_next(100, None) .unwrap(); @@ -2476,16 +2508,17 @@ mod tests { ids.reverse(); - let (inscriptions, latest, prev, next) = context + let (inscriptions, prev, next, lowest, highest) = context .index .get_latest_inscriptions_with_prev_and_next(100, None) .unwrap(); assert_eq!(inscriptions, &ids[..100]); - assert_eq!(latest, 102); assert_eq!(prev, Some(2)); assert_eq!(next, None); + assert_eq!(highest, 102); + assert_eq!(lowest, 0); - let (inscriptions, _latest, prev, next) = context + let (inscriptions, prev, next, _lowest, _highest) = context .index .get_latest_inscriptions_with_prev_and_next(100, Some(101)) .unwrap(); @@ -2493,7 +2526,7 @@ mod tests { assert_eq!(prev, Some(1)); assert_eq!(next, Some(102)); - let (inscriptions, _latest, prev, next) = context + let (inscriptions, prev, next, _lowest, _highest) = context .index .get_latest_inscriptions_with_prev_and_next(100, Some(0)) .unwrap(); diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 4d8157ee32..9e76662899 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -1,5 +1,7 @@ use std::sync::Arc; +use crate::templates::inscriptions::InscriptionsJson; + use { self::{ accept_json::AcceptJson, @@ -177,6 +179,7 @@ impl Server { .route("/input/:block/:transaction/:input", get(Self::input)) .route("/inscription/:inscription_id", get(Self::inscription)) .route("/inscriptions", get(Self::inscriptions)) + .route("/inscriptions/block/:n", get(Self::inscriptions_block)) .route("/inscriptions/:from", get(Self::inscriptions_from)) .route("/inscriptions/:from/:n", get(Self::inscriptions_from_n)) .route("/install.sh", get(Self::install_script)) @@ -995,6 +998,26 @@ impl Server { Self::inscriptions_inner(page_config, index, None, 100, accept_json).await } + async fn inscriptions_block( + Extension(page_config): Extension>, + Extension(index): Extension>, + Path(block_height): Path, + accept_json: AcceptJson, + ) -> ServerResult { + let inscriptions = index.get_inscriptions_from_block(block_height)?; + Ok(if accept_json.0 { + axum::Json(InscriptionsJson::new(inscriptions, None, None, None, None)).into_response() + } else { + InscriptionsHtml { + inscriptions, + prev: None, + next: None, + } + .page(page_config, index.has_sat_index()?) + .into_response() + }) + } + async fn inscriptions_from( Extension(page_config): Extension>, Extension(index): Extension>, @@ -1020,11 +1043,17 @@ impl Server { n: usize, accept_json: AcceptJson, ) -> ServerResult { - let (inscriptions, highest, prev, next) = + let (inscriptions, prev, next, highest, lowest) = index.get_latest_inscriptions_with_prev_and_next(n, from)?; Ok(if accept_json.0 { - axum::Json(serde_json::json!({"inscriptions": inscriptions, "next": next, "prev": prev, "highest":highest})) - .into_response() + axum::Json(InscriptionsJson::new( + inscriptions, + prev, + next, + Some(lowest), + Some(highest), + )) + .into_response() } else { InscriptionsHtml { inscriptions, diff --git a/src/templates.rs b/src/templates.rs index 74886809ba..d350173263 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -26,8 +26,8 @@ mod home; mod iframe; mod input; pub mod inscription; -mod inscriptions; -mod output; +pub mod inscriptions; +pub mod output; mod preview; mod range; mod rare; diff --git a/src/templates/inscriptions.rs b/src/templates/inscriptions.rs index 8399e8fa59..eb2a0e091f 100644 --- a/src/templates/inscriptions.rs +++ b/src/templates/inscriptions.rs @@ -7,6 +7,33 @@ pub(crate) struct InscriptionsHtml { pub(crate) next: Option, } +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct InscriptionsJson { + pub inscriptions: Vec, + pub prev: Option, + pub next: Option, + pub lowest: Option, + pub highest: Option, +} + +impl InscriptionsJson { + pub fn new( + inscriptions: Vec, + prev: Option, + next: Option, + lowest: Option, + highest: Option, + ) -> Self { + Self { + inscriptions, + prev, + next, + lowest, + highest, + } + } +} + impl PageContent for InscriptionsHtml { fn title(&self) -> String { "Inscriptions".into() From 36c61456cb0dcb0820716a368068f05fe8ebd14e Mon Sep 17 00:00:00 2001 From: Ordinally Date: Sat, 12 Aug 2023 22:22:16 +0200 Subject: [PATCH 05/13] Cleanup --- src/subcommand/server.rs | 12 +++++------- src/subcommand/server/accept_json.rs | 4 ++-- src/templates.rs | 4 ++-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 9e76662899..4b029ce69d 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -1,7 +1,5 @@ use std::sync::Arc; -use crate::templates::inscriptions::InscriptionsJson; - use { self::{ accept_json::AcceptJson, @@ -11,8 +9,8 @@ use { super::*, crate::page_config::PageConfig, crate::templates::{ - inscription::InscriptionJson, BlockHtml, ClockSvg, HomeHtml, InputHtml, InscriptionHtml, - InscriptionsHtml, OutputHtml, PageContent, PageHtml, PreviewAudioHtml, PreviewImageHtml, + BlockHtml, ClockSvg, HomeHtml, InputHtml, InscriptionHtml, InscriptionJson, InscriptionsHtml, + InscriptionsJson, OutputHtml, PageContent, PageHtml, PreviewAudioHtml, PreviewImageHtml, PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml, PreviewVideoHtml, RangeHtml, RareTxt, SatHtml, SatJson, TransactionHtml, }, @@ -46,7 +44,7 @@ mod accept_json; mod error; #[derive(Clone)] -pub struct ServerState { +pub struct ServerConfig { pub is_json_api_enabled: bool, } @@ -158,7 +156,7 @@ impl Server { domain: acme_domains.first().cloned(), }); - let server_state = Arc::new(ServerState { + let server_config = Arc::new(ServerConfig { is_json_api_enabled: index.is_json_api_enabled(), }); @@ -211,7 +209,7 @@ impl Server { .allow_origin(Any), ) .layer(CompressionLayer::new()) - .with_state(server_state); + .with_state(server_config); match (self.http_port(), self.https_port()) { (Some(http_port), None) => { diff --git a/src/subcommand/server/accept_json.rs b/src/subcommand/server/accept_json.rs index c5faacdad2..b528b7df2e 100644 --- a/src/subcommand/server/accept_json.rs +++ b/src/subcommand/server/accept_json.rs @@ -6,7 +6,7 @@ pub(crate) struct AcceptJson(pub(crate) bool); #[async_trait::async_trait] impl axum::extract::FromRequestParts for AcceptJson where - Arc: FromRef, + Arc: FromRef, S: Send + Sync, { type Rejection = (StatusCode, &'static str); @@ -25,7 +25,7 @@ where if json_header && json_api_enabled { Ok(Self(true)) } else if json_header && !json_api_enabled { - Err((StatusCode::NOT_ACCEPTABLE, "JSON API disabled")) + Err((StatusCode::NOT_ACCEPTABLE, "JSON API not enabled")) } else { Ok(Self(false)) } diff --git a/src/templates.rs b/src/templates.rs index d350173263..ae81e12b66 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -6,8 +6,8 @@ pub(crate) use { home::HomeHtml, iframe::Iframe, input::InputHtml, - inscription::InscriptionHtml, - inscriptions::InscriptionsHtml, + inscription::{InscriptionHtml, InscriptionJson}, + inscriptions::{InscriptionsHtml, InscriptionsJson}, output::OutputHtml, page_config::PageConfig, preview::{ From 5b9e222451fac9d56640b7104c93e622d1fbd726 Mon Sep 17 00:00:00 2001 From: Ordinally Date: Sat, 12 Aug 2023 23:04:04 +0200 Subject: [PATCH 06/13] Test for /inscription route. --- src/templates/inscription.rs | 31 +++++++------- tests/json_api.rs | 80 +++++++++++++++++++----------------- 2 files changed, 58 insertions(+), 53 deletions(-) diff --git a/src/templates/inscription.rs b/src/templates/inscription.rs index e438b18f30..8700e82a9c 100644 --- a/src/templates/inscription.rs +++ b/src/templates/inscription.rs @@ -16,21 +16,21 @@ pub(crate) struct InscriptionHtml { pub(crate) timestamp: DateTime, } -#[derive(Debug, PartialEq, Serialize)] +#[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct InscriptionJson { - inscription_id: InscriptionId, - number: i64, - genesis_height: u64, - genesis_fee: u64, - output_value: Option, - address: Option
, - sat: Option, - satpoint: SatPoint, - content_type: Option, - content_length: Option, - timestamp: i64, - previous: Option, - next: Option, + pub inscription_id: InscriptionId, + pub number: i64, + pub genesis_height: u64, + pub genesis_fee: u64, + pub output_value: Option, + pub address: Option, + pub sat: Option, + pub satpoint: SatPoint, + pub content_type: Option, + pub content_length: Option, + pub timestamp: i64, + pub previous: Option, + pub next: Option, } impl InscriptionJson { @@ -56,7 +56,8 @@ impl InscriptionJson { output_value: output.as_ref().map(|o| o.value), address: output .as_ref() - .and_then(|o| chain.address_from_script(&o.script_pubkey).ok()), + .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()), diff --git a/tests/json_api.rs b/tests/json_api.rs index 1bb4a10c36..dc048d008e 100644 --- a/tests/json_api.rs +++ b/tests/json_api.rs @@ -1,3 +1,5 @@ +use ord::templates::inscription::InscriptionJson; + use { super::*, ord::inscription_id::InscriptionId, ord::rarity::Rarity, ord::templates::sat::SatJson, ord::SatPoint, @@ -76,44 +78,7 @@ fn get_sat_with_inscription_and_sat_index() { } #[test] -fn get_inscription() { - let rpc_server = test_bitcoincore_rpc::spawn(); - - create_wallet(&rpc_server); - - let Inscribe { reveal, .. } = inscribe(&rpc_server); - let inscription_id = InscriptionId::from(reveal); - - let response = TestServer::spawn_with_args(&rpc_server, &["--index-sats", "--enable-json-api"]) - .json_request(format!("/sat/{}", 50 * COIN_VALUE)); - - assert_eq!(response.status(), StatusCode::OK); - - let sat_json: SatJson = serde_json::from_str(&response.text().unwrap()).unwrap(); - - pretty_assert_eq!( - sat_json, - SatJson { - number: 50 * COIN_VALUE, - decimal: "1.0".into(), - degree: "0°1′1″0‴".into(), - name: "nvtcsezkbth".into(), - block: 1, - cycle: 0, - epoch: 0, - period: 0, - offset: 0, - rarity: Rarity::Uncommon, - percentile: "0.00023809523835714296%".into(), - satpoint: Some(SatPoint::from_str(&format!("{}:{}:{}", reveal, 0, 0)).unwrap()), - timestamp: 1, - inscriptions: vec![inscription_id], - } - ) -} - -#[test] -fn get_inscription_on_common_sat_and_not_first() { +fn get_sat_with_inscription_on_common_sat_and_more_inscriptions() { let rpc_server = test_bitcoincore_rpc::spawn(); create_wallet(&rpc_server); @@ -161,6 +126,45 @@ fn get_inscription_on_common_sat_and_not_first() { ) } +#[test] +fn get_inscription() { + let rpc_server = test_bitcoincore_rpc::spawn(); + + create_wallet(&rpc_server); + + let Inscribe { reveal, .. } = inscribe(&rpc_server); + let inscription_id = InscriptionId::from(reveal); + + let response = TestServer::spawn_with_args(&rpc_server, &["--index-sats", "--enable-json-api"]) + .json_request(format!("/inscription/{}", inscription_id)); + + assert_eq!(response.status(), StatusCode::OK); + + let mut inscription_json: InscriptionJson = + serde_json::from_str(&response.text().unwrap()).unwrap(); + assert_regex_match!(inscription_json.address.unwrap(), r"bc1p.*"); + inscription_json.address = None; + + pretty_assert_eq!( + inscription_json, + InscriptionJson { + inscription_id, + number: 0, + genesis_height: 2, + genesis_fee: 138, + output_value: Some(10000), + address: None, + sat: Some(ord::Sat(50 * COIN_VALUE)), + satpoint: SatPoint::from_str(&format!("{}:{}:{}", reveal, 0, 0)).unwrap(), + content_type: Some("text/plain;charset=utf-8".to_string()), + content_length: Some(3), + timestamp: 2, + previous: None, + next: None + } + ) +} + #[test] fn json_request_fails_when_not_enabled() { let rpc_server = test_bitcoincore_rpc::spawn(); From 2ed4576e2170abf3b32ce717c0832df3771a200f Mon Sep 17 00:00:00 2001 From: Ordinally Date: Sat, 12 Aug 2023 23:11:09 +0200 Subject: [PATCH 07/13] Bugfix --- src/subcommand/server.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 4b029ce69d..ab899877fc 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -1041,7 +1041,7 @@ impl Server { n: usize, accept_json: AcceptJson, ) -> ServerResult { - let (inscriptions, prev, next, highest, lowest) = + let (inscriptions, prev, next, lowest, highest) = index.get_latest_inscriptions_with_prev_and_next(n, from)?; Ok(if accept_json.0 { axum::Json(InscriptionsJson::new( From fd385fca89c55e83f4d8ed19483e27e3fd6531d5 Mon Sep 17 00:00:00 2001 From: Ordinally Date: Sun, 13 Aug 2023 14:23:34 +0200 Subject: [PATCH 08/13] Add JSON support for `/output` --- src/index.rs | 2 +- src/subcommand/server.rs | 31 +++++++++++++++++++++---------- src/templates.rs | 2 +- src/templates/output.rs | 35 +++++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 12 deletions(-) diff --git a/src/index.rs b/src/index.rs index c8b2bff7a4..4ca50b5023 100644 --- a/src/index.rs +++ b/src/index.rs @@ -58,7 +58,7 @@ define_table! { STATISTIC_TO_COUNT, u64, u64 } define_table! { WRITE_TRANSACTION_STARTING_BLOCK_COUNT_TO_TIMESTAMP, u64, u128 } #[derive(Debug, PartialEq)] -pub(crate) enum List { +pub enum List { Spent, Unspent(Vec<(u64, u64)>), } diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index ab899877fc..921399afed 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -10,9 +10,9 @@ use { crate::page_config::PageConfig, crate::templates::{ BlockHtml, ClockSvg, HomeHtml, InputHtml, InscriptionHtml, InscriptionJson, InscriptionsHtml, - InscriptionsJson, OutputHtml, PageContent, PageHtml, PreviewAudioHtml, PreviewImageHtml, - PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml, PreviewVideoHtml, RangeHtml, RareTxt, - SatHtml, SatJson, TransactionHtml, + InscriptionsJson, OutputHtml, OutputJson, PageContent, PageHtml, PreviewAudioHtml, + PreviewImageHtml, PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml, PreviewVideoHtml, + RangeHtml, RareTxt, SatHtml, SatJson, TransactionHtml, }, axum::{ body, @@ -449,7 +449,8 @@ impl Server { Extension(page_config): Extension>, Extension(index): Extension>, Path(outpoint): Path, - ) -> ServerResult> { + accept_json: AcceptJson, + ) -> ServerResult { let list = if index.has_sat_index()? { index.list(outpoint)? } else { @@ -481,7 +482,16 @@ impl Server { let inscriptions = index.get_inscriptions_on_output(outpoint)?; - Ok( + Ok(if accept_json.0 { + Json(OutputJson::new( + outpoint, + list, + page_config.chain, + output, + inscriptions, + )) + .into_response() + } else { OutputHtml { outpoint, inscriptions, @@ -489,8 +499,9 @@ impl Server { chain: page_config.chain, output, } - .page(page_config, index.has_sat_index()?), - ) + .page(page_config, index.has_sat_index()?) + .into_response() + }) } async fn range( @@ -953,7 +964,7 @@ impl Server { let next = index.get_inscription_id_by_inscription_number(entry.number + 1)?; Ok(if accept_json.0 { - axum::Json(InscriptionJson::new( + Json(InscriptionJson::new( page_config.chain, entry.fee, entry.height, @@ -1004,7 +1015,7 @@ impl Server { ) -> ServerResult { let inscriptions = index.get_inscriptions_from_block(block_height)?; Ok(if accept_json.0 { - axum::Json(InscriptionsJson::new(inscriptions, None, None, None, None)).into_response() + Json(InscriptionsJson::new(inscriptions, None, None, None, None)).into_response() } else { InscriptionsHtml { inscriptions, @@ -1044,7 +1055,7 @@ impl Server { let (inscriptions, prev, next, lowest, highest) = index.get_latest_inscriptions_with_prev_and_next(n, from)?; Ok(if accept_json.0 { - axum::Json(InscriptionsJson::new( + Json(InscriptionsJson::new( inscriptions, prev, next, diff --git a/src/templates.rs b/src/templates.rs index ae81e12b66..259ad50370 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -8,7 +8,7 @@ pub(crate) use { input::InputHtml, inscription::{InscriptionHtml, InscriptionJson}, inscriptions::{InscriptionsHtml, InscriptionsJson}, - output::OutputHtml, + output::{OutputHtml, OutputJson}, page_config::PageConfig, preview::{ PreviewAudioHtml, PreviewImageHtml, PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml, diff --git a/src/templates/output.rs b/src/templates/output.rs index 2172795b18..fac57cc1d7 100644 --- a/src/templates/output.rs +++ b/src/templates/output.rs @@ -9,6 +9,41 @@ pub(crate) struct OutputHtml { pub(crate) inscriptions: Vec, } +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct OutputJson { + pub value: u64, + pub script_pubkey: String, + pub address: Option, + pub transaction: String, + pub sat_ranges: Option>, + pub inscriptions: Vec, +} + +impl OutputJson { + pub fn new( + outpoint: OutPoint, + list: Option, + chain: Chain, + output: TxOut, + inscriptions: Vec, + ) -> Self { + Self { + value: output.value, + script_pubkey: output.script_pubkey.to_asm_string(), + address: chain + .address_from_script(&output.script_pubkey) + .ok() + .map(|address| address.to_string()), + transaction: outpoint.txid.to_string(), + sat_ranges: match list { + Some(List::Unspent(ranges)) => Some(ranges), + _ => None, + }, + inscriptions, + } + } +} + impl PageContent for OutputHtml { fn title(&self) -> String { format!("Output {}", self.outpoint) From 6b8c2f20f2c43d3c957e0968aa49368aeac906cb Mon Sep 17 00:00:00 2001 From: Ordinally Date: Sun, 13 Aug 2023 17:39:00 +0200 Subject: [PATCH 09/13] Adding tests --- src/inscription_id.rs | 4 +- tests/json_api.rs | 101 ++++++++++++++++++++++++++++++++++++++++-- tests/lib.rs | 18 ++++++++ 3 files changed, 117 insertions(+), 6 deletions(-) diff --git a/src/inscription_id.rs b/src/inscription_id.rs index 5ff9a347c4..998faeb3c3 100644 --- a/src/inscription_id.rs +++ b/src/inscription_id.rs @@ -2,8 +2,8 @@ use super::*; #[derive(Debug, PartialEq, Copy, Clone, Hash, Eq)] pub struct InscriptionId { - pub(crate) txid: Txid, - pub(crate) index: u32, + pub txid: Txid, + pub index: u32, } impl<'de> Deserialize<'de> for InscriptionId { diff --git a/tests/json_api.rs b/tests/json_api.rs index dc048d008e..7e716b2029 100644 --- a/tests/json_api.rs +++ b/tests/json_api.rs @@ -1,8 +1,7 @@ -use ord::templates::inscription::InscriptionJson; - use { - super::*, ord::inscription_id::InscriptionId, ord::rarity::Rarity, ord::templates::sat::SatJson, - ord::SatPoint, + super::*, ord::inscription_id::InscriptionId, ord::rarity::Rarity, + ord::templates::inscription::InscriptionJson, ord::templates::inscriptions::InscriptionsJson, + ord::templates::sat::SatJson, ord::SatPoint, test_bitcoincore_rpc::TransactionTemplate, }; #[test] @@ -165,6 +164,100 @@ fn get_inscription() { ) } +fn create_210_inscriptions( + rpc_server: &test_bitcoincore_rpc::Handle, +) -> (Vec, Vec) { + let witness = envelope(&[b"ord", &[1], b"text/plain;charset=utf-8", &[], b"bar"]); + + let mut blessed_inscriptions = Vec::new(); + let mut cursed_inscriptions = Vec::new(); + + // Create 150 inscriptions, 50 non-cursed and 100 cursed + for i in 0..50 { + rpc_server.mine_blocks(1); + rpc_server.mine_blocks(1); + rpc_server.mine_blocks(1); + + let txid = rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(i * 3 + 1, 0, 0), (i * 3 + 2, 0, 0), (i * 3 + 3, 0, 0)], + witness: witness.clone(), + ..Default::default() + }); + + blessed_inscriptions.push(InscriptionId { txid, index: 0 }); + cursed_inscriptions.push(InscriptionId { txid, index: 1 }); + cursed_inscriptions.push(InscriptionId { txid, index: 2 }); + } + + rpc_server.mine_blocks(1); + + // Create another 60 non cursed + for _ in 0..60 { + let Inscribe { reveal, .. } = + CommandBuilder::new(format!("wallet inscribe --fee-rate 1 foo.txt")) + .write("foo.txt", "FOO") + .rpc_server(&rpc_server) + .run_and_check_output(); + rpc_server.mine_blocks(1); + blessed_inscriptions.push(InscriptionId::from(reveal)); + } + + rpc_server.mine_blocks(1); + + (blessed_inscriptions, cursed_inscriptions) +} + +#[test] +fn get_inscriptions() { + let rpc_server = test_bitcoincore_rpc::spawn(); + + create_wallet(&rpc_server); + let (blessed_inscriptions, cursed_inscriptions) = create_210_inscriptions(&rpc_server); + + let server = TestServer::spawn_with_args(&rpc_server, &["--index-sats", "--enable-json-api"]); + + let response = server.json_request(format!("/inscriptions")); + assert_eq!(response.status(), StatusCode::OK); + let inscriptions_json: InscriptionsJson = + serde_json::from_str(&response.text().unwrap()).unwrap(); + + assert_eq!(inscriptions_json.inscriptions.len(), 100); + pretty_assert_eq!( + inscriptions_json, + InscriptionsJson { + inscriptions: blessed_inscriptions[10..110] + .iter() + .cloned() + .rev() + .collect(), + prev: Some(9), + next: None, + lowest: Some(-100), + highest: Some(109), + } + ); + + let response = server.json_request(format!("/inscriptions/200/500")); + assert_eq!(response.status(), StatusCode::OK); + + let inscriptions_json: InscriptionsJson = + serde_json::from_str(&response.text().unwrap()).unwrap(); + + assert_eq!( + inscriptions_json.inscriptions.len(), + blessed_inscriptions.len() + cursed_inscriptions.len() + ); + pretty_assert_eq!( + inscriptions_json.inscriptions, + blessed_inscriptions + .iter() + .cloned() + .rev() + .chain(cursed_inscriptions) + .collect::>() + ); +} + #[test] fn json_request_fails_when_not_enabled() { let rpc_server = test_bitcoincore_rpc::spawn(); diff --git a/tests/lib.rs b/tests/lib.rs index 6ad150a58e..037b97110c 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -62,6 +62,24 @@ fn inscribe(rpc_server: &test_bitcoincore_rpc::Handle) -> Inscribe { output } +fn envelope(payload: &[&[u8]]) -> bitcoin::Witness { + let mut builder = bitcoin::script::Builder::new() + .push_opcode(bitcoin::opcodes::OP_FALSE) + .push_opcode(bitcoin::opcodes::all::OP_IF); + + for data in payload { + let mut buf = bitcoin::script::PushBytesBuf::new(); + buf.extend_from_slice(data).unwrap(); + builder = builder.push_slice(buf); + } + + let script = builder + .push_opcode(bitcoin::opcodes::all::OP_ENDIF) + .into_script(); + + bitcoin::Witness::from_slice(&[script.into_bytes(), Vec::new()]) +} + #[derive(Deserialize)] struct Create { mnemonic: Mnemonic, From 4f0b0e3c2ee467c90776408f0ab3e6fd71463c8b Mon Sep 17 00:00:00 2001 From: Ordinally Date: Sun, 13 Aug 2023 17:52:06 +0200 Subject: [PATCH 10/13] Clippy --- tests/json_api.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/json_api.rs b/tests/json_api.rs index 7e716b2029..0fa2c793af 100644 --- a/tests/json_api.rs +++ b/tests/json_api.rs @@ -193,11 +193,10 @@ fn create_210_inscriptions( // Create another 60 non cursed for _ in 0..60 { - let Inscribe { reveal, .. } = - CommandBuilder::new(format!("wallet inscribe --fee-rate 1 foo.txt")) - .write("foo.txt", "FOO") - .rpc_server(&rpc_server) - .run_and_check_output(); + let Inscribe { reveal, .. } = CommandBuilder::new("wallet inscribe --fee-rate 1 foo.txt") + .write("foo.txt", "FOO") + .rpc_server(rpc_server) + .run_and_check_output(); rpc_server.mine_blocks(1); blessed_inscriptions.push(InscriptionId::from(reveal)); } @@ -216,7 +215,7 @@ fn get_inscriptions() { let server = TestServer::spawn_with_args(&rpc_server, &["--index-sats", "--enable-json-api"]); - let response = server.json_request(format!("/inscriptions")); + let response = server.json_request("/inscriptions"); assert_eq!(response.status(), StatusCode::OK); let inscriptions_json: InscriptionsJson = serde_json::from_str(&response.text().unwrap()).unwrap(); @@ -237,7 +236,7 @@ fn get_inscriptions() { } ); - let response = server.json_request(format!("/inscriptions/200/500")); + let response = server.json_request(format!("/inscriptions/{}/{}", 200, 400)); assert_eq!(response.status(), StatusCode::OK); let inscriptions_json: InscriptionsJson = From b7e61ee2ec1b0f3dd82595c5414613172df10273 Mon Sep 17 00:00:00 2001 From: Ordinally Date: Sun, 13 Aug 2023 19:27:43 +0200 Subject: [PATCH 11/13] Adding tests for /output and /inscriptions/block --- src/index.rs | 1 + tests/json_api.rs | 130 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 129 insertions(+), 2 deletions(-) diff --git a/src/index.rs b/src/index.rs index 4ca50b5023..819ef98da1 100644 --- a/src/index.rs +++ b/src/index.rs @@ -929,6 +929,7 @@ impl Index { &self, block_height: u64, ) -> Result> { + // This is a naive approach and will require optimization, but we don't have an index by block let block_inscriptions = self .database .begin_read()? diff --git a/tests/json_api.rs b/tests/json_api.rs index 0fa2c793af..a94f68ec73 100644 --- a/tests/json_api.rs +++ b/tests/json_api.rs @@ -1,7 +1,8 @@ use { super::*, ord::inscription_id::InscriptionId, ord::rarity::Rarity, ord::templates::inscription::InscriptionJson, ord::templates::inscriptions::InscriptionsJson, - ord::templates::sat::SatJson, ord::SatPoint, test_bitcoincore_rpc::TransactionTemplate, + ord::templates::output::OutputJson, ord::templates::sat::SatJson, ord::SatPoint, + test_bitcoincore_rpc::TransactionTemplate, }; #[test] @@ -220,6 +221,7 @@ fn get_inscriptions() { let inscriptions_json: InscriptionsJson = serde_json::from_str(&response.text().unwrap()).unwrap(); + // 100 latest (blessed) inscriptions assert_eq!(inscriptions_json.inscriptions.len(), 100); pretty_assert_eq!( inscriptions_json, @@ -236,6 +238,7 @@ fn get_inscriptions() { } ); + // get all inscriptions let response = server.json_request(format!("/inscriptions/{}/{}", 200, 400)); assert_eq!(response.status(), StatusCode::OK); @@ -252,9 +255,132 @@ fn get_inscriptions() { .iter() .cloned() .rev() - .chain(cursed_inscriptions) + .chain(cursed_inscriptions.clone()) .collect::>() ); + + // iterate over all inscriptions 1 by 1 + let all_inscriptions = cursed_inscriptions + .clone() + .iter() + .cloned() + .rev() + .chain(blessed_inscriptions.clone()) + .collect::>(); // from lowest to highest inscription number + + let (lowest, highest) = ( + inscriptions_json.lowest.unwrap(), + inscriptions_json.highest.unwrap(), + ); + for i in lowest..=highest { + let response = server.json_request(format!("/inscriptions/{}/1", i)); + assert_eq!(response.status(), StatusCode::OK); + + let inscriptions_json: InscriptionsJson = + serde_json::from_str(&response.text().unwrap()).unwrap(); + + assert_eq!(inscriptions_json.inscriptions.len(), 1); + assert_eq!( + inscriptions_json.inscriptions[0], + all_inscriptions[(i - lowest) as usize] + ); + + let response = server.json_request(format!( + "/inscription/{}", + inscriptions_json.inscriptions[0] + )); + assert_eq!(response.status(), StatusCode::OK); + + let inscription_json: InscriptionJson = + serde_json::from_str(&response.text().unwrap()).unwrap(); + + assert_eq!( + inscription_json.inscription_id, + inscriptions_json.inscriptions[0] + ); + assert_eq!(inscription_json.number, i); + } +} + +#[test] +fn get_inscriptions_from_block() { + let rpc_server = test_bitcoincore_rpc::spawn(); + + create_wallet(&rpc_server); + rpc_server.mine_blocks(10); + + let txid = rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0), (2, 0, 0), (3, 0, 0)], + witness: envelope(&[b"ord", &[1], b"text/plain;charset=utf-8", &[], b"bar"]), + ..Default::default() + }); + rpc_server.mine_blocks(1); + + for _ in 0..10 { + inscribe(&rpc_server); + } + rpc_server.mine_blocks(1); + + let server = TestServer::spawn_with_args(&rpc_server, &["--index-sats", "--enable-json-api"]); + + // get all inscriptions from block 11 + let response = server.json_request(format!("/inscriptions/block/{}", 11)); + assert_eq!(response.status(), StatusCode::OK); + + let inscriptions_json: InscriptionsJson = + serde_json::from_str(&response.text().unwrap()).unwrap(); + + assert_eq!(inscriptions_json.inscriptions.len(), 3); + pretty_assert_eq!( + inscriptions_json.inscriptions, + vec![ + InscriptionId { txid, index: 2 }, + InscriptionId { txid, index: 1 }, + InscriptionId { txid, index: 0 } + ] + ); +} + +#[test] +fn get_output() { + let rpc_server = test_bitcoincore_rpc::spawn(); + + create_wallet(&rpc_server); + rpc_server.mine_blocks(3); + + let txid = rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0), (2, 0, 0), (3, 0, 0)], + witness: envelope(&[b"ord", &[1], b"text/plain;charset=utf-8", &[], b"bar"]), + ..Default::default() + }); + rpc_server.mine_blocks(1); + + let server = TestServer::spawn_with_args(&rpc_server, &["--index-sats", "--enable-json-api"]); + + let response = server.json_request(format!("/output/{}:0", txid)); + assert_eq!(response.status(), StatusCode::OK); + + let output_json: OutputJson = serde_json::from_str(&response.text().unwrap()).unwrap(); + + pretty_assert_eq!( + output_json, + OutputJson { + value: 3 * 50 * COIN_VALUE, + script_pubkey: "".to_string(), + address: None, + transaction: txid.to_string(), + sat_ranges: Some(vec![ + (5000000000, 10000000000,), + (10000000000, 15000000000,), + (15000000000, 20000000000,), + ],), + inscriptions: vec![ + InscriptionId { txid, index: 0 }, + InscriptionId { txid, index: 2 }, + InscriptionId { txid, index: 1 } + ] + } + ); } #[test] From 7a19da2d48d85b5d5f12138e1881ae1a731ef52a Mon Sep 17 00:00:00 2001 From: Ordinally Date: Mon, 14 Aug 2023 14:37:28 +0200 Subject: [PATCH 12/13] Address review comments --- src/index.rs | 5 +---- src/subcommand/server.rs | 10 ++++------ src/subcommand/server/accept_json.rs | 3 +-- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/index.rs b/src/index.rs index 819ef98da1..19f6f7c513 100644 --- a/src/index.rs +++ b/src/index.rs @@ -925,10 +925,7 @@ impl Index { Ok((inscriptions, prev, next, lowest, highest)) } - pub(crate) fn get_inscriptions_from_block( - &self, - block_height: u64, - ) -> Result> { + pub(crate) fn get_inscriptions_in_block(&self, block_height: u64) -> Result> { // This is a naive approach and will require optimization, but we don't have an index by block let block_inscriptions = self .database diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 45edb9435b..8cc8f672a1 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use { self::{ accept_json::AcceptJson, @@ -31,7 +29,7 @@ use { caches::DirCache, AcmeConfig, }, - std::{cmp::Ordering, str}, + std::{cmp::Ordering, str, sync::Arc}, tokio_stream::StreamExt, tower_http::{ compression::CompressionLayer, @@ -177,7 +175,7 @@ impl Server { .route("/input/:block/:transaction/:input", get(Self::input)) .route("/inscription/:inscription_id", get(Self::inscription)) .route("/inscriptions", get(Self::inscriptions)) - .route("/inscriptions/block/:n", get(Self::inscriptions_block)) + .route("/inscriptions/block/:n", get(Self::inscriptions_in_block)) .route("/inscriptions/:from", get(Self::inscriptions_from)) .route("/inscriptions/:from/:n", get(Self::inscriptions_from_n)) .route("/install.sh", get(Self::install_script)) @@ -1006,13 +1004,13 @@ impl Server { Self::inscriptions_inner(page_config, index, None, 100, accept_json).await } - async fn inscriptions_block( + async fn inscriptions_in_block( Extension(page_config): Extension>, Extension(index): Extension>, Path(block_height): Path, accept_json: AcceptJson, ) -> ServerResult { - let inscriptions = index.get_inscriptions_from_block(block_height)?; + let inscriptions = index.get_inscriptions_in_block(block_height)?; Ok(if accept_json.0 { Json(InscriptionsJson::new(inscriptions, None, None, None, None)).into_response() } else { diff --git a/src/subcommand/server/accept_json.rs b/src/subcommand/server/accept_json.rs index b528b7df2e..a471c3d049 100644 --- a/src/subcommand/server/accept_json.rs +++ b/src/subcommand/server/accept_json.rs @@ -1,5 +1,4 @@ -use super::*; -use axum::extract::FromRef; +use {super::*, axum::extract::FromRef}; pub(crate) struct AcceptJson(pub(crate) bool); From dd35af6e11e794662e321241326fc019ad914b57 Mon Sep 17 00:00:00 2001 From: raph Date: Mon, 14 Aug 2023 14:46:17 +0200 Subject: [PATCH 13/13] Update tests/json_api.rs --- tests/json_api.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/json_api.rs b/tests/json_api.rs index a94f68ec73..893ede9327 100644 --- a/tests/json_api.rs +++ b/tests/json_api.rs @@ -303,7 +303,7 @@ fn get_inscriptions() { } #[test] -fn get_inscriptions_from_block() { +fn get_inscriptions_in_block() { let rpc_server = test_bitcoincore_rpc::spawn(); create_wallet(&rpc_server);