Skip to content

Commit

Permalink
Add /content endpoint (#976)
Browse files Browse the repository at this point in the history
  • Loading branch information
casey authored Dec 16, 2022
1 parent a71d54e commit b835eb4
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 9 deletions.
14 changes: 9 additions & 5 deletions src/inscription.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,20 @@ impl Inscription {
}
}

pub(crate) fn content_type(&self) -> Option<&str> {
str::from_utf8(self.content_type.as_ref()?).ok()
pub(crate) fn content_bytes(&self) -> Option<&[u8]> {
Some(self.content.as_ref()?)
}

pub(crate) fn content_html(&self) -> Trusted<ContentHtml> {
Trusted(ContentHtml(self.content()))
}

pub(crate) fn content_size(&self) -> Option<usize> {
Some(self.content.as_ref()?.len())
Some(self.content_bytes()?.len())
}

pub(crate) fn content_html(&self) -> Trusted<ContentHtml> {
Trusted(ContentHtml(self.content()))
pub(crate) fn content_type(&self) -> Option<&str> {
str::from_utf8(self.content_type.as_ref()?).ok()
}
}

Expand Down
64 changes: 63 additions & 1 deletion src/subcommand/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,11 @@ impl Server {
.route("/block/:query", get(Self::block))
.route("/bounties", get(Self::bounties))
.route("/clock", get(Self::clock))
.route("/content/:inscription_id", get(Self::content))
.route("/faq", get(Self::faq))
.route("/favicon.ico", get(Self::favicon))
.route("/input/:block/:transaction/:input", get(Self::input))
.route("/inscription/:txid", get(Self::inscription))
.route("/inscription/:inscription_id", get(Self::inscription))
.route("/install.sh", get(Self::install_script))
.route("/ordinal/:sat", get(Self::ordinal))
.route("/output/:output", get(Self::output))
Expand Down Expand Up @@ -635,6 +636,37 @@ impl Server {
Redirect::to("https://docs.ordinals.com/bounty/")
}

async fn content(
Extension(index): Extension<Arc<Index>>,
Path(inscription_id): Path<InscriptionId>,
) -> ServerResult<Response> {
let (inscription, _) = index
.get_inscription_by_inscription_id(inscription_id)
.map_err(|err| {
ServerError::Internal(anyhow!(
"failed to retrieve inscription with inscription id {inscription_id} from index: {err}"
))
})?
.ok_or_else(|| {
ServerError::NotFound(format!("transaction {inscription_id} has no inscription"))
})?;

let (content_type, content) = Self::content_response(inscription).ok_or_else(|| {
ServerError::NotFound(format!("inscription {inscription_id} has no content"))
})?;

Ok(([(header::CONTENT_TYPE, content_type)], content).into_response())
}

fn content_response(inscription: Inscription) -> Option<(String, Vec<u8>)> {
let content = inscription.content_bytes()?;

match inscription.content_type() {
Some(content_type) => Some((content_type.into(), content.to_vec())),
None => Some(("application/octet-stream".into(), content.to_vec())),
}
}

async fn inscription(
Extension(chain): Extension<Chain>,
Extension(index): Extension<Arc<Index>>,
Expand Down Expand Up @@ -1631,4 +1663,34 @@ next.*",
5,
);
}

#[test]
fn content_response_no_content() {
assert_eq!(
Server::content_response(Inscription::new(
Some("text/plain".as_bytes().to_vec()),
None
)),
None
);
}

#[test]
fn content_response_with_content() {
assert_eq!(
Server::content_response(Inscription::new(
Some("text/plain".as_bytes().to_vec()),
Some(vec![1, 2, 3]),
)),
Some(("text/plain".into(), vec![1, 2, 3]))
);
}

#[test]
fn content_response_no_content_type() {
assert_eq!(
Server::content_response(Inscription::new(None, Some(vec![]))),
Some(("application/octet-stream".into(), vec![]))
);
}
}
28 changes: 28 additions & 0 deletions tests/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,31 @@ HELLOWORLD.*",
),
)
}

#[test]
fn inscription_content() {
let rpc_server = test_bitcoincore_rpc::spawn_with(Network::Regtest, "ord");
let txid = rpc_server.mine_blocks(1)[0].txdata[0].txid();

let stdout = CommandBuilder::new(format!(
"--chain regtest wallet inscribe --satpoint {txid}:0:0 --file hello.txt"
))
.write("hello.txt", "HELLOWORLD")
.rpc_server(&rpc_server)
.stdout_regex("commit\t[[:xdigit:]]{64}\nreveal\t[[:xdigit:]]{64}\n")
.run();

let reveal_tx = reveal_txid_from_inscribe_stdout(&stdout);

rpc_server.mine_blocks(1);

let response =
TestServer::spawn_with_args(&rpc_server, &[]).request(&format!("/content/{reveal_tx}"));

assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.headers().get("content-type").unwrap(),
"text/plain;charset=utf-8"
);
assert_eq!(response.bytes().unwrap(), "HELLOWORLD");
}
27 changes: 24 additions & 3 deletions tests/test_server.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use super::*;
use crate::command_builder::ToArgs;
use bitcoincore_rpc::{Auth, Client, RpcApi};
use {
super::*,
crate::command_builder::ToArgs,
bitcoincore_rpc::{Auth, Client, RpcApi},
reqwest::blocking::Response,
};

pub(crate) struct TestServer {
child: Child,
Expand Down Expand Up @@ -76,6 +79,24 @@ impl TestServer {
assert_eq!(response.status(), StatusCode::OK);
assert_regex_match!(response.text().unwrap(), regex);
}

pub(crate) fn request(&self, path: &str) -> Response {
let client = Client::new(&self.rpc_url, Auth::None).unwrap();
let chain_block_count = client.get_block_count().unwrap() + 1;

for i in 0.. {
let response = reqwest::blocking::get(self.url().join("/block-count").unwrap()).unwrap();
assert_eq!(response.status(), StatusCode::OK);
if response.text().unwrap().parse::<u64>().unwrap() == chain_block_count {
break;
} else if i == 20 {
panic!("index failed to synchronize with chain");
}
thread::sleep(Duration::from_millis(25));
}

reqwest::blocking::get(self.url().join(path).unwrap()).unwrap()
}
}

impl Drop for TestServer {
Expand Down

0 comments on commit b835eb4

Please sign in to comment.