Hook up transfers, show mining pool, show script messages & QOL improvements
Diff
indexer/src/main.rs | 71 +++++++++++++++++++++++++++--------------------------------------------
migrations/up/V1__initial_schema.sql | 2 ++
frontend/src/lib/AsmScript.svelte | 21 +++++++++++++++++++++
frontend/src/lib/Blocks.svelte | 48 ++++++++++++++++++++++++++++++++++++++++++++++++
frontend/src/lib/Time.svelte | 50 +++++++++++++++++++++++++-------------------------
frontend/src/lib/Transaction.svelte | 243 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
frontend/src/lib/TransactionInputInfo.svelte | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
frontend/src/lib/TransactionOutputInfo.svelte | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
frontend/src/lib/Transactions.svelte | 31 +++++++++++++++++++++++++++++++
frontend/src/lib/bitcoinScript.ts | 374 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
frontend/src/lib/dayjs.ts | 4 ++--
frontend/src/lib/i18n.ts | 22 ++++++++++------------
frontend/src/lib/store.ts | 19 +++++++++++++++++++
frontend/src/routes/__layout.svelte | 4 ++--
frontend/src/routes/index.svelte | 113 ++++++++++++++++++--------------------------------------------------------------
web-api/src/database/blocks.rs | 23 ++++++++++++++++++-----
web-api/src/database/transactions.rs | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
web-api/src/methods/address.rs | 20 +-------------------
web-api/src/methods/block.rs | 175 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
web-api/src/methods/mod.rs | 3 +++
web-api/src/methods/transaction.rs | 45 +++++++++++++++++++++++++++++++++++++++++++++
frontend/src/lib/i18n/en.json | 1 +
frontend/src/routes/address/[address].svelte | 56 +++++++++++++++++++++++++++++---------------------------
frontend/src/routes/block/[id].svelte | 17 ++++++++++-------
frontend/src/routes/block/index.svelte | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
frontend/src/routes/tx/[id].svelte | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
frontend/src/routes/tx/index.svelte | 40 +++++++++++++++++++++++++++++++++++++---
27 files changed, 1284 insertions(+), 495 deletions(-)
@@ -188,16 +188,12 @@
transaction: &Transaction,
) -> Result<i64, Box<dyn std::error::Error>> {
let query = "
WITH inserted AS (
INSERT INTO transactions
(hash, block_id, version, lock_time, weight, coinbase, replace_by_fee)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT DO NOTHING
RETURNING id
) SELECT COALESCE(
(SELECT id FROM inserted),
(SELECT id FROM transactions WHERE hash = $1)
) AS id
INSERT INTO transactions
(hash, block_id, version, lock_time, weight, coinbase, replace_by_fee)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (hash) DO UPDATE
SET block_id = excluded.block_id
RETURNING id
";
Ok(tx
@@ -223,17 +219,24 @@
transaction_id: i64,
transaction_input: &TxIn,
) -> Result<(), Box<dyn std::error::Error>> {
let previous_output = select_transaction_output(
tx,
&transaction_input.previous_output.txid.to_vec(),
transaction_input.previous_output.vout as i64,
)
.await?;
let query = "
INSERT INTO transaction_inputs
(transaction_id, index, previous_output, script)
VALUES ($1, $2, $3, $4)
(transaction_id, index, sequence, witness, script, previous_output)
VALUES (
$1,
$2,
$3,
$4,
$5,
(
SELECT transaction_outputs.id
FROM transactions
INNER JOIN transaction_outputs
ON transactions.id = transaction_outputs.transaction_id
WHERE transactions.hash = $6
AND transaction_outputs.index = $7
)
)
ON CONFLICT DO NOTHING
";
@@ -242,8 +245,11 @@
&[
&transaction_id,
&index,
&previous_output,
&i64::from(transaction_input.sequence),
&transaction_input.witness.to_vec(),
&transaction_input.script_sig.as_bytes(),
&transaction_input.previous_output.txid.to_vec(),
&(transaction_input.previous_output.vout as i64),
],
)
.await?;
@@ -279,31 +285,6 @@
.await?;
Ok(())
}
async fn select_transaction_output(
tx: &tokio_postgres::Transaction<'_>,
transaction_hash: &[u8],
transaction_index: i64,
) -> Result<Option<i64>, Box<dyn std::error::Error>> {
let query = "
SELECT transaction_outputs.id AS output_id
FROM transactions
INNER JOIN transaction_outputs
ON transactions.id = transaction_outputs.transaction_id
WHERE transactions.hash = $1
AND transaction_outputs.index = $2
";
let row = tx
.query_opt(query, &[&transaction_hash, &transaction_index])
.await?;
Ok(row.map(|v| v.get("output_id")))
}
#[derive(Parser, Debug)]
@@ -56,6 +56,8 @@
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
transaction_id BIGINT NOT NULL,
index BIGINT NOT NULL,
sequence BIGINT NOT NULL,
witness BYTEA[] NOT NULL,
previous_output BIGINT,
script BYTEA NOT NULL,
CONSTRAINT fk_transaction_id
@@ -1,0 +1,21 @@
<script>
import { hexToAsm, fromHex, getScriptType } from "./bitcoinScript";
export let asm;
</script>
<code>
{#each asm as opcode}
{#if opcode.startsWith("OP_")}
<span class="opcode">{` ${opcode} `}</span>
{:else}
<span>{` ${opcode} `}</span>
{/if}
{/each}
</code>
<style lang="scss">
.opcode {
@apply text-blue-200;
}
</style>
@@ -1,0 +1,48 @@
<script>
import { t as _ } from "$lib/i18n";
import { relativeTimestamps, toggleTimestamps } from "./store";
import Time from "./Time.svelte";
export let blocks;
</script>
<div class="table-responsive">
<table class="md:!table-fixed">
<thead>
<tr>
<th>{$_("home.latest_blocks.table.height")}</th>
<th>{$_("home.latest_blocks.table.timestamp")}</th>
<th>{$_("home.latest_blocks.table.pool")}</th>
<th>{$_("home.latest_blocks.table.txns")}</th>
<th>{$_("home.latest_blocks.table.size")}</th>
<th>{$_("home.latest_blocks.table.weight")}</th>
</tr>
</thead>
<tbody>
{#each blocks as block}
<tr>
<th><a href={`/block/${block.height}`}>{block.height}</a></th>
<td>
<span on:click={toggleTimestamps}>
<Time live relative={$relativeTimestamps} timestamp={block.timestamp} />
</span>
</td>
<td>{block.mined_by?.pool || "Unknown"}</td>
<td>{block.tx_count}</td>
<td>{block.bits}</td>
<td>{block.weight}</td>
</tr>
{/each}
</tbody>
</table>
</div>
<style lang="scss">
@import "../_table.scss";
section {
@apply text-xs;
}
</style>
@@ -1,34 +1,34 @@
<script>
import { dayjs } from "./dayjs";
import { onMount } from "svelte";
import { dayjs } from "./dayjs";
import { onMount } from "svelte";
export let timestamp = new Date().toISOString();
export let format = "LLL";
export let relative = false;
export let live = false;
export let formatted = "";
export let timestamp = new Date().toISOString();
export let format = "LLL";
export let relative = false;
export let live = false;
export let formatted = "";
let interval = undefined;
let interval = undefined;
const DEFAULT_INTERVAL = 60 * 1000;
const DEFAULT_INTERVAL = 60 * 1000;
onMount(() => {
if (relative && live !== false) {
interval = setInterval(() => {
formatted = dayjs(timestamp).from();
}, Math.abs(typeof live === "number" ? live : DEFAULT_INTERVAL));
}
return () => {
if (typeof interval === "number") {
clearInterval(interval);
}
};
});
onMount(() => {
if (relative && live !== false) {
interval = setInterval(() => {
formatted = dayjs(timestamp).from();
}, Math.abs(typeof live === "number" ? live : DEFAULT_INTERVAL));
}
return () => {
if (typeof interval === "number") {
clearInterval(interval);
}
};
});
$: formatted = relative ? dayjs(timestamp).from() : dayjs(timestamp).format(format);
$: title = relative ? dayjs(timestamp).format(format) : undefined;
$: formatted = relative ? dayjs(timestamp).from() : dayjs(timestamp).format(format);
$: title = relative ? dayjs(timestamp).format(format) : undefined;
</script>
<time {...$$restProps} title={title} datetime={timestamp}>
{formatted}
<time {...$$restProps} {title} datetime={timestamp}>
{formatted}
</time>
@@ -1,97 +1,210 @@
<script>
import {briefHexToAsm} from "./bitcoinScript";
import AsmScript from "./AsmScript.svelte";
import { fromHex, getScriptType } from "./bitcoinScript";
import TransactionInputInfo from "./TransactionInputInfo.svelte";
import TransactionOutputInfo from "./TransactionOutputInfo.svelte";
import { browser } from "$app/env";
import { page } from "$app/stores";
export let transaction;
const scale = Math.pow(10, 8);
export let transaction;
export let showingMoreInfo = !browser; // default to showing more info for non-js users
export let highlight = false;
export let attachAnchor = false;
export let showTxHeader = true;
let clazz = "";
const scale = Math.pow(10, 8);
export { clazz as class };
</script>
<section class="p-4">
<h3 class="text-base md:text-lg m-2 mb-4 md:mb-2 break-all" id={transaction.hash}>
<a href={`#${transaction.hash}`}>§</a>
{transaction.hash}
</h3>
<div class="flex flex-col md:table table-fixed w-full">
<div class="table-cell break-all">
{#if transaction.coinbase}
<div class="item w-full">
<code>Coinbase</code>
</div>
{:else}
{#each transaction.inputs as input}
<div class="item w-full">
<div class="item-inner">
<div class="flex-grow">
{#if input.previous_output?.address}
<a href="/address/{input.previous_output?.address}">
<code>{input.previous_output?.address}</code>
</a>
{:else}
<code>{briefHexToAsm(input.script).join('\n') || 'WITNESS (TODO)'}</code>
{/if}
</div>
{#if input.previous_output}
<div class="amount">
<code>{(input.previous_output.value / scale).toFixed(8)} BTC</code>
</div>
{/if}
</div>
</div>
{/each}
{/if}
</div>
<section class="p-4 {clazz}">
{#if showTxHeader}
<div class="flex">
<h3 class="text-base md:text-lg m-2 mb-4 md:mb-2 break-all flex-grow" id={transaction.hash}>
<a href="#{transaction.hash}">§</a>
<a href="/tx/{transaction.hash}">{transaction.hash}</a>
</h3>
<div class="hidden md:table-cell text-2xl w-10 align-middle text-center">
→
</div>
<button
on:click={() => (showingMoreInfo = !showingMoreInfo)}
class="!text-white cursor-pointer text-base md:text-lg"
>
{showingMoreInfo ? "-" : "+"}
</button>
</div>
{/if}
<div class="block md:hidden text-center text-2xl m-2 w-full align-middle text-center">
↓
</div>
<div class="flex flex-col md:table table-fixed w-full">
<div class="table-cell break-all">
{#if transaction.coinbase}
<div class="item-outer" class:expanded={showingMoreInfo}>
<div class="item w-full">
<div class="item-inner">
<code>Coinbase</code>
</div>
</div>
<div class="table-cell break-all">
{#each transaction.outputs as output}
<div class="item w-full">
<div class="item-inner">
<div class="flex-grow">
{#if output.address}
<a href="/address/{output.address}">
<code>{output.address}</code>
</a>
{:else}
<code>{briefHexToAsm(output.script).join(' ').trim() || output.script}</code>
{/if}
</div>
<div class="amount">
<code>{(output.value / scale).toFixed(8)} BTC</code>
</div>
</div>
<div class="item-info">
<div class="table-responsive">
<TransactionInputInfo input={transaction.inputs[0]} showMessage={true} />
</div>
</div>
</div>
{:else}
{#each transaction.inputs as input}
<div class="item-outer" class:expanded={showingMoreInfo}>
<div class="item w-full">
<div class="item-inner">
<div class="flex-grow">
{#if highlight && input.previous_output?.address === highlight}
<span class="active"><code>{input.previous_output?.address}</code></span>
{:else if input.previous_output?.address}
<a href="/address/{input.previous_output?.address}">
<code>{input.previous_output?.address}</code>
</a>
{:else if input.previous_output?.script}
<code>{getScriptType(fromHex(input.previous_output?.script))}</code>
{:else if input.previous_output}
<a
href="/tx/{input.previous_output.tx_hash}#output-{input.previous_output
.tx_index}"
>
{input.previous_output.tx_hash}:{input.previous_output.tx_index}
</a>
{:else}
Unknown
{/if}
</div>
{/each}
{#if input.previous_output}
<div class="amount">
<code>{(input.previous_output.value / scale).toFixed(8)} BTC</code>
</div>
{/if}
</div>
</div>
<div class="item-info">
<div class="table-responsive">
<TransactionInputInfo {input} />
</div>
</div>
</div>
{/each}
{/if}
</div>
<div class="hidden md:table-cell text-2xl w-10 align-middle text-center">→</div>
<div class="block md:hidden text-center text-2xl m-2 w-full align-middle text-center">↓</div>
<div class="table-cell break-all">
{#each transaction.outputs as output}
<div
class="item-outer highlight-target"
id={attachAnchor ? `output-${output.index}` : undefined}
class:anchored={$page.url.hash === `#output-${output.index}`}
class:expanded={showingMoreInfo}
>
<div class="item w-full">
<div class="item-inner">
<div class="flex-grow">
{#if highlight && output.address === highlight}
<span class="active"><code>{output.address}</code></span>
{:else if output.address}
<a href="/address/{output.address}">
<code>{output.address}</code>
</a>
{:else}
{getScriptType(fromHex(output.script)) || "Unknown"}
{/if}
</div>
<div class="amount">
<code>{(output.value / scale).toFixed(8)} BTC</code>
</div>
</div>
</div>
<div class="item-info">
<div class="table-responsive">
<TransactionOutputInfo {output} />
</div>
</div>
</div>
{/each}
</div>
</div>
</section>
<style lang="scss">
@import "../src/_section.scss";
@import "../src/_table.scss";
section {
@apply text-xs;
}
.active {
@apply text-orange-400;
}
.table-cell {
counter-reset: inout;
}
.amount {
@apply whitespace-nowrap ml-0 mt-2 md:mt-0 md:ml-4;
}
.item-outer {
@apply rounded-lg border-2 border-gray-900/40 mb-2;
&:target,
&.anchored {
a {
@apply text-orange-400;
}
}
}
.item-info {
@apply hidden;
.expanded & {
@apply block;
}
table {
tr {
td:first-of-type {
@apply whitespace-nowrap;
}
td,
th {
@apply border-b border-gray-900/40 p-4 pl-8 text-left font-normal;
}
&:hover {
td,
th {
background: transparent !important;
}
}
}
}
}
div.item {
@apply bg-gray-900/40 p-4 rounded-lg flex flex mb-2;
@apply rounded-sm bg-gray-900/40 p-4 flex flex;
counter-increment: inout;
.expanded & {
@apply border-b-2 border-transparent rounded-b-lg;
}
&:last-of-type {
@apply mb-0;
@@ -1,0 +1,80 @@
<script>
import AsmScript from "./AsmScript.svelte";
import { fromHex, hexToAsm, takeScriptMessage } from "./bitcoinScript";
export let input;
export let showMessage = false;
const bytes = fromHex(input.script);
const asm = hexToAsm(bytes);
const message = showMessage ? bytes.toString("ascii") : null;
</script>
<table class="tii">
<tbody>
{#if input.script}
<tr>
<td>Script (ASM)</td>
<td><AsmScript {asm} /></td>
</tr>
<tr>
<td>Script (Hex)</td>
<td><code>{input.script}</code></td>
</tr>
{/if}
{#if message}
<tr>
<td>Script Message</td>
<td><code>{message}</code></td>
</tr>
{/if}
<tr>
<td>Sequence</td>
<td><code>0x{input.sequence.toString(16).padStart(2, "0")}</code></td>
</tr>
{#if input.witness.length > 0}
<tr>
<td>Witness</td>
<td><code>{input.witness.join(" ")}</code></td>
</tr>
{/if}
{#if input.previous_output}
<tr>
<td>Previous Output</td>
<td>
<a href="/tx/{input.previous_output.tx_hash}#output-{input.previous_output.tx_index}">
{input.previous_output.tx_hash}:{input.previous_output.tx_index}
</a>
</td>
</tr>
{/if}
</tbody>
</table>
<style lang="scss">
@import "../table";
table.tii {
tr {
td:first-of-type {
@apply whitespace-nowrap;
}
td,
th {
@apply border-b border-gray-900/40 p-4 pl-8 text-left font-normal;
}
&:hover {
td,
th {
background: transparent !important;
}
}
}
}
</style>
@@ -1,0 +1,63 @@
<script>
import AsmScript from "./AsmScript.svelte";
import { fromHex, getScriptType, hexToAsm, takeScriptMessage } from "./bitcoinScript.ts";
export let output;
const bytes = fromHex(output.script);
const asm = hexToAsm(bytes);
const scriptType = getScriptType(bytes);
const message = scriptType === "OP_RETURN" ? takeScriptMessage(asm) : null;
</script>
<table class="toi">
<tbody>
{#if scriptType && scriptType !== "OP_RETURN"}
<tr>
<td>Script Type</td>
<td>{scriptType}</td>
</tr>
{/if}
<tr>
<td>Script (ASM)</td>
<td><AsmScript {asm} /></td>
</tr>
<tr>
<td>Script (Hex)</td>
<td><code>{output.script}</code></td>
</tr>
{#if message}
<tr>
<td>Script Message</td>
<td><code>{message}</code></td>
</tr>
{/if}
</tbody>
</table>
<style lang="scss">
@import "../table";
table.toi {
tr {
td:first-of-type {
@apply whitespace-nowrap;
}
td,
th {
@apply border-b border-gray-900/40 p-4 pl-8 text-left font-normal;
}
&:hover {
td,
th {
background: transparent !important;
}
}
}
}
</style>
@@ -1,0 +1,31 @@
<script>
import { t as _ } from "$lib/i18n";
export let transactions;
</script>
<table>
<thead>
<tr>
<th>{$_("home.latest_txns.table.txn_id")}</th>
<th>{$_("home.latest_txns.table.value")}</th>
<th>{$_("home.latest_txns.table.size")}</th>
<th>{$_("home.latest_txns.table.fee")}</th>
</tr>
</thead>
<tbody>
{#each transactions as txn}
<tr>
<th><a href={`/tx/${txn.hash}`}><code>{txn.hash}</code></a></th>
<td>{txn.amount} BTC</td>
<td>{txn.size} vB</td>
<td>{txn.fee} sat/vB</td>
</tr>
{/each}
</tbody>
</table>
<style lang="scss">
@import "../_table.scss";
</style>
@@ -1,164 +1,238 @@
import { Buffer } from "buffer/";
export function hexToAsm(hex: string) {
const bytes = Buffer.from(hex, 'hex');
const out = [];
for (let i = 0; i < bytes.length; i++) {
const byte = bytes[i];
if (byte < 0x02) {
out.push(byte);
continue;
}
if (byte >= 0x52 && byte <= 0x60) {
out.push(byte - 0x50)
continue;
}
if(byte >= 0x02 && byte <= 0x4b) {
out.push("OP_PUSHBYTES_" + byte);
out.push(bytes.slice(i + 1, i + 1 + byte).toString("hex"));
i += byte;
continue;
}
out.push(Operation[byte] || byte.toString(16))
export function fromHex(hex: string): Buffer {
return Buffer.from(hex, "hex");
}
export function getScriptType(bytes: Buffer): string | null {
if (isP2pk(bytes)) {
return "P2PK";
} else if (isP2pkh(bytes)) {
return "P2PKH";
} else if (isP2sh(bytes)) {
return "P2SH";
} else if (isReturn(bytes)) {
return "OP_RETURN";
} else {
return null;
}
}
export function takeScriptMessage(asmInput: string[]): string | null {
try {
let input = "";
for (let i = 0; i < asmInput.length; i++) {
const asm = asmInput[i];
if (asm.startsWith("OP_PUSHBYTES_")) {
input += asmInput[i + 1];
i++;
}
}
if (input) {
return Buffer.from(input, "hex").toString("ascii").trim();
} else {
return null;
}
} catch (e) {
return null;
}
}
export function hexToAsm(bytes: Buffer) {
const out = [];
for (let i = 0; i < bytes.length; i++) {
const byte = bytes[i];
if (byte >= 0x52 && byte <= 0x60) {
out.push((byte - 0x50).toString(16));
continue;
}
if (byte >= 0x02 && byte <= 0x4b) {
out.push("OP_PUSHBYTES_" + byte);
out.push(bytes.slice(i + 1, i + 1 + byte).toString("hex"));
i += byte;
continue;
}
return out;
out.push(Operation[byte] || byte.toString(16));
}
return out;
}
export function briefHexToAsm(hex: string) {
const asm = hexToAsm(hex);
export function briefHexToAsm(hex: Buffer) {
const asm = hexToAsm(hex);
const OP_RETURN = Operation[Operation.OP_RETURN];
const OP_RETURN = Operation[Operation.OP_RETURN];
for (const op of asm) {
if (op === OP_RETURN) {
return [OP_RETURN];
}
for (const op of asm) {
if (op === OP_RETURN) {
return [OP_RETURN];
}
}
return asm;
}
function isP2sh(bytes: Buffer): boolean {
return (
bytes.length === 23 &&
bytes[0] == Operation["OP_HASH160"] &&
bytes[1] == 0x14 &&
bytes[22] == Operation["OP_EQUAL"]
);
}
function isP2pkh(bytes: Buffer): boolean {
return (
bytes.length == 25 &&
bytes[0] == Operation["OP_DUP"] &&
bytes[1] == Operation["OP_HASH160"] &&
bytes[2] == 0x14 &&
bytes[23] == Operation["OP_EQUALVERIFY"] &&
bytes[24] == Operation["OP_CHECKSIG"]
);
}
function isP2pk(bytes: Buffer): boolean {
if (bytes.length == 67) {
return (
bytes[0] == 0x41 &&
bytes[66] == Operation["OP_CHECKSIG"]
);
} else if (bytes.length == 35) {
return (
bytes[0] == 0x21 &&
bytes[34] == Operation["OP_CHECKSIG"]
);
} else {
return false;
}
}
return asm;
function isReturn(bytes: Buffer): boolean {
return bytes.includes(Operation["OP_RETURN"]);
}
enum Operation {
OP_FALSE = 0x00,
OP_PUSHDATA1 = 0x4c,
OP_PUSHDATA2 = 0x4d,
OP_PUSHDATA4 = 0x4e,
OP_1NEGATE = 0x4f,
OP_RESERVED = 0x50,
OP_TRUE = 0x51,
OP_2 = 0x52,
OP_3 = 0x53,
OP_4 = 0x54,
OP_5 = 0x55,
OP_6 = 0x56,
OP_7 = 0x57,
OP_8 = 0x58,
OP_9 = 0x59,
OP_10 = 0x5a,
OP_11 = 0x5b,
OP_12 = 0x5c,
OP_13 = 0x5d,
OP_14 = 0x5e,
OP_15 = 0x5f,
OP_16 = 0x60,
OP_NOP = 0x61,
OP_VER = 0x62,
OP_IF = 0x63,
OP_NOTIF = 0x64,
OP_VERIF = 0x65,
OP_VERNOTIF = 0x66,
OP_ELSE = 0x67,
OP_ENDIF = 0x68,
OP_VERIFY = 0x69,
OP_RETURN = 0x6a,
OP_TOALTSTACK = 0x6b,
OP_FROMALTSTACK = 0x6c,
OP_2DROP = 0x6d,
OP_2DUP = 0x6e,
OP_3DUP = 0x6f,
OP_2OVER = 0x70,
OP_2ROT = 0x71,
OP_2SWAP = 0x72,
OP_IFDUP = 0x73,
OP_DEPTH = 0x74,
OP_DROP = 0x75,
OP_DUP = 0x76,
OP_NIP = 0x77,
OP_OVER = 0x78,
OP_PICK = 0x79,
OP_ROLL = 0x7a,
OP_ROT = 0x7b,
OP_SWAP = 0x7c,
OP_TUCK = 0x7d,
OP_CAT = 0x7e,
OP_SUBSTR = 0x7f,
OP_LEFT = 0x80,
OP_RIGHT = 0x81,
OP_SIZE = 0x82,
OP_INVERT = 0x83,
OP_AND = 0x84,
OP_OR = 0x85,
OP_XOR = 0x86,
OP_EQUAL = 0x87,
OP_EQUALVERIFY = 0x88,
OP_RESERVED1 = 0x89,
OP_RESERVED2 = 0x8a,
OP_1ADD = 0x8b,
OP_1SUB = 0x8c,
OP_2MUL = 0x8d,
OP_2DIV = 0x8e,
OP_NEGATE = 0x8f,
OP_ABS = 0x90,
OP_NOT = 0x91,
OP_0NOTEQUAL = 0x92,
OP_ADD = 0x93,
OP_SUB = 0x94,
OP_MUL = 0x95,
OP_DIV = 0x96,
OP_MOD = 0x97,
OP_LSHIFT = 0x98,
OP_RSHIFT = 0x99,
OP_BOOLAND = 0x9a,
OP_BOOLOR = 0x9b,
OP_NUMEQUAL = 0x9c,
OP_NUMEQUALVERIFY = 0x9d,
OP_NUMNOTEQUAL = 0x9e,
OP_LESSTHAN = 0x9f,
OP_GREATERTHAN = 0xa0,
OP_LESSTHANOREQUAL = 0xa1,
OP_GREATERTHANOREQUAL = 0xa2,
OP_MIN = 0xa3,
OP_MAX = 0xa4,
OP_WITHIN = 0xa5,
OP_RIPEMD160 = 0xa6,
OP_SHA1 = 0xa7,
OP_SHA256 = 0xa8,
OP_HASH160 = 0xa9,
OP_HASH256 = 0xaa,
OP_CODESEPARATOR = 0xab,
OP_CHECKSIG = 0xac,
OP_CHECKSIGVERIFY = 0xad,
OP_CHECKMULTISIG = 0xae,
OP_CHECKMULTISIGVERIFY = 0xaf,
OP_NOP1 = 0xb0,
OP_CHECKLOCKTIMEVERIFY = 0xb1,
OP_CHECKSEQUENCEVERIFY = 0xb2,
OP_NOP4 = 0xb3,
OP_NOP5 = 0xb4,
OP_NOP6 = 0xb5,
OP_NOP7 = 0xb6,
OP_NOP8 = 0xb7,
OP_NOP9 = 0xb8,
OP_NOP10 = 0xb9,
OP_PUBKEYHASH = 0xfd,
OP_PUBKEY = 0xfe,
OP_INVALIDOPCODE = 0xff,
OP_FALSE = 0x00,
OP_PUSHDATA1 = 0x4c,
OP_PUSHDATA2 = 0x4d,
OP_PUSHDATA4 = 0x4e,
OP_1NEGATE = 0x4f,
OP_RESERVED = 0x50,
OP_TRUE = 0x51,
OP_2 = 0x52,
OP_3 = 0x53,
OP_4 = 0x54,
OP_5 = 0x55,
OP_6 = 0x56,
OP_7 = 0x57,
OP_8 = 0x58,
OP_9 = 0x59,
OP_10 = 0x5a,
OP_11 = 0x5b,
OP_12 = 0x5c,
OP_13 = 0x5d,
OP_14 = 0x5e,
OP_15 = 0x5f,
OP_16 = 0x60,
OP_NOP = 0x61,
OP_VER = 0x62,
OP_IF = 0x63,
OP_NOTIF = 0x64,
OP_VERIF = 0x65,
OP_VERNOTIF = 0x66,
OP_ELSE = 0x67,
OP_ENDIF = 0x68,
OP_VERIFY = 0x69,
OP_RETURN = 0x6a,
OP_TOALTSTACK = 0x6b,
OP_FROMALTSTACK = 0x6c,
OP_2DROP = 0x6d,
OP_2DUP = 0x6e,
OP_3DUP = 0x6f,
OP_2OVER = 0x70,
OP_2ROT = 0x71,
OP_2SWAP = 0x72,
OP_IFDUP = 0x73,
OP_DEPTH = 0x74,
OP_DROP = 0x75,
OP_DUP = 0x76,
OP_NIP = 0x77,
OP_OVER = 0x78,
OP_PICK = 0x79,
OP_ROLL = 0x7a,
OP_ROT = 0x7b,
OP_SWAP = 0x7c,
OP_TUCK = 0x7d,
OP_CAT = 0x7e,
OP_SUBSTR = 0x7f,
OP_LEFT = 0x80,
OP_RIGHT = 0x81,
OP_SIZE = 0x82,
OP_INVERT = 0x83,
OP_AND = 0x84,
OP_OR = 0x85,
OP_XOR = 0x86,
OP_EQUAL = 0x87,
OP_EQUALVERIFY = 0x88,
OP_RESERVED1 = 0x89,
OP_RESERVED2 = 0x8a,
OP_1ADD = 0x8b,
OP_1SUB = 0x8c,
OP_2MUL = 0x8d,
OP_2DIV = 0x8e,
OP_NEGATE = 0x8f,
OP_ABS = 0x90,
OP_NOT = 0x91,
OP_0NOTEQUAL = 0x92,
OP_ADD = 0x93,
OP_SUB = 0x94,
OP_MUL = 0x95,
OP_DIV = 0x96,
OP_MOD = 0x97,
OP_LSHIFT = 0x98,
OP_RSHIFT = 0x99,
OP_BOOLAND = 0x9a,
OP_BOOLOR = 0x9b,
OP_NUMEQUAL = 0x9c,
OP_NUMEQUALVERIFY = 0x9d,
OP_NUMNOTEQUAL = 0x9e,
OP_LESSTHAN = 0x9f,
OP_GREATERTHAN = 0xa0,
OP_LESSTHANOREQUAL = 0xa1,
OP_GREATERTHANOREQUAL = 0xa2,
OP_MIN = 0xa3,
OP_MAX = 0xa4,
OP_WITHIN = 0xa5,
OP_RIPEMD160 = 0xa6,
OP_SHA1 = 0xa7,
OP_SHA256 = 0xa8,
OP_HASH160 = 0xa9,
OP_HASH256 = 0xaa,
OP_CODESEPARATOR = 0xab,
OP_CHECKSIG = 0xac,
OP_CHECKSIGVERIFY = 0xad,
OP_CHECKMULTISIG = 0xae,
OP_CHECKMULTISIGVERIFY = 0xaf,
OP_NOP1 = 0xb0,
OP_CHECKLOCKTIMEVERIFY = 0xb1,
OP_CHECKSEQUENCEVERIFY = 0xb2,
OP_NOP4 = 0xb3,
OP_NOP5 = 0xb4,
OP_NOP6 = 0xb5,
OP_NOP7 = 0xb6,
OP_NOP8 = 0xb7,
OP_NOP9 = 0xb8,
OP_NOP10 = 0xb9,
OP_PUBKEYHASH = 0xfd,
OP_PUBKEY = 0xfe,
OP_INVALIDOPCODE = 0xff,
}
@@ -1,10 +1,10 @@
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime.js";
import localizedFormat from "dayjs/plugin/localizedFormat.js";
import 'dayjs/locale/en-gb';
import "dayjs/locale/en-gb";
dayjs.locale('en-gb');
dayjs.locale("en-gb");
dayjs.extend(relativeTime);
dayjs.extend(localizedFormat);
@@ -1,16 +1,14 @@
import i18n from 'sveltekit-i18n';
import i18n from "sveltekit-i18n";
const config = ({
loaders: [
{
locale: 'en',
key: '',
loader: async () => (
await import('./i18n/en.json')
).default,
}
],
});
const config = {
loaders: [
{
locale: "en",
key: "",
loader: async () => (await import("./i18n/en.json")).default,
},
],
};
export const { t, locale, locales, loading, loadTranslations } = new i18n(config);
@@ -1,0 +1,19 @@
import { browser } from "$app/env";
import { writable } from "svelte/store";
let initialRelativeTimestamps;
if (browser) {
const storedRelativeTimestamps = window.localStorage.getItem("relativeTimestamps");
initialRelativeTimestamps =
storedRelativeTimestamps !== null ? storedRelativeTimestamps === "1" : true;
} else {
initialRelativeTimestamps = false;
}
export const relativeTimestamps = writable(initialRelativeTimestamps);
export const toggleTimestamps = () => relativeTimestamps.update((v) => !v);
relativeTimestamps.subscribe((v) => {
if (browser) {
window.localStorage.setItem("relativeTimestamps", v ? "1" : "0");
}
});
@@ -1,10 +1,10 @@
<script context="module">
import { locale, loadTranslations } from '$lib/i18n';
import { locale, loadTranslations } from "$lib/i18n";
export async function load({ url }) {
const { pathname } = url;
const defaultLocale = 'en'; // TODO: get from cookie, user session, ...
const defaultLocale = "en"; // TODO: get from cookie, user session, ...
const initLocale = locale.get() || defaultLocale;
await loadTranslations(initLocale, pathname);
@@ -1,17 +1,23 @@
<script context="module">
export async function load({ fetch }) {
let res = await fetch('http://localhost:3001/block');
const [blocks, txs] = await Promise.all([
fetch("http://localhost:3001/block?limit=5"),
fetch("http://localhost:3001/tx?limit=5"),
]);
if (res.ok) {
if (txs.ok && blocks.ok) {
const [blocksJson, txsJson] = await Promise.all([blocks.json(), txs.json()]);
return {
props: {
blocks: await res.json()
}
blocks: blocksJson,
transactions: txsJson,
},
};
}
return {
status: res.status,
error: new Error()
status: blocks.status !== 200 ? blocks.status : txs.status,
error: new Error(),
};
}
</script>
@@ -19,45 +25,12 @@
<script>
import { t as _ } from "$lib/i18n";
import Time from "$lib/Time.svelte";
import Blocks from "$lib/Blocks.svelte";
import Transactions from "$lib/Transactions.svelte";
// TODO: needs loader
export let blocks = [];
let relativeTimestamps = true;
const toggleTimestamps = () => relativeTimestamps = !relativeTimestamps;
let transactions = [
{
hash: "cd16563757e4504d7f204cb3af0f3f388e6118b4f91e7fc711b3afe8108f7bf2",
amount: {
value: "50.13948574",
unit: "BTC",
},
size: {
value: "141",
unit: "vB",
},
fee: {
value: "28.6",
unit: "sat/vB",
},
},
{
hash: "4a22799110919efb8fd476d3b06d21c563813bd4a523ffd50963565445ccbab2",
amount: {
value: "12.24129544",
unit: "BTC",
},
size: {
value: "201",
unit: "vB",
},
fee: {
value: "1.2",
unit: "sat/vB",
},
},
];
export let transactions = [];
</script>
<div>
@@ -68,68 +41,18 @@
</div>
<div class="table-responsive">
<table class="table-fixed md:table-auto">
<thead>
<tr>
<th>{$_("home.latest_blocks.table.height")}</th>
<th>{$_("home.latest_blocks.table.timestamp")}</th>
<th>{$_("home.latest_blocks.table.txns")}</th>
<th>{$_("home.latest_blocks.table.size")}</th>
<th>{$_("home.latest_blocks.table.weight")}</th>
</tr>
</thead>
<tbody>
{#each blocks as block}
<tr>
<th><a href={`/block/${block.height}`}>{block.height}</a></th>
<td>
<span on:click={toggleTimestamps}>
<Time
live
relative={relativeTimestamps}
timestamp={block.timestamp}
/>
</span>
</td>
<td>{block.tx_count}</td>
<td>{block.bits}</td>
<td>{block.weight}</td>
</tr>
{/each}
</tbody>
</table>
<Blocks {blocks} />
</div>
</section>
<section>
<section class="mb-2">
<div class="flex">
<h2 class="flex-grow">{$_("home.latest_txns.header")}</h2>
<a class="header-size text-white hover:text-slate-400" href="/tx">→</a>
</div>
<div class="table-responsive">
<table>
<thead>
<tr>
<th>{$_("home.latest_txns.table.txn_id")}</th>
<th>{$_("home.latest_txns.table.value")}</th>
<th>{$_("home.latest_txns.table.size")}</th>
<th>{$_("home.latest_txns.table.fee")}</th>
</tr>
</thead>
<tbody>
{#each transactions as txn}
<tr>
<th><a href={`/tx/${txn.hash}`}><pre>{txn.hash}</pre></a></th>
<td>{txn.amount.value} {txn.amount.unit}</td>
<td>{txn.size.value} {txn.size.unit}</td>
<td>{txn.fee.value} {txn.fee.unit}</td>
</tr>
{/each}
</tbody>
</table>
<Transactions {transactions} />
</div>
</section>
</div>
@@ -46,17 +46,29 @@
pub async fn fetch_latest_blocks(
db: &Connection,
count: i64,
) -> Result<Vec<(Block, TransactionCount)>> {
offset: i64,
) -> Result<Vec<(Block, TransactionCount, Vec<u8>)>> {
let blocks = db
.query(
"SELECT blocks.*, COUNT(transactions.id) AS tx_count
"SELECT
blocks.*,
COUNT(transactions.id) AS tx_count,
(
SELECT script
FROM transactions
INNER JOIN transaction_inputs
ON transaction_inputs.transaction_id = transactions.id
WHERE transactions.block_id = blocks.id
AND transactions.coinbase = true
LIMIT 1
) AS coinbase_script
FROM blocks
LEFT JOIN transactions
ON transactions.block_id = blocks.id
GROUP BY blocks.id
ORDER BY blocks.height DESC
LIMIT $1",
&[&count],
LIMIT $1 OFFSET $2",
&[&count, &offset],
)
.await?;
@@ -64,7 +76,8 @@
.into_iter()
.map(|row| {
let tx_count = row.try_get("tx_count")?;
Ok((Block::from_row(row)?, tx_count))
let coinbase_script = row.try_get("coinbase_script")?;
Ok((Block::from_row(row)?, tx_count, coinbase_script))
})
.collect::<Result<Vec<_>>>()
}
@@ -1,4 +1,5 @@
use crate::database::{Connection, Result};
use futures::StreamExt;
use serde::de::Error;
use serde::{Deserialize, Deserializer};
use tokio::time::Instant;
@@ -34,13 +35,20 @@
#[derive(Deserialize, Debug)]
pub struct TransactionInput {
pub previous_output_tx: Option<TransactionOutput>,
pub sequence: i64,
#[serde(deserialize_with = "trim_hex_prefix_vec")]
pub witness: Vec<String>,
#[serde(deserialize_with = "trim_hex_prefix")]
pub script: String,
#[serde(deserialize_with = "parse_hex_opt")]
pub previous_output_tx_hash: Option<Vec<u8>>,
#[serde(rename = "previous_output_item")]
pub previous_output: Option<TransactionOutput>,
}
#[derive(Deserialize, Debug)]
pub struct TransactionOutput {
pub index: i64,
pub value: i64,
#[serde(deserialize_with = "trim_hex_prefix")]
pub script: String,
@@ -48,6 +56,31 @@
pub address: Option<String>,
}
fn parse_hex_opt<'de, D: Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<Option<Vec<u8>>, D::Error> {
let s = <Option<String>>::deserialize(deserializer)?;
s.map(|mut s| {
s.remove(0);
s.remove(0);
hex::decode(s).map_err(D::Error::custom)
})
.transpose()
}
fn trim_hex_prefix_vec<'de, D: Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<Vec<String>, D::Error> {
let s = <Vec<String>>::deserialize(deserializer)?;
Ok(s.into_iter()
.map(|mut s| {
s.remove(0);
s.remove(0);
s
})
.collect())
}
fn trim_hex_prefix<'de, D: Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<String, D::Error> {
@@ -77,10 +110,15 @@
(
SELECT JSON_AGG(transaction_inputs)
FROM (
SELECT ROW_TO_JSON(transaction_outputs) AS previous_output_tx, transaction_inputs.*
SELECT
pot.hash AS previous_output_tx_hash,
ROW_TO_JSON(po) AS previous_output_item,
transaction_inputs.*
FROM transaction_inputs
LEFT JOIN transaction_outputs
ON transaction_outputs.id = transaction_inputs.previous_output
LEFT JOIN transaction_outputs po
ON po.id = transaction_inputs.previous_output
LEFT JOIN transactions pot
ON pot.id = po.transaction_id
WHERE transactions.id = transaction_inputs.transaction_id
) transaction_inputs
) AS inputs,
@@ -121,10 +159,15 @@
(
SELECT JSON_AGG(transaction_inputs)
FROM (
SELECT ROW_TO_JSON(transaction_outputs) AS previous_output_tx, transaction_inputs.*
SELECT
pot.hash AS previous_output_tx_hash,
ROW_TO_JSON(po) AS previous_output_item,
transaction_inputs.*
FROM transaction_inputs
LEFT JOIN transaction_outputs
ON transaction_outputs.id = transaction_inputs.previous_output
LEFT JOIN transaction_outputs po
ON po.id = transaction_inputs.previous_output
LEFT JOIN transactions pot
ON pot.id = po.transaction_id
WHERE transactions.id = transaction_inputs.transaction_id
) transaction_inputs
) AS inputs,
@@ -149,9 +192,61 @@
";
let transactions = db.query(select_query, &[&address]).await?;
transactions
.into_iter()
.map(Transaction::from_row)
.collect()
}
pub async fn fetch_latest_transactions(db: &Connection, limit: i64) -> Result<Vec<Transaction>> {
let select_query = "
SELECT *, JSON_BUILD_ARRAY() AS inputs, JSON_BUILD_ARRAY() AS outputs
FROM transactions
ORDER BY transactions.id DESC
LIMIT $1
";
let transactions = db.query(select_query, &[&limit]).await?;
transactions
.into_iter()
.map(Transaction::from_row)
.collect()
}
pub async fn fetch_transaction_by_hash(
db: &Connection,
hash: &[u8],
) -> Result<Option<Transaction>> {
let select_query = "
SELECT
transactions.*,
(
SELECT JSON_AGG(transaction_inputs)
FROM (
SELECT
pot.hash AS previous_output_tx_hash,
ROW_TO_JSON(po) AS previous_output_item,
transaction_inputs.*
FROM transaction_inputs
LEFT JOIN transaction_outputs po
ON po.id = transaction_inputs.previous_output
LEFT JOIN transactions pot
ON pot.id = po.transaction_id
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.hash = $1
";
let transaction = db.query_opt(select_query, &[&hash]).await?;
transaction.map(Transaction::from_row).transpose()
}
@@ -12,23 +12,5 @@
.await
.unwrap();
Json(
transactions
.into_iter()
.map(|mut tx| {
tx.hash.reverse();
Transaction {
hash: hex::encode(tx.hash),
version: tx.version,
lock_time: tx.lock_time,
weight: tx.weight,
coinbase: tx.coinbase,
replace_by_fee: tx.replace_by_fee,
inputs: tx.inputs.0.into_iter().map(Into::into).collect(),
outputs: tx.outputs.0.into_iter().map(Into::into).collect(),
}
})
.collect(),
)
Json(transactions.into_iter().map(Into::into).collect())
}
@@ -1,14 +1,25 @@
use crate::Database;
use axum::extract::{Path, Query};
use axum::{Extension, Json};
use chrono::NaiveDateTime;
use futures::StreamExt;
use serde::{Deserialize, Serialize};
use std::ptr::hash;
#[derive(Serialize)]
pub struct MinedBy {
pool: &'static str,
}
impl From<Pool> for MinedBy {
fn from(pool: Pool) -> Self {
Self { pool: pool.name() }
}
}
#[derive(Serialize)]
pub struct BlockList {
hash: String,
mined_by: Option<MinedBy>,
height: i64,
version: i32,
timestamp: NaiveDateTime,
@@ -16,24 +27,43 @@
nonce: u32,
difficulty: i64,
tx_count: i64,
}
#[derive(Deserialize)]
pub struct ListParams {
#[serde(default)]
limit: u32,
#[serde(default)]
offset: u32,
}
pub async fn list(Extension(database): Extension<Database>) -> Json<Vec<BlockList>> {
pub async fn list(
Extension(database): Extension<Database>,
Query(params): Query<ListParams>,
) -> Json<Vec<BlockList>> {
let database = database.get().await.unwrap();
let blocks = crate::database::blocks::fetch_latest_blocks(&database, 5)
.await
.unwrap();
let limit = std::cmp::min(20, std::cmp::max(5, params.limit));
let offset = params.offset;
let blocks = crate::database::blocks::fetch_latest_blocks(
&database,
i64::from(limit),
i64::from(offset),
)
.await
.unwrap();
Json(
blocks
.into_iter()
.map(|(mut block, tx_count)| {
.map(|(mut block, tx_count, coinbase_script)| {
block.hash.reverse();
BlockList {
hash: hex::encode(block.hash),
mined_by: Pool::fetch_from_script(&coinbase_script).map(Into::into),
height: block.height,
version: block.version,
timestamp: block.timestamp,
@@ -77,20 +107,61 @@
pub weight: i64,
pub coinbase: bool,
pub replace_by_fee: bool,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub inputs: Vec<TransactionInput>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub outputs: Vec<TransactionOutput>,
}
impl From<crate::database::transactions::Transaction> for Transaction {
fn from(mut tx: crate::database::transactions::Transaction) -> Self {
tx.hash.reverse();
Transaction {
hash: hex::encode(tx.hash),
version: tx.version,
lock_time: tx.lock_time,
weight: tx.weight,
coinbase: tx.coinbase,
replace_by_fee: tx.replace_by_fee,
inputs: tx.inputs.0.into_iter().map(Into::into).collect(),
outputs: tx.outputs.0.into_iter().map(Into::into).collect(),
}
}
}
#[derive(Serialize)]
pub struct PreviousOutput {
#[serde(flatten)]
output: TransactionOutput,
tx_hash: String,
tx_index: i64,
}
#[derive(Serialize)]
pub struct TransactionInput {
previous_output: Option<TransactionOutput>,
witness: Vec<String>,
sequence: i64,
previous_output: Option<PreviousOutput>,
script: String,
}
impl From<crate::database::transactions::TransactionInput> for TransactionInput {
fn from(txi: crate::database::transactions::TransactionInput) -> Self {
Self {
previous_output: txi.previous_output_tx.map(Into::into),
witness: txi.witness.into_iter().map(hex::encode).collect(),
sequence: txi.sequence,
previous_output: txi.previous_output.map(|v| PreviousOutput {
tx_index: v.index,
output: v.into(),
tx_hash: txi
.previous_output_tx_hash
.map(|mut h| {
h.reverse();
hex::encode(h)
})
.unwrap_or_default(),
}),
script: txi.script,
}
}
@@ -98,6 +169,7 @@
#[derive(Serialize)]
pub struct TransactionOutput {
index: i64,
value: i64,
script: String,
unspendable: bool,
@@ -107,6 +179,7 @@
impl From<crate::database::transactions::TransactionOutput> for TransactionOutput {
fn from(txo: crate::database::transactions::TransactionOutput) -> Self {
Self {
index: txo.index,
value: txo.value,
script: txo.script,
unspendable: txo.unspendable,
@@ -154,27 +227,79 @@
bits: block.bits,
nonce: block.nonce,
difficulty: block.difficulty,
transactions: transactions
.into_iter()
.map(|mut tx| {
tx.hash.reverse();
Transaction {
hash: hex::encode(tx.hash),
version: tx.version,
lock_time: tx.lock_time,
weight: tx.weight,
coinbase: tx.coinbase,
replace_by_fee: tx.replace_by_fee,
inputs: tx.inputs.0.into_iter().map(Into::into).collect(),
outputs: tx.outputs.0.into_iter().map(Into::into).collect(),
}
})
.collect(),
transactions: transactions.into_iter().map(Into::into).collect(),
};
Json(GetResponse {
tx_count: count,
block,
})
}
pub enum Pool {
Luxor,
F2Pool,
Binance,
FoundryUsa,
Slush,
Poolin,
ViaBtc,
BtcCom,
AntPool,
MaraPool,
SbiCrypto,
}
impl Pool {
pub fn name(&self) -> &'static str {
match self {
Pool::Luxor => "Luxor",
Pool::F2Pool => "F2Pool",
Pool::Binance => "Binance",
Pool::FoundryUsa => "Foundry USA",
Pool::Slush => "Slush",
Pool::Poolin => "Poolin",
Pool::ViaBtc => "ViaBTC",
Pool::BtcCom => "BTC.com",
Pool::AntPool => "AntPool",
Pool::MaraPool => "MaraPool",
Pool::SbiCrypto => "SBICrypto",
}
}
}
impl Pool {
fn fetch_from_script(coinbase_script: &[u8]) -> Option<Self> {
let text = String::from_utf8_lossy(coinbase_script);
macro_rules! define {
($($signature:expr => $pool:ident,)*) => {
if false {
None
}
$(
else if text.contains($signature) {
Some(Self::$pool)
}
)*
else {
None
}
}
}
define! {
"Powered by Luxor Tech" => Luxor,
"F2Pool" => F2Pool,
"binance" => Binance,
"Foundry USA Pool" => FoundryUsa,
"slush" => Slush,
"poolin.com" => Poolin,
"ViaBTC" => ViaBtc,
"btcpool" => BtcCom,
"Mined by AntPool" => AntPool,
"MARA Pool" => MaraPool,
"SBICrypto" => SbiCrypto,
}
}
}
@@ -1,9 +1,10 @@
use axum::routing::get;
use axum::Router;
mod address;
mod block;
mod height;
mod transaction;
pub fn router() -> Router {
Router::new()
@@ -11,4 +12,6 @@
.route("/block", get(block::list))
.route("/block/:height", get(block::handle))
.route("/address/:address", get(address::handle))
.route("/tx", get(transaction::list))
.route("/tx/:hash", get(transaction::handle))
}
@@ -1,0 +1,45 @@
use crate::database::transactions::{fetch_latest_transactions, fetch_transaction_by_hash};
use crate::{
database::transactions::fetch_transactions_for_address, methods::block::Transaction, Database,
};
use axum::extract::Query;
use axum::{extract::Path, Extension, Json};
use futures::StreamExt;
use serde::Deserialize;
#[derive(Deserialize)]
pub struct ListQuery {
#[serde(default)]
limit: u32,
}
pub async fn list(
Extension(database): Extension<Database>,
Query(query): Query<ListQuery>,
) -> Json<Vec<Transaction>> {
let database = database.get().await.unwrap();
let limit = std::cmp::min(20, std::cmp::max(5, query.limit));
let transactions = fetch_latest_transactions(&database, limit.into())
.await
.unwrap();
Json(transactions.into_iter().map(Into::into).collect())
}
pub async fn handle(
Extension(database): Extension<Database>,
Path(hash): Path<String>,
) -> Json<Transaction> {
let mut hash = hex::decode(&hash).unwrap();
hash.reverse();
let database = database.get().await.unwrap();
let transaction = fetch_transaction_by_hash(&database, &hash)
.await
.unwrap()
.unwrap();
Json(transaction.into())
}
@@ -11,6 +11,7 @@
"table": {
"height": "Height",
"timestamp": "Timestamp",
"pool": "Pool",
"txns": "Transactions",
"size": "Size (KB)",
"weight": "Weight (KWU)"
@@ -1,42 +1,44 @@
<script context="module">
export async function load({ fetch, params, url }) {
let res = await fetch(`http://localhost:3001/address/${params.address}`);
export async function load({ fetch, params, url }) {
let res = await fetch(`http://localhost:3001/address/${params.address}`);
if (res.ok) {
return {
props: {
transactions: await res.json(),
address: params.address,
}
};
}
return {
status: res.status,
error: new Error()
};
if (res.ok) {
return {
props: {
transactions: await res.json(),
address: params.address,
},
};
}
return {
status: res.status,
error: new Error(),
};
}
</script>
<script>
import { briefHexToAsm } from "$lib/bitcoinScript";
import Transaction from "$lib/Transaction.svelte";
import { briefHexToAsm } from "$lib/bitcoinScript";
import Transaction from "$lib/Transaction.svelte";
export let transactions = {};
export let address = '';
export let transactions = {};
export let address = "";
</script>
<div>
<section class="p-7">
<h2 class="!p-0 !py-4">{address}</h2>
</section>
<section class="p-7">
<h2 class="!p-0 !text-base md:!text-lg">{address}</h2>
</section>
<section class="!bg-transparent">
<h3 class="text-white text-2xl">{transactions.length} Transaction{transactions.length > 1 ? 's' : ''}</h3>
</section>
<section class="!bg-transparent">
<h3 class="text-white text-2xl">
{transactions.length} Transaction{transactions.length > 1 ? "s" : ""}
</h3>
</section>
{#each transactions as transaction}
<Transaction transaction={transaction} />
{/each}
{#each transactions as transaction}
<Transaction highlight={address} {transaction} />
{/each}
</div>
<style lang="scss">
@@ -1,6 +1,6 @@
<script context="module">
export async function load({ fetch, params, url }) {
const offset = Math.max(0, Number.parseInt(url.searchParams.get('offset') || '0'));
const offset = Math.max(0, Number.parseInt(url.searchParams.get("offset") || "0"));
let res = await fetch(`http://localhost:3001/block/${params.id}?offset=${offset}`);
@@ -10,7 +10,7 @@
if (offset >= block.tx_count) {
return {
status: 404,
error: new Error("Offset exceeds the transaction count in this block")
error: new Error("Offset exceeds the transaction count in this block"),
};
}
@@ -18,12 +18,12 @@
props: {
block,
currentPage: Math.floor(offset / 30),
}
},
};
}
return {
status: res.status,
error: new Error()
error: new Error(),
};
}
</script>
@@ -58,16 +58,19 @@
</section>
<section class="!bg-transparent">
<h3 class="text-white text-2xl">{block.tx_count} Transaction{block.tx_count > 1 ? 's' : ''}</h3>
<h3 class="text-white text-2xl">{block.tx_count} Transaction{block.tx_count > 1 ? "s" : ""}</h3>
</section>
{#each block.transactions as transaction}
<Transaction transaction={transaction} />
<Transaction {transaction} />
{/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>
<a
href="/block/{block.height}{i === 0 ? '' : `?offset=${i * 30}`}"
class:active={i === currentPage}>{i + 1}</a
>
{/each}
</div>
</div>
@@ -1,7 +1,79 @@
<section>
<h2>block index</h2>
</section>
<script context="module">
export async function load({ fetch, url }) {
const offset = Math.max(0, Number.parseInt(url.searchParams.get("offset") || 0));
const limit = 20;
let res = await fetch(`http://localhost:3001/block?limit=${limit}&offset=${offset}`);
if (res.ok) {
return {
props: {
blocks: await res.json(),
offset,
limit,
},
};
}
return {
status: res.status,
error: new Error(),
};
}
</script>
<script>
import Blocks from "$lib/Blocks.svelte";
export let blocks;
export let offset;
export let limit;
</script>
<div class="mb-4">
<section class="mb-3">
<h2>Blocks</h2>
<div class="table-responsive">
<Blocks {blocks} />
</div>
</section>
<section class="!bg-transparent !mt-0 mb-3 flex">
{#if offset > 0}
{#if offset - limit <= 0}
<a
href="/block"
class="!text-slate-200 text-base rounded-lg bg-gray-800 p-3 cursor-pointer"
>
← Previous
</a>
{:else}
<a
href="/block?offset={Math.max(0, offset - limit)}"
class="!text-slate-200 text-base rounded-lg bg-gray-800 p-3 cursor-pointer"
>
← Previous
</a>
{/if}
{/if}
<div class="flex-grow" />
{#if blocks.length >= limit}
<a
href="/block?offset={offset + limit}"
class="!text-slate-200 text-base rounded-lg bg-gray-800 p-3 cursor-pointer"
>
Next →
</a>
{/if}
</section>
</div>
<style lang="scss">
@import "../../_section.scss";
section {
@apply text-xs;
}
</style>
@@ -1,7 +1,68 @@
<section>
<h2>single txn</h2>
</section>
<script context="module">
export async function load({ fetch, params, url }) {
let res = await fetch(`http://localhost:3001/tx/${params.id}`);
if (res.ok) {
return {
props: {
tx: await res.json(),
},
};
}
return {
status: res.status,
error: new Error(),
};
}
</script>
<script>
import Transaction from "$lib/Transaction.svelte";
let showingMoreInfo = false;
export let tx;
</script>
<div>
<section class="p-7">
<h2 class="!p-0">Transaction {tx.hash}</h2>
</section>
<section class="table-responsive">
<table class="text-xs">
<tbody>
<tr>
<th>Version</th>
<td>{tx.version}</td>
</tr>
<tr>
<th>Weight</th>
<td>{tx.weight}</td>
</tr>
<tr>
<th>Replace By Fee</th>
<td>{tx.replace_by_fee ? "Opted in" : "No"}</td>
</tr>
</tbody>
</table>
</section>
<section class="flex !bg-transparent mb-2">
<div class="flex-grow" />
<button
on:click={() => (showingMoreInfo = !showingMoreInfo)}
class="text-slate-200 text-base rounded-lg bg-gray-800 p-2 cursor-pointer"
>
{showingMoreInfo ? "- Less Info" : "+ More Info"}
</button>
</section>
<Transaction attachAnchor class="!mt-0" transaction={tx} {showingMoreInfo} showTxHeader={false} />
</div>
<style lang="scss">
@import "../../_section.scss";
@import "../../_table.scss";
</style>
@@ -1,7 +1,41 @@
<section>
<h2>txn index</h2>
</section>
<script context="module">
export async function load({ fetch }) {
const txs = await fetch("http://localhost:3001/tx?limit=20");
if (txs.ok) {
return {
props: {
transactions: await txs.json(),
},
};
}
return {
status: txs.status,
error: new Error(),
};
}
</script>
<script>
import Transactions from "$lib/Transactions.svelte";
export let transactions;
</script>
<div class="mb-4">
<section class="mb-3">
<h2>Transactions</h2>
<div class="table-responsive">
<Transactions {transactions} />
</div>
</section>
</div>
<style lang="scss">
@import "../../_section.scss";
section {
@apply text-xs;
}
</style>