use crate::database::{Connection, Result};
use serde::de::Error;
use serde::{Deserialize, Deserializer};
use tokio_postgres::{
types::{Json, ToSql},
Row,
};
#[derive(Debug)]
pub struct Transaction {
pub hash: Vec<u8>,
pub version: i32,
pub weight: i64,
pub lock_time: i32,
pub coinbase: bool,
pub replace_by_fee: bool,
pub inputs: Json<Vec<TransactionInput>>,
pub outputs: Json<Vec<TransactionOutput>>,
}
impl Transaction {
pub fn from_row(row: Row) -> Result<Self> {
Ok(Self {
hash: row.try_get("hash")?,
version: row.try_get("version")?,
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")?,
outputs: row.try_get("outputs")?,
})
}
}
#[derive(Deserialize, Debug)]
pub struct TransactionInput {
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,
pub unspendable: bool,
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> {
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
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.block_id = $1
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)
)?;
Ok((
count.try_get("count")?,
transactions
.into_iter()
.map(Transaction::from_row)
.collect::<Result<_>>()?,
))
}
pub async fn fetch_transactions_for_address(
db: &Connection,
address: &str,
) -> Result<Vec<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.id IN (
SELECT transaction_outputs.transaction_id
FROM transaction_outputs
WHERE transaction_outputs.address = $1
UNION
SELECT transaction_inputs.transaction_id
FROM transaction_inputs
LEFT JOIN transaction_outputs
ON transaction_outputs.id = transaction_inputs.previous_output
WHERE transaction_outputs.address = $1
)
ORDER BY transactions.id DESC
";
let transactions = db.query(select_query, &[&address]).await?;
transactions
.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<TransactionWithDetails>> {
let select_query = "
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
";
let transactions = db.query(select_query, &[&limit]).await?;
transactions
.into_iter()
.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()
}
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()
}