From 528cc79725d0660f1b6c938fe17067743a353d1e Mon Sep 17 00:00:00 2001 From: Dagur Valberg Johannsson Date: Wed, 25 Sep 2024 11:56:55 +0200 Subject: [PATCH] API call for latest transactions --- src/bcmr/utilurl.rs | 5 +++ src/bcmr/wellknowndowloader.rs | 5 +++ src/db/cauldron/tx.rs | 60 ++++++++++++++++++++++++++++++++-- src/main.rs | 3 +- src/rpc/mod.rs | 1 + src/rpc/tx.rs | 54 ++++++++++++++++++++++++++++++ 6 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 src/rpc/tx.rs diff --git a/src/bcmr/utilurl.rs b/src/bcmr/utilurl.rs index 74158b1..51c22dc 100644 --- a/src/bcmr/utilurl.rs +++ b/src/bcmr/utilurl.rs @@ -1,3 +1,8 @@ +// Copyright (C) 2024 Riften Labs AS +// +// This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later. +// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html + use std::time; use anyhow::{bail, Result}; diff --git a/src/bcmr/wellknowndowloader.rs b/src/bcmr/wellknowndowloader.rs index 9b9b2de..46a9dad 100644 --- a/src/bcmr/wellknowndowloader.rs +++ b/src/bcmr/wellknowndowloader.rs @@ -1,3 +1,8 @@ +// Copyright (C) 2024 Riften Labs AS +// +// This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later. +// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html + use crate::{ bcmr::{parsedbcmr::parse_all_bcmr_identities, utilurl::get_url}, db::{ diff --git a/src/db/cauldron/tx.rs b/src/db/cauldron/tx.rs index 485942a..a79b52a 100644 --- a/src/db/cauldron/tx.rs +++ b/src/db/cauldron/tx.rs @@ -5,8 +5,8 @@ use std::time::{SystemTime, UNIX_EPOCH}; use anyhow::Result; -use bitcoin_hashes::hex::ToHex; -use bitcoincash::{BlockHash, Txid}; +use bitcoin_hashes::hex::{FromHex, ToHex}; +use bitcoincash::{BlockHash, TokenID, Txid}; use rusqlite::{params, Connection}; pub fn create_table(conn: &Connection) { @@ -49,3 +49,59 @@ pub fn insert_mempool_tx(conn: &rusqlite::Connection, txid: &Txid) -> Result<()> conn.execute(sql, params![&txid.to_hex(), current_timestamp])?; Ok(()) } + +pub fn latest( + conn: &rusqlite::Connection, + limit: usize, + offset: usize, + token_id: Option, +) -> Result, u64)>, rusqlite::Error> { + let (sql, params) = match token_id { + Some(tid) => ( + "SELECT DISTINCT tx.txid, tx.blockhash, tx.mtp_timestamp, tx.first_seen_timestamp + FROM tx + JOIN utxo_funding ON tx.txid = utxo_funding.txid + WHERE utxo_funding.token_id = ? + ORDER BY COALESCE(tx.first_seen_timestamp, tx.mtp_timestamp) DESC + LIMIT ? OFFSET ?", + params![tid.to_hex(), limit, offset], + ), + None => ( + "SELECT tx.txid, tx.blockhash, tx.mtp_timestamp, tx.first_seen_timestamp + FROM tx + ORDER BY COALESCE(tx.first_seen_timestamp, tx.mtp_timestamp) DESC + LIMIT ? OFFSET ?", + params![limit, offset], + ), + }; + + let mut stmt = conn.prepare(sql)?; + let tx_iter = stmt.query_map(params, |row| { + let txid_hex: String = row.get(0)?; + let blockhash_hex: Option = row.get(1)?; + let mtp_timestamp: Option = row.get(2).ok(); // Handle potential NULL + let first_seen_timestamp: Option = row.get(3).ok(); // Handle potential NULL + + // Use first_seen_timestamp if available, otherwise mtp_timestamp, or return an error if both are NULL + let timestamp = first_seen_timestamp.or(mtp_timestamp).ok_or_else(|| { + rusqlite::Error::InvalidColumnType( + 2, + "timestamp".to_string(), + rusqlite::types::Type::Null, + ) + })?; + + Ok(( + Txid::from_hex(&txid_hex).expect("Invalid Txid hex"), + blockhash_hex.map(|hex| BlockHash::from_hex(&hex).expect("Invalid BlockHash hex")), + timestamp, + )) + })?; + + let mut txs = Vec::with_capacity(limit); + for tx in tx_iter { + txs.push(tx?); + } + + Ok(txs) +} diff --git a/src/main.rs b/src/main.rs index 4247e2a..4da274b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -253,7 +253,8 @@ fn launch() -> _ { rpc::contract::contract_count_token, rpc::contract::contract_count_all, rpc::contract::contract_volume, - rpc::user::unique_addresses + rpc::user::unique_addresses, + rpc::tx::tx_latest, ], ) .mount( diff --git a/src/rpc/mod.rs b/src/rpc/mod.rs index 8b10adc..d00391a 100644 --- a/src/rpc/mod.rs +++ b/src/rpc/mod.rs @@ -22,6 +22,7 @@ pub mod pool; pub mod price; pub mod tokens; pub mod tvl; +pub mod tx; pub mod user; pub type ResponseCache = Arc)>>>; diff --git a/src/rpc/tx.rs b/src/rpc/tx.rs new file mode 100644 index 0000000..d6a55c2 --- /dev/null +++ b/src/rpc/tx.rs @@ -0,0 +1,54 @@ +// Copyright (C) 2024 Riften Labs AS +// +// This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later. +// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html + +use bitcoin_hashes::hex::FromHex; +use bitcoincash::TokenID; +use rocket::{get, http::Status, response::status::Custom, serde::json::Json, State}; +use serde_json::json; +use serde_json::Value; + +use crate::db::DB; + +#[get("/tx/latest?&&")] +pub fn tx_latest( + limit: Option, + offset: Option, + token: Option<&str>, + conn: &State, +) -> Result, Custom> { + let db = conn.cauldron_r.get().map_err(|_| { + Custom( + Status::InternalServerError, + "Failed to get DB connection".into(), + ) + })?; + + let limit = limit.unwrap_or(100).min(10000); + let offset = offset.unwrap_or(0); + + let token = match token { + None => None, + Some(tokenhex) => match TokenID::from_hex(tokenhex) { + Ok(token) => Some(token), + Err(_) => return Err(Custom(Status::BadRequest, "Invalid token ID".into())), + }, + }; + + let txs = crate::db::cauldron::tx::latest(&db, limit, offset, token) + .map_err(|e| Custom(Status::InternalServerError, format!("Error: {}", e)))?; + + let txs_json: Vec = txs + .into_iter() + .map(|tx| { + json!({ + "txid": tx.0.to_string(), + "blockhash": tx.1.map(|blockhex| blockhex.to_string()), + "timestamp_guess": tx.2 + }) + }) + .collect(); + + Ok(Json(json!(txs_json))) +}