diff --git a/docs/src/inscriptions/recursion.md b/docs/src/inscriptions/recursion.md index 023568c005..1deab60581 100644 --- a/docs/src/inscriptions/recursion.md +++ b/docs/src/inscriptions/recursion.md @@ -29,6 +29,7 @@ The recursive endpoints are: - `/r/blocktime`: UNIX time stamp of latest block. - `/r/children/`: the first 100 child inscription ids. - `/r/children//`: the set of 100 child inscription ids on ``. +- `/r/inscription/:inscription_id`: information about an inscription - `/r/metadata/`: JSON string containing the hex-encoded CBOR metadata. - `/r/sat/`: the first 100 inscription ids on a sat. - `/r/sat//`: the set of 100 inscription ids on ``. @@ -50,16 +51,38 @@ plain-text responses. Examples -------- +- `/r/blockhash/0`: + +```json +"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" +``` + - `/r/blockheight`: ```json 777000 ``` -- `/r/blockhash/0`: +- `/r/blockinfo/0`: ```json -"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" +{ + "bits": 486604799, + "chainwork": 0, + "confirmations": 0, + "difficulty": 0.0, + "hash": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", + "height": 0, + "median_time": null, + "merkle_root": "0000000000000000000000000000000000000000000000000000000000000000", + "next_block": null, + "nonce": 0, + "previous_block": null, + "target": "00000000ffff0000000000000000000000000000000000000000000000000000", + "timestamp": 0, + "transaction_count": 0, + "version": 1 +} ``` - `/r/blocktime`: @@ -68,6 +91,40 @@ Examples 1700770905 ``` +- `/r/children/60bcf821240064a9c55225c4f01711b0ebbcab39aa3fafeefe4299ab158536fai0/49`: + +```json +{ + "ids":[ + "7cd66b8e3a63dcd2fada917119830286bca0637267709d6df1ca78d98a1b4487i4900", + "7cd66b8e3a63dcd2fada917119830286bca0637267709d6df1ca78d98a1b4487i4901", + ... + "7cd66b8e3a63dcd2fada917119830286bca0637267709d6df1ca78d98a1b4487i4935", + "7cd66b8e3a63dcd2fada917119830286bca0637267709d6df1ca78d98a1b4487i4936" + ], + "more":false, + "page":49 +} +``` + +- `r/inscription/3bd72a7ef68776c9429961e43043ff65efa7fb2d8bb407386a9e3b19f149bc36i0` + +```json +{ + "charms": [], + "content_type": "image/png", + "content_length": 144037, + "fee": 36352, + "height": 209, + "number": 2, + "output": "3bd72a7ef68776c9429961e43043ff65efa7fb2d8bb407386a9e3b19f149bc36:0", + "sat": null, + "satpoint": "3bd72a7ef68776c9429961e43043ff65efa7fb2d8bb407386a9e3b19f149bc36:0:0", + "timestamp": 1708312562, + "value": 10000 +} +``` + - `/r/metadata/35b66389b44535861c44b2b18ed602997ee11db9a30d384ae89630c9fc6f011fi3`: ```json @@ -109,25 +166,3 @@ Examples "page":49 } ``` - -- `/r/blockinfo/0`: - -```json -{ - "bits": 486604799, - "chainwork": 0, - "confirmations": 0, - "difficulty": 0.0, - "hash": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", - "height": 0, - "median_time": null, - "merkle_root": "0000000000000000000000000000000000000000000000000000000000000000", - "next_block": null, - "nonce": 0, - "previous_block": null, - "target": "00000000ffff0000000000000000000000000000000000000000000000000000", - "timestamp": 0, - "transaction_count": 0, - "version": 1 -} -``` diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 9ca830bef1..77aa93b1ae 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -10,12 +10,12 @@ use { templates::{ BlockHtml, BlockInfoJson, BlockJson, BlocksHtml, BlocksJson, ChildrenHtml, ChildrenJson, ClockSvg, CollectionsHtml, HomeHtml, InputHtml, InscriptionHtml, InscriptionJson, - InscriptionsBlockHtml, InscriptionsHtml, InscriptionsJson, OutputHtml, OutputJson, - PageContent, PageHtml, PreviewAudioHtml, PreviewCodeHtml, PreviewFontHtml, PreviewImageHtml, - PreviewMarkdownHtml, PreviewModelHtml, PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml, - PreviewVideoHtml, RangeHtml, RareTxt, RuneBalancesHtml, RuneHtml, RuneJson, RunesHtml, - RunesJson, SatHtml, SatInscriptionJson, SatInscriptionsJson, SatJson, TransactionHtml, - TransactionJson, + InscriptionRecursiveJson, InscriptionsBlockHtml, InscriptionsHtml, InscriptionsJson, + OutputHtml, OutputJson, PageContent, PageHtml, PreviewAudioHtml, PreviewCodeHtml, + PreviewFontHtml, PreviewImageHtml, PreviewMarkdownHtml, PreviewModelHtml, PreviewPdfHtml, + PreviewTextHtml, PreviewUnknownHtml, PreviewVideoHtml, RangeHtml, RareTxt, RuneBalancesHtml, + RuneHtml, RuneJson, RunesHtml, RunesJson, SatHtml, SatInscriptionJson, SatInscriptionsJson, + SatJson, TransactionHtml, TransactionJson, }, }, axum::{ @@ -272,6 +272,10 @@ impl Server { .route("/r/blockheight", get(Self::block_height)) .route("/r/blocktime", get(Self::block_time)) .route("/r/blockinfo/:query", get(Self::block_info)) + .route( + "/r/inscription/:inscription_id", + get(Self::inscription_recursive), + ) .route("/r/children/:inscription_id", get(Self::children_recursive)) .route( "/r/children/:inscription_id/:page", @@ -873,6 +877,65 @@ impl Server { }) } + async fn inscription_recursive( + Extension(index): Extension>, + Path(inscription_id): Path, + ) -> ServerResult { + task::block_in_place(|| { + let inscription = index + .get_inscription_by_id(inscription_id)? + .ok_or_not_found(|| format!("inscription {inscription_id}"))?; + + let entry = index + .get_inscription_entry(inscription_id) + .unwrap() + .unwrap(); + + let satpoint = index + .get_inscription_satpoint_by_id(inscription_id) + .ok() + .flatten() + .unwrap(); + + let output = if satpoint.outpoint == unbound_outpoint() { + None + } else { + Some( + index + .get_transaction(satpoint.outpoint.txid)? + .ok_or_not_found(|| format!("inscription {inscription_id} current transaction"))? + .output + .into_iter() + .nth(satpoint.outpoint.vout.try_into().unwrap()) + .ok_or_not_found(|| { + format!("inscription {inscription_id} current transaction output") + })?, + ) + }; + + Ok( + Json(InscriptionRecursiveJson { + charms: Charm::ALL + .iter() + .filter(|charm| charm.is_set(entry.charms)) + .map(|charm| charm.title().into()) + .collect(), + content_type: inscription.content_type().map(|s| s.to_string()), + content_length: inscription.content_length(), + fee: entry.fee, + height: entry.height, + number: entry.inscription_number, + output: satpoint.outpoint, + value: output.as_ref().map(|o| o.value), + sat: entry.sat, + satpoint, + timestamp: timestamp(entry.timestamp).timestamp(), + }) + .into_response(), + ) + }) + } + async fn status( Extension(server_config): Extension>, Extension(index): Extension>, diff --git a/src/templates.rs b/src/templates.rs index 39642b4210..2dedc4bb12 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -9,7 +9,7 @@ pub(crate) use { home::HomeHtml, iframe::Iframe, input::InputHtml, - inscription::{InscriptionHtml, InscriptionJson}, + inscription::{InscriptionHtml, InscriptionJson, InscriptionRecursiveJson}, inscriptions::{InscriptionsHtml, InscriptionsJson}, inscriptions_block::InscriptionsBlockHtml, metadata::MetadataHtml, diff --git a/src/templates/inscription.rs b/src/templates/inscription.rs index 166527dfe5..b23d5dd890 100644 --- a/src/templates/inscription.rs +++ b/src/templates/inscription.rs @@ -20,6 +20,21 @@ pub(crate) struct InscriptionHtml { pub(crate) charms: u16, } +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct InscriptionRecursiveJson { + pub charms: Vec, + pub content_type: Option, + pub content_length: Option, + pub fee: u64, + pub height: u32, + pub number: i32, + pub output: OutPoint, + pub sat: Option, + pub satpoint: SatPoint, + pub timestamp: i64, + pub value: Option, +} + #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] pub struct InscriptionJson { pub address: Option, diff --git a/tests/lib.rs b/tests/lib.rs index d449b3bba5..f1f3c924b3 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -16,8 +16,9 @@ use { subcommand::runes::RuneInfo, templates::{ block::BlockJson, blocks::BlocksJson, inscription::InscriptionJson, - inscriptions::InscriptionsJson, output::OutputJson, rune::RuneJson, runes::RunesJson, - sat::SatJson, status::StatusJson, transaction::TransactionJson, + inscription::InscriptionRecursiveJson, inscriptions::InscriptionsJson, output::OutputJson, + rune::RuneJson, runes::RunesJson, sat::SatJson, status::StatusJson, + transaction::TransactionJson, }, Edict, InscriptionId, Rune, RuneEntry, RuneId, Runestone, }, diff --git a/tests/server.rs b/tests/server.rs index 2981d9f144..d1b1034f05 100644 --- a/tests/server.rs +++ b/tests/server.rs @@ -281,6 +281,57 @@ fn inscription_metadata() { ); } +#[test] +fn recursive_inscription_endpoint() { + let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + let output = CommandBuilder::new("wallet inscribe --fee-rate 1 --file foo.txt") + .write("foo.txt", "FOO") + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + bitcoin_rpc_server.mine_blocks(1); + + let inscription = output.inscriptions.first().unwrap(); + let response = ord_rpc_server.request(format!("/r/inscription/{}", inscription.id)); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get("content-type").unwrap(), + "application/json" + ); + + let inscription_recursive_json: InscriptionRecursiveJson = + serde_json::from_str(&response.text().unwrap()).unwrap(); + + pretty_assert_eq!( + inscription_recursive_json, + InscriptionRecursiveJson { + charms: vec!["coin".into(), "uncommon".into()], + content_type: Some("text/plain;charset=utf-8".to_string()), + content_length: Some(3), + fee: 138, + height: 2, + number: 0, + output: inscription.location.outpoint, + sat: Some(Sat(50 * COIN_VALUE)), + satpoint: SatPoint { + outpoint: inscription.location.outpoint, + offset: 0, + }, + timestamp: 2, + value: Some(10000), + } + ) +} + #[test] fn inscriptions_page() { let bitcoin_rpc_server = test_bitcoincore_rpc::spawn();