Add pagination to single block view
Diff
frontend/package-lock.json | 27 +++++++++++++++++++++++++++
frontend/package.json | 3 ++-
frontend/src/global.scss | 4 ++++
indexer/src/main.rs | 2 +-
frontend/src/lib/bitcoinScript.ts | 16 +++++++++++++++-
frontend/src/routes/index.svelte | 3 ++-
web-api/src/database/blocks.rs | 5 ++++-
web-api/src/database/transactions.rs | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
web-api/src/methods/block.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++-------
frontend/src/routes/block/[id].svelte | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
10 files changed, 250 insertions(+), 85 deletions(-)
@@ -9,6 +9,7 @@
"version": "0.0.1",
"dependencies": {
"buffer": "^6.0.3",
"svelte-time": "^0.7.0",
"sveltekit-i18n": "^2.2.1"
},
"devDependencies": {
@@ -871,6 +872,11 @@
"node": ">=4"
}
},
"node_modules/dayjs": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz",
"integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw=="
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -2888,6 +2894,14 @@
"dev": true,
"dependencies": {
"sourcemap-codec": "^1.4.8"
}
},
"node_modules/svelte-time": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/svelte-time/-/svelte-time-0.7.0.tgz",
"integrity": "sha512-AJFAJInN+Kp0HmOKOMATlSV4A7/j0psmGpaV6KDfYDTBszi0E3p/VOYiayfZ/krTl0CZqBHKRLGPCB7XdAqG9A==",
"dependencies": {
"dayjs": "^1.10.7"
}
},
"node_modules/sveltekit-i18n": {
@@ -3725,6 +3739,11 @@
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true
},
"dayjs": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz",
"integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw=="
},
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -5034,6 +5053,14 @@
"sourcemap-codec": "^1.4.8"
}
}
}
},
"svelte-time": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/svelte-time/-/svelte-time-0.7.0.tgz",
"integrity": "sha512-AJFAJInN+Kp0HmOKOMATlSV4A7/j0psmGpaV6KDfYDTBszi0E3p/VOYiayfZ/krTl0CZqBHKRLGPCB7XdAqG9A==",
"requires": {
"dayjs": "^1.10.7"
}
},
"sveltekit-i18n": {
@@ -35,6 +35,7 @@
"type": "module",
"dependencies": {
"buffer": "^6.0.3",
"sveltekit-i18n": "^2.2.1"
"sveltekit-i18n": "^2.2.1",
"svelte-time": "^0.7.0"
}
}
@@ -6,6 +6,10 @@
@apply bg-gray-900;
}
time {
@apply decoration-dashed underline cursor-help;
}
a {
@apply text-sky-400 transition-all;
@@ -46,7 +46,7 @@
let height = bitcoin_rpc.get_block_count().await?;
eprintln!("Current block height: {}", height);
let start = 0;
let start = 737000;
let (tx, mut rx) = tokio::sync::mpsc::channel::<(u64, BlockHash, Block)>(200);
@@ -15,7 +15,7 @@
}
if (byte >= 0x52 && byte <= 0x60) {
out.push(byte-0x50)
out.push(byte - 0x50)
continue;
}
@@ -28,6 +28,20 @@
}
return out;
}
export function briefHexToAsm(hex: string) {
const asm = hexToAsm(hex);
const OP_RETURN = Operation[Operation.OP_RETURN];
for (const op of asm) {
if (op === OP_RETURN) {
return [OP_RETURN];
}
}
return asm;
}
enum Operation {
@@ -18,6 +18,7 @@
<script>
import { t as _ } from "$lib/i18n";
import Time from "svelte-time";
// TODO: needs loader
export let blocks = [];
@@ -78,7 +79,7 @@
{#each blocks as block}
<tr>
<th><a href={`/block/${block.height}`}>{block.height}</a></th>
<td>{block.timestamp}</td>
<td><Time live relative timestamp={block.timestamp} /></td>
<td>{block.tx_count}</td>
<td>{block.bits}</td>
<td>{block.weight}</td>
@@ -43,7 +43,10 @@
pub type TransactionCount = i64;
pub async fn fetch_latest_blocks(db: &Connection, count: i64) -> Result<Vec<(Block, TransactionCount)>> {
pub async fn fetch_latest_blocks(
db: &Connection,
count: i64,
) -> Result<Vec<(Block, TransactionCount)>> {
let blocks = db
.query(
"SELECT blocks.*, COUNT(transactions.id) AS tx_count
@@ -1,8 +1,8 @@
use crate::database::{Connection, Result};
use serde::{Deserialize, Deserializer};
use serde::de::Error;
use serde::{Deserialize, Deserializer};
use tokio::time::Instant;
use tokio_postgres::types::Json;
use tokio_postgres::types::{Json, ToSql};
use tokio_postgres::Row;
#[derive(Debug)]
@@ -48,42 +48,66 @@
pub address: Option<String>,
}
fn trim_hex_prefix<'de, D: Deserializer<'de>>(deserializer: D) -> std::result::Result<String, D::Error> {
fn trim_hex_prefix<'de, D: Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<String, D::Error> {
let mut s = String::deserialize(deserializer)?;
s.remove(0);
s.remove(0);
Ok(s)
}
pub async fn fetch_transactions_for_block(
db: &Connection,
id: i64,
limit: i64,
offset: i64,
) -> Result<(i64, Vec<Transaction>)> {
let count_query = "
SELECT COUNT(*) AS count
FROM transactions
WHERE transactions.block_id = $1
";
let count_query_params: &[&(dyn ToSql + Sync)] = &[&id];
let select_query = "
SELECT
transactions.*,
(
SELECT JSON_AGG(transaction_inputs)
FROM (
SELECT ROW_TO_JSON(transaction_outputs) AS previous_output_tx, transaction_inputs.*
FROM transaction_inputs
LEFT JOIN transaction_outputs
ON transaction_outputs.id = transaction_inputs.previous_output
WHERE transactions.id = transaction_inputs.transaction_id
) transaction_inputs
) AS inputs,
(
SELECT JSON_AGG(transaction_outputs.*)
FROM transaction_outputs
WHERE transactions.id = transaction_outputs.transaction_id
) AS outputs
FROM transactions
WHERE transactions.block_id = $1
GROUP BY transactions.id
ORDER BY transactions.id ASC
LIMIT $2 OFFSET $3
";
let select_query_params: &[&(dyn ToSql + Sync)] = &[&id, &limit, &offset];
let (count, transactions) = tokio::try_join!(
db.query_one(count_query, count_query_params),
db.query(select_query, select_query_params)
)?;
pub async fn fetch_transactions_for_block(db: &Connection, id: i64) -> Result<Vec<Transaction>> {
let transactions = db
.query(
"SELECT
transactions.*,
JSON_AGG(transaction_inputs) AS inputs,
JSON_AGG(transaction_outputs) AS outputs
FROM transactions
LEFT JOIN
(
SELECT
row_to_json(transaction_outputs) AS previous_output_tx,
transaction_inputs.*
FROM transaction_inputs
LEFT JOIN transaction_outputs
ON transaction_outputs.id = transaction_inputs.previous_output
) transaction_inputs
ON transactions.id = transaction_inputs.transaction_id
LEFT JOIN transaction_outputs
ON transactions.id = transaction_outputs.transaction_id
WHERE transactions.block_id = $1
GROUP BY transactions.id
ORDER BY transactions.id ASC",
&[&id],
)
.await?;
transactions
.into_iter()
.map(Transaction::from_row)
.collect()
Ok((
count.try_get("count")?,
transactions
.into_iter()
.map(Transaction::from_row)
.collect::<Result<_>>()?,
))
}
@@ -1,9 +1,10 @@
use std::ptr::hash;
use crate::Database;
use axum::extract::Path;
use axum::extract::{Path, Query};
use axum::{Extension, Json};
use chrono::NaiveDateTime;
use serde::Serialize;
use futures::StreamExt;
use serde::{Deserialize, Serialize};
use std::ptr::hash;
#[derive(Serialize)]
pub struct BlockList {
@@ -20,10 +21,13 @@
pub async fn list(Extension(database): Extension<Database>) -> Json<Vec<BlockList>> {
let database = database.get().await.unwrap();
let blocks = crate::database::blocks::fetch_latest_blocks(&database, 5).await.unwrap();
let blocks = crate::database::blocks::fetch_latest_blocks(&database, 5)
.await
.unwrap();
Json(
blocks.into_iter()
blocks
.into_iter()
.map(|(mut block, tx_count)| {
block.hash.reverse();
@@ -36,14 +40,21 @@
bits: block.bits,
nonce: block.nonce,
difficulty: block.difficulty,
tx_count
tx_count,
}
})
.collect()
.collect(),
)
}
#[derive(Serialize)]
pub struct GetResponse {
tx_count: i64,
#[serde(flatten)]
block: Block,
}
#[derive(Serialize)]
pub struct Block {
height: i64,
version: i32,
@@ -102,28 +113,38 @@
address: txo.address,
}
}
}
#[derive(Deserialize)]
pub struct HandleQuery {
#[serde(default)]
offset: u32,
}
pub async fn handle(
Extension(database): Extension<Database>,
Path(height): Path<i64>,
) -> Json<Block> {
Query(query): Query<HandleQuery>,
) -> Json<GetResponse> {
let database = database.get().await.unwrap();
let offset = i64::from(query.offset);
let limit = 30;
let mut block = crate::database::blocks::fetch_block_by_height(&database, height)
.await
.unwrap()
.unwrap();
let transactions =
crate::database::transactions::fetch_transactions_for_block(&database, block.id)
.await
.unwrap();
let (count, transactions) = crate::database::transactions::fetch_transactions_for_block(
&database, block.id, limit, offset,
)
.await
.unwrap();
block.hash.reverse();
Json(Block {
let block = Block {
hash: hex::encode(block.hash),
height: block.height,
version: block.version,
@@ -146,5 +167,10 @@
outputs: tx.outputs.0.into_iter().map(Into::into).collect(),
})
.collect(),
};
Json(GetResponse {
tx_count: count,
block,
})
}
@@ -1,11 +1,23 @@
<script context="module">
export async function load({ fetch, params }) {
let res = await fetch(`http://localhost:3001/block/${params.id}`);
export async function load({ fetch, params, url }) {
const offset = Math.max(0, Number.parseInt(url.searchParams.get('offset') || '0'));
let res = await fetch(`http://localhost:3001/block/${params.id}?offset=${offset}`);
if (res.ok) {
const block = await res.json();
if (offset >= block.tx_count) {
return {
status: 404,
error: new Error("Offset exceeds the transaction count in this block")
};
}
return {
props: {
block: await res.json()
block,
currentPage: Math.floor(offset / 30),
}
};
}
@@ -17,15 +29,12 @@
</script>
<script>
import { hexToAsm } from "$lib/bitcoinScript";
import { briefHexToAsm } from "$lib/bitcoinScript";
export let block = {};
export let currentPage = 0;
for (let transaction of block.transactions) {
for (let output of transaction.outputs) {
console.log(hexToAsm(output.script));
}
}
const scale = Math.pow(10, 8);
</script>
<div>
@@ -50,52 +59,108 @@
</section>
<section class="!bg-transparent">
<h3 class="text-white text-2xl">{block.transactions.length} Transactions</h3>
<h3 class="text-white text-2xl">{block.tx_count} Transaction{block.tx_count > 1 ? 's' : ''}</h3>
</section>
{#each block.transactions as transaction}
<section>
<h3 class="text-lg m-2">{transaction.hash}</h3>
<div class="flex">
<table>
<tbody>
<section class="p-4">
<h3 class="text-lg m-2" id={transaction.hash}>
<a href={`#${transaction.hash}`}>§</a>
{transaction.hash}
</h3>
<div class="table table-fixed w-full">
<div class="table-cell break-all">
{#if transaction.coinbase}
<tr>
<td>Coinbase</td>
</tr>
<div class="item w-full">
<code>Coinbase</code>
</div>
{:else}
{#each transaction.inputs as input}
<tr>
<td>{input.previous_output?.address || hexToAsm(input.script).join('\n')}</td>
</tr>
<div class="item w-full">
<div class="flex-grow">
<code>{input.previous_output?.address || briefHexToAsm(input.script).join('\n')}</code>
</div>
{#if input.previous_output}
<div class="amount">
<code>{(input.previous_output.value / scale).toFixed(8)} BTC</code>
</div>
{/if}
</div>
{/each}
{/if}
</tbody>
</table>
</div>
<div class="text-2xl mx-4 self-center">
<div class="text-2xl table-cell w-10 align-middle text-center">
→
</div>
<table>
<tbody>
<tr>
{#each transaction.outputs as output}
<td>{output.address || hexToAsm(output.script).join('\n')}</td>
{/each}
</tbody>
</table>
<div class="table-cell break-all">
{#each transaction.outputs as output}
<div class="item w-full">
<div class="flex-grow">
<code>{output.address || briefHexToAsm(output.script).join(' ').trim() || output.script}</code>
</div>
<div class="amount">
<code>{(output.value / scale).toFixed(8)} BTC</code>
</div>
</div>
{/each}
</div>
</div>
</section>
{/each}
<div class="pagination">
{#each { length: Math.ceil(block.tx_count / 30) } as _, i}
<a href="/block/{block.height}{i === 0 ? '' : `?offset=${i * 30}`}" class:active={i === currentPage}>{i + 1}</a>
{/each}
</div>
</div>
<style lang="scss">
@import "../../_section.scss";
@import "../../_table.scss";
.pagination {
@apply m-auto text-center my-7;
max-width: 90rem;
a {
@apply inline-block p-3 m-1 bg-gray-800 text-white rounded-lg;
&.active {
@apply bg-orange-400;
}
}
}
section {
@apply text-xs;
}
.table-cell {
counter-reset: inout;
}
.amount {
@apply whitespace-nowrap ml-4;
}
div.item {
@apply bg-gray-900/40 p-4 rounded-lg flex mb-2;
counter-increment: inout;
&:last-of-type {
@apply mb-0;
}
&::before {
@apply inline-block w-6 mr-2 select-none text-zinc-500;
content: counter(inout);
}
}
</style>