Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Embellish block page #605

Merged
merged 8 commits into from
Oct 4, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use {
super::*,
bitcoin::consensus::encode::{deserialize, serialize},
bitcoin::BlockHeader,
bitcoincore_rpc::{Auth, Client, RpcApi},
bitcoincore_rpc::{json::GetBlockHeaderResult, Auth, Client, RpcApi},
rayon::iter::{IntoParallelRefIterator, ParallelIterator},
redb::WriteStrategy,
std::sync::atomic::{AtomicBool, Ordering},
Expand All @@ -21,6 +21,8 @@ pub(crate) struct Index {
database_path: PathBuf,
height_limit: Option<u64>,
reorged: AtomicBool,
genesis_block_coinbase_txid: Txid,
genesis_block_coinbase_transaction: Transaction,
}

#[derive(Debug, PartialEq)]
Expand Down Expand Up @@ -103,12 +105,20 @@ impl Index {

tx.commit()?;

let genesis_block_coinbase_transaction =
bitcoin::blockdata::constants::genesis_block(options.chain.network())
.coinbase()
.unwrap()
.clone();

Ok(Self {
client,
database,
database_path,
height_limit: options.height_limit,
reorged: AtomicBool::new(false),
genesis_block_coinbase_txid: genesis_block_coinbase_transaction.txid(),
genesis_block_coinbase_transaction,
})
}

Expand Down Expand Up @@ -427,12 +437,20 @@ impl Index {
self.client.get_block_header(&hash).into_option()
}

pub(crate) fn block_header_info(&self, hash: BlockHash) -> Result<Option<GetBlockHeaderResult>> {
self.client.get_block_header_info(&hash).into_option()
}

pub(crate) fn block_with_hash(&self, hash: BlockHash) -> Result<Option<Block>> {
self.client.get_block(&hash).into_option()
}

pub(crate) fn transaction(&self, txid: Txid) -> Result<Option<Transaction>> {
self.client.get_raw_transaction(&txid, None).into_option()
if txid == self.genesis_block_coinbase_txid {
Ok(Some(self.genesis_block_coinbase_transaction.clone()))
} else {
self.client.get_raw_transaction(&txid, None).into_option()
}
}

pub(crate) fn is_transaction_in_active_chain(&self, txid: Txid) -> Result<bool> {
Expand Down
4 changes: 3 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use {
rarity::Rarity,
sat_point::SatPoint,
subcommand::Subcommand,
tally::Tally,
},
anyhow::{anyhow, bail, Context, Error},
axum::{extract, http::StatusCode, response::Html, response::IntoResponse, routing::get, Router},
Expand All @@ -23,7 +24,7 @@ use {
consensus::{Decodable, Encodable},
hash_types::BlockHash,
hashes::Hash,
Address, Block, Network, OutPoint, Transaction, Txid,
Address, Block, Network, OutPoint, Transaction, TxOut, Txid,
},
chrono::{DateTime, NaiveDateTime, Utc},
clap::Parser,
Expand Down Expand Up @@ -71,6 +72,7 @@ mod ordinal;
mod rarity;
mod sat_point;
mod subcommand;
mod tally;

type Result<T = (), E = Error> = std::result::Result<T, E>;

Expand Down
121 changes: 96 additions & 25 deletions src/subcommand/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use {
deserialize_ordinal_from_str::DeserializeOrdinalFromStr,
templates::{
block::BlockHtml, clock::ClockSvg, home::HomeHtml, ordinal::OrdinalHtml, output::OutputHtml,
range::RangeHtml, transaction::TransactionHtml, Content,
range::RangeHtml, transaction::TransactionHtml, Content, PageHtml,
},
},
axum::{
Expand All @@ -30,6 +30,31 @@ use {
mod deserialize_ordinal_from_str;
mod templates;

enum ServerError {
InternalError(Error),
NotFound(String),
}

impl IntoResponse for ServerError {
fn into_response(self) -> Response {
match self {
Self::InternalError(error) => {
eprintln!("error serving request: {error}");
(
StatusCode::INTERNAL_SERVER_ERROR,
Html(
StatusCode::INTERNAL_SERVER_ERROR
.canonical_reason()
.unwrap_or_default(),
),
)
.into_response()
}
Self::NotFound(message) => (StatusCode::NOT_FOUND, Html(message)).into_response(),
}
}
}

#[derive(Deserialize)]
struct Search {
query: String,
Expand Down Expand Up @@ -284,24 +309,31 @@ impl Server {
async fn output(
index: extract::Extension<Arc<Index>>,
extract::Path(outpoint): extract::Path<OutPoint>,
) -> impl IntoResponse {
match index.list(outpoint) {
Ok(Some(list)) => OutputHtml { outpoint, list }.page().into_response(),
Ok(None) => (StatusCode::NOT_FOUND, Html("Output unknown.".to_string())).into_response(),
Err(err) => {
eprintln!("Error serving request for output: {err}");
(
StatusCode::INTERNAL_SERVER_ERROR,
Html(
StatusCode::INTERNAL_SERVER_ERROR
.canonical_reason()
.unwrap_or_default()
.to_string(),
),
)
.into_response()
network: extract::Extension<Network>,
) -> Result<PageHtml, ServerError> {
let list = index
.list(outpoint)
.map_err(ServerError::InternalError)?
.ok_or_else(|| ServerError::NotFound(format!("Output {outpoint} unknown")))?;

let output = index
.transaction(outpoint.txid)
.map_err(ServerError::InternalError)?
.ok_or_else(|| ServerError::NotFound(format!("Output {outpoint} unknown")))?
.output
.into_iter()
.nth(outpoint.vout as usize)
.ok_or_else(|| ServerError::NotFound(format!("Output {outpoint} unknown")))?;

Ok(
OutputHtml {
outpoint,
list,
network: network.0,
output,
}
}
.page(),
)
}

async fn range(
Expand Down Expand Up @@ -343,8 +375,39 @@ impl Server {
extract::Path(hash): extract::Path<BlockHash>,
index: extract::Extension<Arc<Index>>,
) -> impl IntoResponse {
let info = match index.block_header_info(hash) {
Ok(Some(info)) => info,
Ok(None) => {
return (
StatusCode::NOT_FOUND,
Html(
StatusCode::NOT_FOUND
.canonical_reason()
.unwrap_or_default()
.to_string(),
),
)
.into_response()
}
Err(error) => {
eprintln!("Error serving request for block with hash {hash}: {error}");
return (
StatusCode::INTERNAL_SERVER_ERROR,
Html(
StatusCode::INTERNAL_SERVER_ERROR
.canonical_reason()
.unwrap_or_default()
.to_string(),
),
)
.into_response();
}
};

match index.block_with_hash(hash) {
Ok(Some(block)) => BlockHtml::new(block).page().into_response(),
Ok(Some(block)) => BlockHtml::new(block, Height(info.height as u64))
.page()
.into_response(),
Ok(None) => (
StatusCode::NOT_FOUND,
Html(
Expand Down Expand Up @@ -915,7 +978,7 @@ mod tests {
StatusCode::OK,
r".*<title>Ordinal range \[0,1\)</title>.*<h1>Ordinal range \[0,1\)</h1>
<dl>
<dt>size</dt><dd>1</dd>
<dt>value</dt><dd>1</dd>
<dt>first</dt><dd><a href=/ordinal/0 class=mythic>0</a></dd>
</dl>.*",
);
Expand Down Expand Up @@ -987,9 +1050,10 @@ mod tests {
StatusCode::OK,
".*<title>Output 4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0</title>.*<h1>Output 4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0</h1>
<dl>
<dt>size</dt><dd>5000000000</dd>
<dt>value</dt><dd>5000000000</dd>
<dt>script pubkey</dt><dd>OP_PUSHBYTES_65 04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f OP_CHECKSIG</dd>
</dl>
<h2>Ordinal Ranges</h2>
<h2>1 Ordinal Range</h2>
<ul class=monospace>
<li><a href=/range/0/5000000000 class=mythic>\\[0,5000000000\\)</a></li>
</ul>.*",
Expand All @@ -1001,7 +1065,7 @@ mod tests {
TestServer::new().assert_response(
"/output/0000000000000000000000000000000000000000000000000000000000000000:0",
StatusCode::NOT_FOUND,
"Output unknown.",
"Output 0000000000000000000000000000000000000000000000000000000000000000:0 unknown",
);
}

Expand Down Expand Up @@ -1115,7 +1179,14 @@ mod tests {
&format!("/block/{block_hash}"),
StatusCode::OK,
".*<h1>Block [[:xdigit:]]{64}</h1>
<h2>Transactions</h2>
<dl>
<dt>height</dt><dd>2</dd>
<dt>timestamp</dt><dd>0</dd>
<dt>size</dt><dd>203</dd>
<dt>weight</dt><dd>812</dd>
<dt>prev blockhash</dt><dd><a href=/block/659f9b67fbc0b5cba0ef6ebc0aea322e1c246e29e43210bd581f5f3bd36d17bf>659f9b67fbc0b5cba0ef6ebc0aea322e1c246e29e43210bd581f5f3bd36d17bf</a></dd>
</dl>
<h2>2 Transactions</h2>
<ul class=monospace>
<li><a href=/tx/[[:xdigit:]]{64}>[[:xdigit:]]{64}</a></li>
<li><a href=/tx/[[:xdigit:]]{64}>[[:xdigit:]]{64}</a></li>
Expand All @@ -1135,7 +1206,7 @@ mod tests {
StatusCode::OK,
&format!(
".*<title>Transaction {txid}</title>.*<h1>Transaction {txid}</h1>
<h2>Outputs</h2>
<h2>1 Output</h2>
<ul class=monospace>
<li>
<a href=/output/9068a11b8769174363376b606af9a4b8b29dd7b13d013f4b0cbbd457db3c3ce5:0>
Expand Down
19 changes: 14 additions & 5 deletions src/subcommand/server/templates/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ use super::*;
#[derive(Boilerplate)]
pub(crate) struct BlockHtml {
hash: BlockHash,
txids: Vec<Txid>,
block: Block,
height: Height,
}

impl BlockHtml {
pub(crate) fn new(block: Block) -> Self {
pub(crate) fn new(block: Block, height: Height) -> Self {
Self {
hash: block.header.block_hash(),
txids: block.txdata.iter().map(Transaction::txid).collect(),
block,
height,
}
}
}
Expand All @@ -30,11 +32,18 @@ mod tests {
assert_eq!(
BlockHtml::new(bitcoin::blockdata::constants::genesis_block(
Network::Bitcoin
))
), Height(0))
.to_string(),
"
<h1>Block 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f</h1>
<h2>Transactions</h2>
<dl>
<dt>height</dt><dd>0</dd>
<dt>timestamp</dt><dd>1231006505</dd>
<dt>size</dt><dd>285</dd>
<dt>weight</dt><dd>1140</dd>
<dt>prev blockhash</dt><dd><a href=/block/0000000000000000000000000000000000000000000000000000000000000000>0000000000000000000000000000000000000000000000000000000000000000</a></dd>
</dl>
<h2>1 Transaction</h2>
<ul class=monospace>
<li><a href=/tx/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b>4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b</a></li>
</ul>
Expand Down
31 changes: 27 additions & 4 deletions src/subcommand/server/templates/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use super::*;
pub(crate) struct OutputHtml {
pub(crate) outpoint: OutPoint,
pub(crate) list: List,
pub(crate) network: Network,
pub(crate) output: TxOut,
}

impl Content for OutputHtml {
Expand All @@ -14,7 +16,12 @@ impl Content for OutputHtml {

#[cfg(test)]
mod tests {
use {super::*, pretty_assertions::assert_eq, unindent::Unindent};
use {
super::*,
bitcoin::{blockdata::script, PubkeyHash, Script},
pretty_assertions::assert_eq,
unindent::Unindent,
};

#[test]
fn unspent_output() {
Expand All @@ -23,15 +30,22 @@ mod tests {
outpoint: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0"
.parse()
.unwrap(),
list: List::Unspent(vec![(0, 1), (1, 2)])
list: List::Unspent(vec![(0, 1), (1, 2)]),
network: Network::Bitcoin,
output: TxOut {
value: 2,
script_pubkey: Script::new_p2pkh(&PubkeyHash::all_zeros()),
},
}
.to_string(),
"
<h1>Output 4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0</h1>
<dl>
<dt>size</dt><dd>2</dd>
<dt>value</dt><dd>2</dd>
<dt>script pubkey</dt><dd>OP_DUP OP_HASH160 OP_PUSHBYTES_20 0000000000000000000000000000000000000000 OP_EQUALVERIFY OP_CHECKSIG</dd>
<dt>address</dt><dd>1111111111111111111114oLvT2</dd>
</dl>
<h2>Ordinal Ranges</h2>
<h2>2 Ordinal Ranges</h2>
<ul class=monospace>
<li><a href=/range/0/1 class=mythic>[0,1)</a></li>
<li><a href=/range/1/2 class=common>[1,2)</a></li>
Expand All @@ -49,10 +63,19 @@ mod tests {
.parse()
.unwrap(),
list: List::Spent,
network: Network::Bitcoin,
output: TxOut {
value: 1,
script_pubkey: script::Builder::new().push_scriptint(0).into_script(),
},
}
.to_string(),
"
<h1>Output 0000000000000000000000000000000000000000000000000000000000000000:0</h1>
<dl>
<dt>value</dt><dd>1</dd>
<dt>script pubkey</dt><dd>OP_0</dd>
</dl>
<p>Output has been spent.</p>
"
.unindent()
Expand Down
Loading