Skip to content

Commit

Permalink
Add JSON API endpoint /sat/<SAT> (ordinals#2250)
Browse files Browse the repository at this point in the history
Co-authored-by: raphjaph <[email protected]>
  • Loading branch information
Mathieu-Be and raphjaph committed Aug 10, 2023
1 parent fb0b1e8 commit 13d5c2f
Show file tree
Hide file tree
Showing 14 changed files with 205 additions and 36 deletions.
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ members = [".", "test-bitcoincore-rpc"]

[dependencies]
anyhow = { version = "1.0.56", features = ["backtrace"] }
async-trait = "0.1.72"
axum = { version = "0.6.1", features = ["headers"] }
axum-server = "0.5.0"
base64 = "0.21.0"
Expand Down
2 changes: 1 addition & 1 deletion src/epoch.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::*;

#[derive(Copy, Clone, Eq, PartialEq, Debug, Display, PartialOrd)]
#[derive(Copy, Clone, Eq, PartialEq, Debug, Display, Serialize, PartialOrd)]
pub(crate) struct Epoch(pub(crate) u64);

impl Epoch {
Expand Down
2 changes: 1 addition & 1 deletion src/height.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::*;

#[derive(Copy, Clone, Debug, Display, FromStr, Ord, Eq, PartialEq, PartialOrd)]
#[derive(Copy, Clone, Debug, Display, FromStr, Ord, Eq, Serialize, PartialEq, PartialOrd)]
pub(crate) struct Height(pub(crate) u64);

impl Height {
Expand Down
4 changes: 4 additions & 0 deletions src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,10 @@ impl Index {
Ok(())
}

pub(crate) fn is_json_api_enabled(&self) -> bool {
self.options.enable_json_api
}

pub(crate) fn is_reorged(&self) -> bool {
self.reorged.load(atomic::Ordering::Relaxed)
}
Expand Down
8 changes: 4 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,19 +108,19 @@ mod fee_rate;
mod height;
mod index;
mod inscription;
mod inscription_id;
pub mod inscription_id;
mod media;
mod object;
mod options;
mod outgoing;
mod page_config;
mod rarity;
pub mod rarity;
mod representation;
mod sat;
pub mod sat;
mod sat_point;
pub mod subcommand;
mod tally;
mod templates;
pub mod templates;
mod wallet;

type Result<T = (), E = Error> = std::result::Result<T, E>;
Expand Down
2 changes: 2 additions & 0 deletions src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ pub(crate) struct Options {
pub(crate) testnet: bool,
#[clap(long, default_value = "ord", help = "Use wallet named <WALLET>.")]
pub(crate) wallet: String,
#[clap(long, short, help = "Enable JSON API.")]
pub(crate) enable_json_api: bool,
}

impl Options {
Expand Down
45 changes: 36 additions & 9 deletions src/subcommand/server.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use {
self::{
accept_json::AcceptJson,
deserialize_from_str::DeserializeFromStr,
error::{OptionExt, ServerError, ServerResult},
},
Expand All @@ -8,11 +9,11 @@ use {
crate::templates::{
BlockHtml, ClockSvg, HomeHtml, InputHtml, InscriptionHtml, InscriptionsHtml, OutputHtml,
PageContent, PageHtml, PreviewAudioHtml, PreviewImageHtml, PreviewPdfHtml, PreviewTextHtml,
PreviewUnknownHtml, PreviewVideoHtml, RangeHtml, RareTxt, SatHtml, TransactionHtml,
PreviewUnknownHtml, PreviewVideoHtml, RangeHtml, RareTxt, SatHtml, SatJson, TransactionHtml,
},
axum::{
body,
extract::{Extension, Path, Query},
extract::{Extension, Json, Path, Query},
headers::UserAgent,
http::{header, HeaderMap, HeaderValue, StatusCode, Uri},
response::{IntoResponse, Redirect, Response},
Expand All @@ -36,6 +37,7 @@ use {
},
};

mod accept_json;
mod error;

enum BlockQuery {
Expand Down Expand Up @@ -382,18 +384,43 @@ impl Server {
Extension(page_config): Extension<Arc<PageConfig>>,
Extension(index): Extension<Arc<Index>>,
Path(DeserializeFromStr(sat)): Path<DeserializeFromStr<Sat>>,
) -> ServerResult<PageHtml<SatHtml>> {
accept_json: AcceptJson,
) -> ServerResult<Response> {
let satpoint = index.rare_sat_satpoint(sat)?;

Ok(
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()
}
} else {
SatHtml {
sat,
satpoint,
blocktime: index.block_time(sat.height())?,
inscriptions: index.get_inscription_ids_by_sat(sat)?,
blocktime,
inscriptions,
}
.page(page_config, index.has_sat_index()?),
)
.page(page_config, index.has_sat_index()?)
.into_response()
})
}

async fn ordinal(Path(sat): Path<String>) -> Redirect {
Expand Down
24 changes: 24 additions & 0 deletions src/subcommand/server/accept_json.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use super::*;

pub(crate) struct AcceptJson(pub(crate) bool);

#[async_trait::async_trait]
impl<S> axum::extract::FromRequestParts<S> for AcceptJson
where
S: Send + Sync,
{
type Rejection = ();

async fn from_request_parts(
parts: &mut http::request::Parts,
_state: &S,
) -> Result<Self, Self::Rejection> {
Ok(Self(
parts
.headers
.get("accept")
.map(|value| value == "application/json")
.unwrap_or_default(),
))
}
}
4 changes: 2 additions & 2 deletions src/templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub(crate) use {
},
range::RangeHtml,
rare::RareTxt,
sat::SatHtml,
sat::{SatHtml, SatJson},
transaction::TransactionHtml,
};

Expand All @@ -31,7 +31,7 @@ mod output;
mod preview;
mod range;
mod rare;
mod sat;
pub mod sat;
mod transaction;

#[derive(Boilerplate)]
Expand Down
18 changes: 18 additions & 0 deletions src/templates/sat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@ pub(crate) struct SatHtml {
pub(crate) inscriptions: Vec<InscriptionId>,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct SatJson {
pub number: u64,
pub decimal: String,
pub degree: String,
pub name: String,
pub block: u64,
pub cycle: u64,
pub epoch: u64,
pub period: u64,
pub offset: u64,
pub rarity: Rarity,
pub percentile: String,
pub satpoint: Option<SatPoint>,
pub timestamp: String,
pub inscriptions: Vec<InscriptionId>,
}

impl PageContent for SatHtml {
fn title(&self) -> String {
format!("Sat {}", self.sat)
Expand Down
86 changes: 86 additions & 0 deletions tests/json_api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use {
super::*, ord::inscription_id::InscriptionId, ord::rarity::Rarity, ord::templates::sat::SatJson,
ord::SatPoint,
};

#[test]
fn get_sat_without_sat_index() {
let rpc_server = test_bitcoincore_rpc::spawn();

let response = TestServer::spawn_with_args(&rpc_server, &["--enable-json-api"])
.json_request("/sat/2099999997689999");

assert_eq!(response.status(), StatusCode::OK);

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();

pretty_assert_eq!(
sat_json,
SatJson {
number: 2099999997689999,
decimal: "6929999.0".into(),
degree: "5°209999′1007″0‴".into(),
name: "a".into(),
block: 6929999,
cycle: 5,
epoch: 32,
period: 3437,
offset: 0,
rarity: Rarity::Uncommon,
percentile: "100%".into(),
satpoint: None,
timestamp: "".into(),
inscriptions: vec![],
}
)
}

#[test]
fn get_sat_with_inscription_and_sat_index() {
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: "1970-01-01 00:00:01 UTC".into(),
inscriptions: vec![inscription_id],
}
)
}

#[test]
fn json_request_fails_when_not_enabled() {
let rpc_server = test_bitcoincore_rpc::spawn();

let response =
TestServer::spawn_with_args(&rpc_server, &[]).json_request("/sat/2099999997689999");

assert_eq!(response.status(), StatusCode::NOT_ACCEPTABLE);
}
6 changes: 4 additions & 2 deletions tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,18 +74,20 @@ fn create_wallet(rpc_server: &test_bitcoincore_rpc::Handle) {
}

mod command_builder;
mod expected;
mod test_server;

mod core;
mod epochs;
mod expected;
mod find;
mod index;
mod info;
mod json_api;
mod list;
mod parse;
mod server;
mod subsidy;
mod supply;
mod test_server;
mod traits;
mod version;
mod wallet;
Loading

0 comments on commit 13d5c2f

Please sign in to comment.