diff --git a/app/rpc_server.rs b/app/rpc_server.rs index b37b5ad5..ca96ff31 100644 --- a/app/rpc_server.rs +++ b/app/rpc_server.rs @@ -11,7 +11,7 @@ use thunder::{ types::{Address, PointedOutput, Txid, WithdrawalBundle}, wallet::Balance, }; -use thunder_app_rpc_api::RpcServer; +use thunder_app_rpc_api::{GetTransactionResponse, RpcServer}; use tower_http::{ cors::CorsLayer, request_id::{ @@ -143,6 +143,19 @@ impl RpcServer for RpcServerImpl { self.app.wallet.get_new_address().map_err(custom_err) } + async fn get_transaction( + &self, + txid: Txid, + ) -> RpcResult> { + let res = self + .app + .node + .try_get_transaction(txid) + .map_err(custom_err)? + .map(|(tx, block_hash)| GetTransactionResponse { tx, block_hash }); + Ok(res) + } + async fn get_wallet_addresses(&self) -> RpcResult> { let addrs = self.app.wallet.get_addresses().map_err(custom_err)?; let mut res: Vec<_> = addrs.into_iter().collect(); diff --git a/cli/lib.rs b/cli/lib.rs index 9aefccbf..477af15b 100644 --- a/cli/lib.rs +++ b/cli/lib.rs @@ -44,6 +44,8 @@ pub enum Command { }, /// Get a new address GetNewAddress, + /// Get transaction by txid + GetTransaction { txid: Txid }, /// Get wallet addresses, sorted by base58 encoding GetWalletAddresses, /// Get wallet UTXOs @@ -166,6 +168,10 @@ where let address = rpc_client.get_new_address().await?; format!("{address}") } + Command::GetTransaction { txid } => { + let tx_info = rpc_client.get_transaction(txid).await?; + serde_json::to_string_pretty(&tx_info)? + } Command::GetWalletAddresses => { let addresses = rpc_client.get_wallet_addresses().await?; serde_json::to_string_pretty(&addresses)? diff --git a/lib/archive.rs b/lib/archive.rs index ac6cc205..95222d0c 100644 --- a/lib/archive.rs +++ b/lib/archive.rs @@ -1,6 +1,6 @@ use std::{ cmp::Ordering, - collections::{HashMap, HashSet}, + collections::{BTreeMap, HashMap, HashSet}, path::PathBuf, }; @@ -8,16 +8,22 @@ use bitcoin::{self, hashes::Hash as _}; use fallible_iterator::{FallibleIterator, IteratorExt}; use heed::types::SerdeBincode; use sneed::{ - DatabaseUnique, EnvError, RoTxn, RwTxn, UnitKey, - db::error::Error as DbError, rwtxn::Error as RwTxnError, + DatabaseUnique, DbError, EnvError, RoTxn, RwTxn, RwTxnError, UnitKey, db, + env, }; use crate::types::{ - Accumulator, BlockHash, BmmResult, Body, Header, Tip, VERSION, Version, - proto::mainchain, + Accumulator, BlockHash, BmmResult, Body, Header, Tip, Txid, VERSION, + Version, proto::mainchain, }; -#[derive(Debug, thiserror::Error)] +#[allow(clippy::duplicated_attributes)] +#[derive(Debug, thiserror::Error, transitive::Transitive)] +#[transitive( + from(db::error::Put, DbError), + from(db::error::TryGet, DbError), + from(env::error::CreateDb, EnvError) +)] pub enum Error { #[error(transparent)] Db(#[from] DbError), @@ -136,11 +142,16 @@ pub struct Archive { SerdeBincode, SerdeBincode, >, + /// Blocks in which a tx has been included, and index within the block + txid_to_inclusions: DatabaseUnique< + SerdeBincode, + SerdeBincode>, + >, _version: DatabaseUnique>, } impl Archive { - pub const NUM_DBS: u32 = 14; + pub const NUM_DBS: u32 = 15; pub fn new(env: &sneed::Env) -> Result { let mut rwtxn = env.write_txn().map_err(EnvError::from)?; @@ -152,10 +163,11 @@ impl Archive { if db_version < Version { major: 0, - minor: 13, + minor: 15, patch: 0, } => { + // `txid_to_inclusions` added in 0.15.0 // Merkle root structure changed in 0.13.0 // `deposits` and `main_bmm_commitments` were removed in // 0.12.0, and `main_block_infos` was added @@ -229,6 +241,8 @@ impl Archive { } let total_work = DatabaseUnique::create(env, &mut rwtxn, "total_work") .map_err(EnvError::from)?; + let txid_to_inclusions = + DatabaseUnique::create(env, &mut rwtxn, "txid_to_inclusions")?; rwtxn.commit().map_err(RwTxnError::from)?; Ok(Self { accumulators, @@ -244,6 +258,7 @@ impl Archive { main_successors, successors, total_work, + txid_to_inclusions, _version: version, }) } @@ -496,6 +511,19 @@ impl Archive { .ok_or(Error::NoMainHeaderInfo(block_hash)) } + /// Get blocks in which a tx was included, and tx index within each block + pub fn get_tx_inclusions( + &self, + rotxn: &RoTxn, + txid: Txid, + ) -> Result, Error> { + let inclusions = self + .txid_to_inclusions + .try_get(rotxn, &txid)? + .unwrap_or_default(); + Ok(inclusions) + } + /// Try to get the best valid mainchain verification for the specified block. pub fn try_get_best_main_verification( &self, @@ -690,7 +718,16 @@ impl Archive { self.bodies .put(rwtxn, &block_hash, body) .map_err(DbError::from)?; - Ok(()) + body.transactions + .iter() + .enumerate() + .try_for_each(|(txin, tx)| { + let txid = tx.txid(); + let mut inclusions = self.get_tx_inclusions(rwtxn, txid)?; + inclusions.insert(block_hash, txin as u32); + self.txid_to_inclusions.put(rwtxn, &txid, &inclusions)?; + Ok(()) + }) } /// Store a header. diff --git a/lib/mempool.rs b/lib/mempool.rs index d9ee0156..183ed9b7 100644 --- a/lib/mempool.rs +++ b/lib/mempool.rs @@ -3,8 +3,7 @@ use std::collections::VecDeque; use fallible_iterator::FallibleIterator as _; use heed::types::SerdeBincode; use sneed::{ - DatabaseUnique, EnvError, RoTxn, RwTxn, RwTxnError, UnitKey, - db::error::Error as DbError, + DatabaseUnique, DbError, EnvError, RoTxn, RwTxn, RwTxnError, UnitKey, db, }; use crate::types::{ @@ -12,7 +11,9 @@ use crate::types::{ Version, }; -#[derive(Debug, thiserror::Error)] +#[allow(clippy::duplicated_attributes)] +#[derive(Debug, thiserror::Error, transitive::Transitive)] +#[transitive(from(db::error::TryGet, DbError))] pub enum Error { #[error(transparent)] Db(#[from] DbError), diff --git a/lib/node/mod.rs b/lib/node/mod.rs index 44c1333f..35582b8e 100644 --- a/lib/node/mod.rs +++ b/lib/node/mod.rs @@ -7,7 +7,7 @@ use std::{ }; use bitcoin::amount::CheckedSum; -use fallible_iterator::FallibleIterator; +use fallible_iterator::{FallibleIterator, IteratorExt}; use futures::{Stream, future::BoxFuture}; use sneed::{DbError, Env, EnvError, RwTxnError, env}; use tokio::sync::Mutex; @@ -497,6 +497,41 @@ where Ok((returned_transactions, fee)) } + /// Get a transaction if it exists in the active chain or mempool. + /// Returns the transaction and the block it was included in, if it exists + /// in the active chain. + pub fn try_get_transaction( + &self, + txid: Txid, + ) -> Result)>, Error> { + let rotxn = self.env.read_txn()?; + let tip = self.state.try_get_tip(&rotxn)?; + if let Some(tip) = tip + && let Some((block_hash, txin)) = self + .archive + .get_tx_inclusions(&rotxn, txid)? + .into_iter() + .map(Ok) + .transpose_into_fallible() + .find(|(block_hash, _idx)| { + self.archive.is_descendant(&rotxn, tip, *block_hash) + })? + { + let body = self.archive.get_body(&rotxn, block_hash)?; + let tx = body.transactions.into_iter().nth(txin as usize).unwrap(); + Ok(Some((tx, Some(block_hash)))) + } else if let Some(auth_tx) = self + .mempool + .transactions + .try_get(&rotxn, &txid) + .map_err(mempool::Error::from)? + { + Ok(Some((auth_tx.transaction, None))) + } else { + Ok(None) + } + } + pub fn try_get_pending_withdrawal_bundle( &self, ) -> Result, Error> { diff --git a/rpc-api/lib.rs b/rpc-api/lib.rs index 7ceb11d1..b365447c 100644 --- a/rpc-api/lib.rs +++ b/rpc-api/lib.rs @@ -4,17 +4,27 @@ use std::net::SocketAddr; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; use l2l_openapi::open_api; +use serde::{Deserialize, Serialize}; use thunder::{ net::Peer, types::{ - Address, MerkleRoot, OutPoint, Output, OutputContent, PointedOutput, - Txid, WithdrawalBundle, schema as thunder_schema, + Address, BlockHash, MerkleRoot, OutPoint, Output, OutputContent, + PointedOutput, Transaction, Txid, WithdrawalBundle, + schema as thunder_schema, }, wallet::Balance, }; +use utoipa::ToSchema; mod schema; +#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] +pub struct GetTransactionResponse { + pub tx: Transaction, + /// Block hash, if in the active chain + pub block_hash: Option, +} + #[open_api(ref_schemas[ Address, MerkleRoot, OutPoint, Output, OutputContent, Txid, schema::BitcoinTxid, thunder_schema::BitcoinAddr, @@ -109,6 +119,13 @@ pub trait Rpc { #[method(name = "get_new_address")] async fn get_new_address(&self) -> RpcResult
; + /// Get transaction by txid + #[method(name = "get_transaction")] + async fn get_transaction( + &self, + txid: Txid, + ) -> RpcResult>; + /// Get wallet addresses, sorted by base58 encoding #[method(name = "get_wallet_addresses")] async fn get_wallet_addresses(&self) -> RpcResult>;