Calculate fees, weight, block size & use locale for number display
Diff
Cargo.lock | 37 +++++++++++++++++++++++++++++++++++++
web-api/Cargo.toml | 2 ++
web-api/config.toml | 5 -----
indexer/src/main.rs | 4 +++-
web-api/src/main.rs | 1 -
frontend/src/lib/Blocks.svelte | 7 +++----
frontend/src/lib/Transaction.svelte | 4 ++--
frontend/src/lib/Transactions.svelte | 17 +++++++++++++++--
web-api/src/database/blocks.rs | 9 ++++++---
web-api/src/database/transactions.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++++++--
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(-)
@@ -21,6 +21,12 @@
]
[[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"
@@ -817,6 +823,20 @@
version = "0.1.0"
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"
@@ -1004,6 +1024,21 @@
version = "0.1.7"
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"
@@ -1582,11 +1617,13 @@
version = "0.1.0"
dependencies = [
"axum",
"bitcoin",
"chrono",
"clap",
"deadpool-postgres",
"futures",
"hex 0.4.3",
"rust_decimal",
"serde",
"tokio",
"tokio-postgres",
@@ -7,7 +7,9 @@
[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"
@@ -1,5 +1,0 @@
[database]
user = "postgres"
password = "postgres"
host = "127.0.0.1"
database = "postgres"
@@ -46,7 +46,7 @@
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 verbose: usize,
#[clap(short, long, parse(try_from_str = Config::from_toml_path))]
pub config: Config,
pub start: u64,
}
impl Args {
@@ -5,7 +5,6 @@
use crate::config::Config;
use crate::database::Database;
use axum::routing::get;
use axum::{Extension, Router};
use clap::Parser;
use tower::ServiceBuilder;
@@ -29,10 +29,9 @@
</span>
</td>
<td>{block.mined_by?.pool || "Unknown"}</td>
<td>{block.tx_count}</td>
<td>{block.bits}</td>
<td>{block.weight}</td>
<td>{block.tx_count.toLocaleString()}</td>
<td>{(block.size / 1000).toLocaleString()}</td>
<td>{(block.weight / 1000).toLocaleString()}</td>
</tr>
{/each}
</tbody>
@@ -80,7 +80,7 @@
{#if input.previous_output}
<div class="amount">
<code>{(input.previous_output.value / scale).toFixed(8)} BTC</code>
<code>{(input.previous_output.value / scale).toFixed(8).toLocaleString()} BTC</code>
</div>
{/if}
</div>
@@ -123,7 +123,7 @@
</div>
<div class="amount">
<code>{(output.value / scale).toFixed(8)} BTC</code>
<code>{(output.value / scale).toFixed(8).toLocaleString()} BTC</code>
</div>
</div>
</div>
@@ -1,7 +1,9 @@
<script>
import { t as _ } from "$lib/i18n";
export let transactions;
const WITNESS_SCALE_FACTOR = 4;
</script>
<table>
@@ -18,9 +20,18 @@
{#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>
<td><code>{(txn.output_total_value / Math.pow(10, 8)).toFixed(8)} BTC</code></td>
<td><code>{Math.ceil((txn.weight + WITNESS_SCALE_FACTOR - 1) / WITNESS_SCALE_FACTOR).toLocaleString()} vB</code></td>
<td>
<code>
{#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
</code>
</td>
</tr>
{/each}
</tbody>
@@ -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 type TransactionCount = i64;
pub type TransactionWeight = rust_decimal::Decimal;
pub async fn fetch_latest_blocks(
db: &Connection,
count: i64,
offset: i64,
) -> Result<Vec<(Block, TransactionCount, Vec<u8>)>> {
) -> Result<Vec<(Block, TransactionCount, TransactionWeight, Vec<u8>)>> {
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 @@
.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::<Result<Vec<_>>>()
}
@@ -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<u8>,
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<Vec<TransactionInput>>,
@@ -23,8 +23,8 @@
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")?,
@@ -197,11 +197,34 @@
.into_iter()
.map(Transaction::from_row)
.collect()
}
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<Vec<Transaction>> {
pub async fn fetch_latest_transactions(
db: &Connection,
limit: i64,
) -> Result<Vec<TransactionWithDetails>> {
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 @@
transactions
.into_iter()
.map(Transaction::from_row)
.map(|tx| {
Ok(TransactionWithDetails {
input_total_value: tx
.try_get::<_, Option<_>>("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()
}
@@ -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 @@
bits: i32,
nonce: u32,
difficulty: i64,
weight: u64,
tx_count: i64,
size: i32,
}
#[derive(Deserialize)]
@@ -57,7 +60,7 @@
Json(
blocks
.into_iter()
.map(|(mut block, tx_count, coinbase_script)| {
.map(|(mut block, tx_count, tx_weight, coinbase_script)| {
block.hash.reverse();
@@ -67,9 +70,13 @@
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 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 @@
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(),
@@ -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 @@
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<Database>,
Query(query): Query<ListQuery>,
) -> Json<Vec<Transaction>> {
) -> Json<Vec<ListResponseTransaction>> {
let database = database.get().await.unwrap();
let limit = std::cmp::min(20, std::cmp::max(5, query.limit));
@@ -25,7 +30,16 @@
.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(
@@ -1,6 +1,5 @@
use axum::handler::Handler;
use axum::{
extract::{self, FromRequest, RequestParts},
http::{Request, Response},