From 9dc3110ee521662ce5059dfe9f467622734be489 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Mon, 23 May 2022 01:07:27 +0100 Subject: [PATCH] Calculate fees, weight, block size & use locale for number display --- Cargo.lock | 37 +++++++++++++++++++++++++++++++++++++ frontend/src/lib/Blocks.svelte | 7 +++---- frontend/src/lib/Transaction.svelte | 4 ++-- frontend/src/lib/Transactions.svelte | 17 ++++++++++++++--- indexer/src/main.rs | 4 +++- web-api/Cargo.toml | 2 ++ web-api/config.toml | 5 ----- web-api/src/database/blocks.rs | 9 ++++++--- web-api/src/database/transactions.rs | 51 ++++++++++++++++++++++++++++++++++++++++++--------- web-api/src/main.rs | 1 - web-api/src/methods/block.rs | 15 +++++++++++---- web-api/src/methods/transaction.rs | 28 +++++++++++++++++++++------- web-api/src/middleware/logging.rs | 1 - 13 files changed, 141 insertions(+), 40 deletions(-) delete mode 100644 web-api/config.toml diff --git a/Cargo.lock b/Cargo.lock index b76a3e0..fc5806c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,6 +21,12 @@ dependencies = [ ] [[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] name = "async-trait" version = "0.1.53" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -819,6 +825,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] +name = "postgres" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8bbcd5f6deb39585a0d9f4ef34c4a41c25b7ad26d23c75d837d78c8e7adc85f" +dependencies = [ + "bytes", + "fallible-iterator", + "futures", + "log", + "tokio", + "tokio-postgres", +] + +[[package]] name = "postgres-protocol" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1006,6 +1026,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c31b5c4033f8fdde8700e4657be2c497e7288f01515be52168c631e2e4d4086" [[package]] +name = "rust_decimal" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22dc69eadbf0ee2110b8d20418c0c6edbaefec2811c4963dc17b6344e11fe0f8" +dependencies = [ + "arrayvec", + "byteorder", + "bytes", + "num-traits", + "postgres", + "serde", + "tokio-postgres", +] + +[[package]] name = "ryu" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1582,11 +1617,13 @@ name = "web-api" version = "0.1.0" dependencies = [ "axum", + "bitcoin", "chrono", "clap", "deadpool-postgres", "futures", "hex 0.4.3", + "rust_decimal", "serde", "tokio", "tokio-postgres", diff --git a/frontend/src/lib/Blocks.svelte b/frontend/src/lib/Blocks.svelte index dcbdd06..fc9712c 100644 --- a/frontend/src/lib/Blocks.svelte +++ b/frontend/src/lib/Blocks.svelte @@ -29,10 +29,9 @@ {block.mined_by?.pool || "Unknown"} - {block.tx_count} - {block.bits} - - {block.weight} + {block.tx_count.toLocaleString()} + {(block.size / 1000).toLocaleString()} + {(block.weight / 1000).toLocaleString()} {/each} diff --git a/frontend/src/lib/Transaction.svelte b/frontend/src/lib/Transaction.svelte index 9c5d6dd..efae15d 100644 --- a/frontend/src/lib/Transaction.svelte +++ b/frontend/src/lib/Transaction.svelte @@ -80,7 +80,7 @@ {#if input.previous_output}
- {(input.previous_output.value / scale).toFixed(8)} BTC + {(input.previous_output.value / scale).toFixed(8).toLocaleString()} BTC
{/if} @@ -123,7 +123,7 @@
- {(output.value / scale).toFixed(8)} BTC + {(output.value / scale).toFixed(8).toLocaleString()} BTC
diff --git a/frontend/src/lib/Transactions.svelte b/frontend/src/lib/Transactions.svelte index 134b2b7..2ed4519 100644 --- a/frontend/src/lib/Transactions.svelte +++ b/frontend/src/lib/Transactions.svelte @@ -2,6 +2,8 @@ import { t as _ } from "$lib/i18n"; export let transactions; + + const WITNESS_SCALE_FACTOR = 4; @@ -18,9 +20,18 @@ {#each transactions as txn} - - - + + + {/each} diff --git a/indexer/src/main.rs b/indexer/src/main.rs index 1c2c266..44507d1 100644 --- a/indexer/src/main.rs +++ b/indexer/src/main.rs @@ -46,7 +46,7 @@ async fn main() -> Result<(), Box> { let height = bitcoin_rpc.get_block_count().await?; eprintln!("Current block height: {}", height); - let start = 737000; + let start = args.start; let (tx, mut rx) = tokio::sync::mpsc::channel::<(u64, BlockHash, Block)>(200); @@ -295,6 +295,8 @@ pub struct Args { pub verbose: usize, #[clap(short, long, parse(try_from_str = Config::from_toml_path))] pub config: Config, + /// Block height to start at + pub start: u64, } impl Args { diff --git a/web-api/Cargo.toml b/web-api/Cargo.toml index 614f203..0c49e13 100644 --- a/web-api/Cargo.toml +++ b/web-api/Cargo.toml @@ -7,7 +7,9 @@ edition = "2021" [dependencies] axum = "0.5" +bitcoin = "0.28" deadpool-postgres = "0.10" +rust_decimal = { version = "1.23", features = ["db-tokio-postgres"] } chrono = { version = "0.4", features = ["serde"] } clap = { version = "3.1", features = ["derive"] } futures = "0.3" diff --git a/web-api/config.toml b/web-api/config.toml deleted file mode 100644 index 4e6ed98..0000000 --- a/web-api/config.toml +++ /dev/null @@ -1,5 +0,0 @@ -[database] -user = "postgres" -password = "postgres" -host = "127.0.0.1" -database = "postgres" diff --git a/web-api/src/database/blocks.rs b/web-api/src/database/blocks.rs index 04cd55b..1ff9f51 100644 --- a/web-api/src/database/blocks.rs +++ b/web-api/src/database/blocks.rs @@ -1,5 +1,5 @@ use crate::database::{Connection, Result}; -use chrono::{DateTime, NaiveDateTime, Utc}; +use chrono::NaiveDateTime; use tokio_postgres::Row; #[derive(Debug)] @@ -42,17 +42,19 @@ pub async fn fetch_height(db: &Connection) -> Result { } pub type TransactionCount = i64; +pub type TransactionWeight = rust_decimal::Decimal; pub async fn fetch_latest_blocks( db: &Connection, count: i64, offset: i64, -) -> Result)>> { +) -> Result)>> { let blocks = db .query( "SELECT blocks.*, COUNT(transactions.id) AS tx_count, + SUM(transactions.weight) AS tx_weight, ( SELECT script FROM transactions @@ -76,8 +78,9 @@ pub async fn fetch_latest_blocks( .into_iter() .map(|row| { let tx_count = row.try_get("tx_count")?; + let tx_weight = row.try_get("tx_weight")?; let coinbase_script = row.try_get("coinbase_script")?; - Ok((Block::from_row(row)?, tx_count, coinbase_script)) + Ok((Block::from_row(row)?, tx_count, tx_weight, coinbase_script)) }) .collect::>>() } diff --git a/web-api/src/database/transactions.rs b/web-api/src/database/transactions.rs index 9ac1466..fb1a6ba 100644 --- a/web-api/src/database/transactions.rs +++ b/web-api/src/database/transactions.rs @@ -1,17 +1,17 @@ use crate::database::{Connection, Result}; -use futures::StreamExt; use serde::de::Error; use serde::{Deserialize, Deserializer}; -use tokio::time::Instant; -use tokio_postgres::types::{Json, ToSql}; -use tokio_postgres::Row; +use tokio_postgres::{ + types::{Json, ToSql}, + Row, +}; #[derive(Debug)] pub struct Transaction { pub hash: Vec, pub version: i32, - pub lock_time: i32, pub weight: i64, + pub lock_time: i32, pub coinbase: bool, pub replace_by_fee: bool, pub inputs: Json>, @@ -23,8 +23,8 @@ impl Transaction { Ok(Self { hash: row.try_get("hash")?, version: row.try_get("version")?, - lock_time: row.try_get("lock_time")?, weight: row.try_get("weight")?, + lock_time: row.try_get("lock_time")?, coinbase: row.try_get("coinbase")?, replace_by_fee: row.try_get("replace_by_fee")?, inputs: row.try_get("inputs")?, @@ -199,9 +199,32 @@ pub async fn fetch_transactions_for_address( .collect() } -pub async fn fetch_latest_transactions(db: &Connection, limit: i64) -> Result> { +pub struct TransactionWithDetails { + pub input_total_value: rust_decimal::Decimal, + pub output_total_value: rust_decimal::Decimal, + pub transaction: Transaction, +} + +pub async fn fetch_latest_transactions( + db: &Connection, + limit: i64, +) -> Result> { let select_query = " - SELECT *, JSON_BUILD_ARRAY() AS inputs, JSON_BUILD_ARRAY() AS outputs + SELECT transactions.*, + JSON_BUILD_ARRAY() AS inputs, + JSON_BUILD_ARRAY() AS outputs, + ( + SELECT SUM(po.value) + FROM transaction_inputs input + LEFT JOIN transaction_outputs po + ON po.id = input.previous_output + WHERE input.transaction_id = transactions.id + ) AS input_total_value, + ( + SELECT SUM(out.value) + FROM transaction_outputs out + WHERE out.transaction_id = transactions.id + ) AS output_total_value FROM transactions ORDER BY transactions.id DESC LIMIT $1 @@ -211,7 +234,17 @@ pub async fn fetch_latest_transactions(db: &Connection, limit: i64) -> Result>("input_total_value")? + .unwrap_or_default(), + output_total_value: tx + .try_get::<_, Option<_>>("output_total_value")? + .unwrap_or_default(), + transaction: Transaction::from_row(tx)?, + }) + }) .collect() } diff --git a/web-api/src/main.rs b/web-api/src/main.rs index 6f88655..9765fec 100644 --- a/web-api/src/main.rs +++ b/web-api/src/main.rs @@ -5,7 +5,6 @@ mod middleware; use crate::config::Config; use crate::database::Database; -use axum::routing::get; use axum::{Extension, Router}; use clap::Parser; use tower::ServiceBuilder; diff --git a/web-api/src/methods/block.rs b/web-api/src/methods/block.rs index d79fb69..776e234 100644 --- a/web-api/src/methods/block.rs +++ b/web-api/src/methods/block.rs @@ -1,8 +1,9 @@ use crate::Database; use axum::extract::{Path, Query}; use axum::{Extension, Json}; +use bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR; +use bitcoin::VarInt; use chrono::NaiveDateTime; -use futures::StreamExt; use serde::{Deserialize, Serialize}; #[derive(Serialize)] @@ -26,7 +27,9 @@ pub struct BlockList { bits: i32, nonce: u32, difficulty: i64, + weight: u64, tx_count: i64, + size: i32, } #[derive(Deserialize)] @@ -57,7 +60,7 @@ pub async fn list( Json( blocks .into_iter() - .map(|(mut block, tx_count, coinbase_script)| { + .map(|(mut block, tx_count, tx_weight, coinbase_script)| { // TODO: do this on insert block.hash.reverse(); @@ -67,9 +70,13 @@ pub async fn list( height: block.height, version: block.version, timestamp: block.timestamp, + size: block.size, bits: block.bits, nonce: block.nonce, difficulty: block.difficulty, + weight: (u64::try_from(WITNESS_SCALE_FACTOR).unwrap() + * u64::try_from(VarInt(u64::try_from(tx_count).unwrap()).len()).unwrap()) + + u64::try_from(tx_weight.mantissa()).unwrap(), tx_count, } }) @@ -103,8 +110,8 @@ pub struct Block { pub struct Transaction { pub hash: String, pub version: i32, - pub lock_time: i32, pub weight: i64, + pub lock_time: i32, pub coinbase: bool, pub replace_by_fee: bool, #[serde(skip_serializing_if = "Vec::is_empty")] @@ -120,8 +127,8 @@ impl From for Transaction { Transaction { hash: hex::encode(tx.hash), version: tx.version, - lock_time: tx.lock_time, weight: tx.weight, + lock_time: tx.lock_time, coinbase: tx.coinbase, replace_by_fee: tx.replace_by_fee, inputs: tx.inputs.0.into_iter().map(Into::into).collect(), diff --git a/web-api/src/methods/transaction.rs b/web-api/src/methods/transaction.rs index 4254588..55224b9 100644 --- a/web-api/src/methods/transaction.rs +++ b/web-api/src/methods/transaction.rs @@ -1,11 +1,8 @@ use crate::database::transactions::{fetch_latest_transactions, fetch_transaction_by_hash}; -use crate::{ - database::transactions::fetch_transactions_for_address, methods::block::Transaction, Database, -}; +use crate::{methods::block::Transaction, Database}; use axum::extract::Query; use axum::{extract::Path, Extension, Json}; -use futures::StreamExt; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; #[derive(Deserialize)] pub struct ListQuery { @@ -13,10 +10,18 @@ pub struct ListQuery { limit: u32, } +#[derive(Serialize)] +pub struct ListResponseTransaction { + #[serde(flatten)] + transaction: Transaction, + input_total_value: i64, + output_total_value: i64, +} + pub async fn list( Extension(database): Extension, Query(query): Query, -) -> Json> { +) -> Json> { let database = database.get().await.unwrap(); let limit = std::cmp::min(20, std::cmp::max(5, query.limit)); @@ -25,7 +30,16 @@ pub async fn list( .await .unwrap(); - Json(transactions.into_iter().map(Into::into).collect()) + Json( + transactions + .into_iter() + .map(|v| ListResponseTransaction { + transaction: v.transaction.into(), + input_total_value: i64::try_from(v.input_total_value.mantissa()).unwrap(), + output_total_value: i64::try_from(v.output_total_value.mantissa()).unwrap(), + }) + .collect(), + ) } pub async fn handle( diff --git a/web-api/src/middleware/logging.rs b/web-api/src/middleware/logging.rs index 688cf01..591e773 100644 --- a/web-api/src/middleware/logging.rs +++ b/web-api/src/middleware/logging.rs @@ -1,6 +1,5 @@ //! Logs each and every request out in a format similar to that of Apache's logs. -use axum::handler::Handler; use axum::{ extract::{self, FromRequest, RequestParts}, http::{Request, Response}, -- libgit2 1.7.2
{txn.hash}{txn.amount} BTC{txn.size} vB{txn.fee} sat/vB{(txn.output_total_value / Math.pow(10, 8)).toFixed(8)} BTC{Math.ceil((txn.weight + WITNESS_SCALE_FACTOR - 1) / WITNESS_SCALE_FACTOR).toLocaleString()} vB + + {#if txn.input_total_value > 0} + {((txn.input_total_value - txn.output_total_value) / ((txn.weight + WITNESS_SCALE_FACTOR - 1) / WITNESS_SCALE_FACTOR)).toFixed(2).toLocaleString()} + {:else} + 0 + {/if} + sat/vB + +