🏡 index : ~doyle/blocks.ls.git

author Jordan Doyle <jordan@doyle.la> 2022-05-21 13:34:40.0 +00:00:00
committer Jordan Doyle <jordan@doyle.la> 2022-05-21 13:36:09.0 +00:00:00
commit
1eb91d8e9cac8f54b4c6a60d2be6ed1cbcde5240 [patch]
tree
db02df138747a163b7a7bed9fec29f8bbef733ae
parent
4cb31beb32713f4fb0d7450a44a094bc0c9cde93
download
1eb91d8e9cac8f54b4c6a60d2be6ed1cbcde5240.tar.gz

Add pagination to single block view



Diff

 frontend/package-lock.json            |  27 +++++++-
 frontend/package.json                 |   3 +-
 frontend/src/global.scss              |   4 +-
 frontend/src/lib/bitcoinScript.ts     |  16 +++-
 frontend/src/routes/block/[id].svelte | 129 ++++++++++++++++++++++++++---------
 frontend/src/routes/index.svelte      |   3 +-
 indexer/src/main.rs                   |   2 +-
 web-api/src/database/blocks.rs        |   5 +-
 web-api/src/database/transactions.rs  |  90 +++++++++++++++---------
 web-api/src/methods/block.rs          |  52 ++++++++++----
 10 files changed, 248 insertions(+), 83 deletions(-)

diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 11464ed..2a0987e 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -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",
@@ -2890,6 +2896,14 @@
        "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": {
      "version": "2.2.1",
      "resolved": "https://registry.npmjs.org/sveltekit-i18n/-/sveltekit-i18n-2.2.1.tgz",
@@ -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",
@@ -5036,6 +5055,14 @@
        }
      }
    },
    "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": {
      "version": "2.2.1",
      "resolved": "https://registry.npmjs.org/sveltekit-i18n/-/sveltekit-i18n-2.2.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 953906a..b52575c 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -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"
  }
}
diff --git a/frontend/src/global.scss b/frontend/src/global.scss
index 2a049eb..2eaaf9d 100644
--- a/frontend/src/global.scss
+++ b/frontend/src/global.scss
@@ -6,6 +6,10 @@ body {
  @apply bg-gray-900;
}

time {
  @apply decoration-dashed underline cursor-help;
}

a {
  @apply text-sky-400 transition-all;

diff --git a/frontend/src/lib/bitcoinScript.ts b/frontend/src/lib/bitcoinScript.ts
index 28d9d06..8f8adb5 100644
--- a/frontend/src/lib/bitcoinScript.ts
+++ b/frontend/src/lib/bitcoinScript.ts
@@ -15,7 +15,7 @@ export function hexToAsm(hex: string) {
        }

        if (byte >= 0x52 && byte <= 0x60)  {
            out.push(byte-0x50)
            out.push(byte - 0x50)
            continue;
        }

@@ -30,6 +30,20 @@ export function hexToAsm(hex: string) {
    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 {
    OP_FALSE = 0x00,
    OP_PUSHDATA1 = 0x4c,
diff --git a/frontend/src/routes/block/[id].svelte b/frontend/src/routes/block/[id].svelte
index 7301541..2daf05f 100644
--- a/frontend/src/routes/block/[id].svelte
+++ b/frontend/src/routes/block/[id].svelte
@@ -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>
    <section class="p-4">
      <h3 class="text-lg m-2" id={transaction.hash}>
        <a href={`#${transaction.hash}`}>§</a>
        {transaction.hash}
      </h3>

      <div class="flex">
        <table>
          <tbody>
      <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>
diff --git a/frontend/src/routes/index.svelte b/frontend/src/routes/index.svelte
index abd6eaf..ffe2de5 100644
--- a/frontend/src/routes/index.svelte
+++ b/frontend/src/routes/index.svelte
@@ -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> <!-- todo: moment.js -->
            <td><Time live relative timestamp={block.timestamp} /></td> <!-- todo: moment.js -->
            <td>{block.tx_count}</td>
            <td>{block.bits}</td> <!-- todo: this isn't really size -->
            <td>{block.weight}</td>
diff --git a/indexer/src/main.rs b/indexer/src/main.rs
index d2375de..0aeae00 100644
--- a/indexer/src/main.rs
+++ b/indexer/src/main.rs
@@ -46,7 +46,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
    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);

diff --git a/web-api/src/database/blocks.rs b/web-api/src/database/blocks.rs
index 332ed68..6cbe3c1 100644
--- a/web-api/src/database/blocks.rs
+++ b/web-api/src/database/blocks.rs
@@ -43,7 +43,10 @@ pub async fn fetch_height(db: &Connection) -> Result<u64> {

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
diff --git a/web-api/src/database/transactions.rs b/web-api/src/database/transactions.rs
index e1198e3..40848ee 100644
--- a/web-api/src/database/transactions.rs
+++ b/web-api/src/database/transactions.rs
@@ -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 struct TransactionOutput {
    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) -> 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?;
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)
    )?;

    transactions
        .into_iter()
        .map(Transaction::from_row)
        .collect()
    Ok((
        count.try_get("count")?,
        transactions
            .into_iter()
            .map(Transaction::from_row)
            .collect::<Result<_>>()?,
    ))
}
diff --git a/web-api/src/methods/block.rs b/web-api/src/methods/block.rs
index d15fdaf..492f306 100644
--- a/web-api/src/methods/block.rs
+++ b/web-api/src/methods/block.rs
@@ -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 struct BlockList {
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)| {
                // TODO: do this on insert
                block.hash.reverse();
@@ -36,14 +40,21 @@ pub async fn list(Extension(database): Extension<Database>) -> Json<Vec<BlockLis
                    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,
@@ -104,26 +115,36 @@ impl From<crate::database::transactions::TransactionOutput> for TransactionOutpu
    }
}

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

    // TODO: do this on insert
    block.hash.reverse();

    Json(Block {
    let block = Block {
        hash: hex::encode(block.hash),
        height: block.height,
        version: block.version,
@@ -146,5 +167,10 @@ pub async fn handle(
                outputs: tx.outputs.0.into_iter().map(Into::into).collect(),
            })
            .collect(),
    };

    Json(GetResponse {
        tx_count: count,
        block,
    })
}