From a2d397a3f0ca6eed5d889e97b466c60da870f87d Mon Sep 17 00:00:00 2001 From: raph Date: Mon, 23 Dec 2024 20:51:42 +0100 Subject: [PATCH] Add `/r/utxo/:outpoint` endpoint (#4148) --- docs/src/guides/api.md | 2 +- docs/src/inscriptions/recursion.md | 58 ++++++++++++ src/api.rs | 8 ++ src/index.rs | 21 +++++ src/subcommand/server.rs | 137 +++++++++++++++++++++++++++++ 5 files changed, 225 insertions(+), 1 deletion(-) diff --git a/docs/src/guides/api.md b/docs/src/guides/api.md index c368d036e2..f53f77afc0 100644 --- a/docs/src/guides/api.md +++ b/docs/src/guides/api.md @@ -5129,4 +5129,4 @@ curl -s -H "Accept: application/json" \ See [Recursion](../inscriptions/recursion.md) for an explanation of these. -{{#include ../inscriptions/recursion.md:35:3371}} +{{#include ../inscriptions/recursion.md:35:3429}} diff --git a/docs/src/inscriptions/recursion.md b/docs/src/inscriptions/recursion.md index abb151bc9c..c8d8a1d7bf 100644 --- a/docs/src/inscriptions/recursion.md +++ b/docs/src/inscriptions/recursion.md @@ -3370,6 +3370,64 @@ curl -s -H "Accept: application/json" \ ``` +
+ + GET + /r/utxo/<OUTPOINT> + + +### Description + +Get assets held by an unspent transaction output. + +### Examples + +Unspent transaction output with server without any indices: + +```bash +curl -s -H "Accept: application/json" \ + http://0.0.0.0:80/r/utxo/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0 +``` + +```json +{ + "inscriptions": null, + "runes": null, + "sat_ranges": null, + "value": 5000000000 +} +``` + +With rune, inscription, and sat index: + +```bash +curl -s -H "Accept: application/json" \ + http://0.0.0.0:80/r/utxo/626860df36c1047194866c6812f04c15ab84f3690e7cc06fd600c841f1943e05:0 +``` + +```json +{ + "inscriptions": [ + "6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0" + ], + "runes": { + "UNCOMMON•GOODS": { + "amount": 6845, + "divisibility": 0, + "symbol": "⧉" + } + }, + "sat_ranges": [ + [ + 1905800627509113, + 1905800627509443 + ] + ], + "value": 330 +} +``` +
+     diff --git a/src/api.rs b/src/api.rs index 5b335c48e0..9675f5f3d2 100644 --- a/src/api.rs +++ b/src/api.rs @@ -151,6 +151,14 @@ pub struct Inscriptions { pub page_index: u32, } +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +pub struct UtxoRecursive { + pub inscriptions: Option>, + pub runes: Option>, + pub sat_ranges: Option>, + pub value: u64, +} + #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] pub struct Output { pub address: Option>, diff --git a/src/index.rs b/src/index.rs index b5e035b668..16122a6ee8 100644 --- a/src/index.rs +++ b/src/index.rs @@ -2358,6 +2358,27 @@ impl Index { Ok(acc) } + pub(crate) fn get_utxo_recursive( + &self, + outpoint: OutPoint, + ) -> Result> { + let Some(utxo_entry) = self + .database + .begin_read()? + .open_table(OUTPOINT_TO_UTXO_ENTRY)? + .get(&outpoint.store())? + else { + return Ok(None); + }; + + Ok(Some(api::UtxoRecursive { + inscriptions: self.get_inscriptions_for_output(outpoint)?, + runes: self.get_rune_balances_for_output(outpoint)?, + sat_ranges: self.list(outpoint)?, + value: utxo_entry.value().parse(self).total_value(), + })) + } + pub(crate) fn get_output_info(&self, outpoint: OutPoint) -> Result> { let sat_ranges = self.list(outpoint)?; diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index cdbaf763b2..547060ea0f 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -272,6 +272,7 @@ impl Server { "/r/sat/:sat_number/at/:index", get(Self::sat_inscription_at_index), ) + .route("/r/utxo/:outpoint", get(Self::utxo_recursive)) .route("/rare.txt", get(Self::rare_txt)) .route("/rune/:rune", get(Self::rune)) .route("/runes", get(Self::runes)) @@ -659,6 +660,22 @@ impl Server { }) } + async fn utxo_recursive( + Extension(index): Extension>, + Path(outpoint): Path, + ) -> ServerResult { + task::block_in_place(|| { + Ok( + Json( + index + .get_utxo_recursive(outpoint)? + .ok_or_not_found(|| format!("output {outpoint}"))?, + ) + .into_response(), + ) + }) + } + async fn satpoint( Extension(index): Extension>, Path(satpoint): Path, @@ -6335,6 +6352,126 @@ next ); } + #[test] + fn utxo_recursive_endpoint_all() { + let server = TestServer::builder() + .chain(Chain::Regtest) + .index_sats() + .index_runes() + .build(); + + let rune = Rune(RUNE); + + let (txid, id) = server.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + divisibility: Some(1), + rune: Some(rune), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + None, + ); + + pretty_assert_eq!( + server.index.runes().unwrap(), + [( + id, + RuneEntry { + block: id.block, + divisibility: 1, + etching: txid, + spaced_rune: SpacedRune { rune, spacers: 0 }, + premine: u128::MAX, + timestamp: id.block, + ..default() + } + )] + ); + + server.mine_blocks(1); + + // merge rune with two inscriptions + let txid = server.core.broadcast_tx(TransactionTemplate { + inputs: &[ + (6, 0, 0, inscription("text/plain", "foo").to_witness()), + (7, 0, 0, inscription("text/plain", "bar").to_witness()), + (7, 1, 0, Witness::new()), + ], + ..default() + }); + + server.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + let second_inscription_id = InscriptionId { txid, index: 1 }; + let outpoint: OutPoint = OutPoint { txid, vout: 0 }; + + let utxo_recursive = server.get_json::(format!("/r/utxo/{}", outpoint)); + + pretty_assert_eq!( + utxo_recursive, + api::UtxoRecursive { + inscriptions: Some(vec![inscription_id, second_inscription_id]), + runes: Some( + [( + SpacedRune { rune, spacers: 0 }, + Pile { + amount: u128::MAX, + divisibility: 1, + symbol: None + } + )] + .into_iter() + .collect() + ), + sat_ranges: Some(vec![ + (6 * 50 * COIN_VALUE, 7 * 50 * COIN_VALUE), + (7 * 50 * COIN_VALUE, 8 * 50 * COIN_VALUE), + (50 * COIN_VALUE, 2 * 50 * COIN_VALUE) + ]), + value: 150 * COIN_VALUE, + } + ); + } + + #[test] + fn utxo_recursive_endpoint_only_inscriptions() { + let server = TestServer::builder().chain(Chain::Regtest).build(); + + server.mine_blocks(1); + + let txid = server.core.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())], + ..default() + }); + + server.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + let outpoint: OutPoint = OutPoint { txid, vout: 0 }; + + let utxo_recursive = server.get_json::(format!("/r/utxo/{}", outpoint)); + + pretty_assert_eq!( + utxo_recursive, + api::UtxoRecursive { + inscriptions: Some(vec![inscription_id]), + runes: None, + sat_ranges: None, + value: 50 * COIN_VALUE, + } + ); + } + #[test] fn sat_recursive_endpoints() { let server = TestServer::builder()