// 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::{SystemTime, UNIX_EPOCH}; use anyhow::Result; use bitcoin_hashes::hex::{FromHex, ToHex}; use bitcoincash::{BlockHash, TokenID, Txid}; use rusqlite::{params, Connection}; pub fn create_table(conn: &Connection) { let tbl = "CREATE TABLE tx ( txid TEXT PRIMARY KEY, blockhash TEXT, mtp_timestamp BIGINT, first_seen_timestamp BIGINT )"; conn.execute(tbl, []).expect("failed to create tx table"); } pub fn insert_block_tx( conn: &rusqlite::Connection, txid: &Txid, blockhash: &BlockHash, mtp: i64, ) -> Result<()> { let sql = "INSERT INTO tx (txid, blockhash, mtp_timestamp) VALUES (?1, ?2, ?3) ON CONFLICT(txid) DO UPDATE SET blockhash = excluded.blockhash, mtp_timestamp = excluded.mtp_timestamp"; conn.execute(sql, params![&txid.to_hex(), &blockhash.to_hex(), &mtp])?; Ok(()) } pub fn insert_mempool_tx(conn: &rusqlite::Connection, txid: &Txid) -> Result<()> { let sql = "INSERT INTO tx (txid, first_seen_timestamp) VALUES (?1, ?2) ON CONFLICT(txid) DO UPDATE SET first_seen_timestamp = excluded.first_seen_timestamp"; let current_timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs() as i64; 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) }