From 361607c6b4568c25e528e4be76d5a76c1e6f49a6 Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Fri, 3 May 2024 04:11:04 +0800 Subject: [PATCH] add get_transaction and get_transaction_info RPCs --- Cargo.lock | 9 ++--- Cargo.toml | 2 +- app/app.rs | 4 +-- app/rpc_server.rs | 46 ++++++++++++++++++++++++- lib/archive.rs | 27 ++++++++------- lib/lib.rs | 1 + lib/node.rs | 83 ++++++++++++++++++++++++++++++++++++++++++---- lib/state.rs | 2 +- lib/types/mod.rs | 7 ++++ rpc-api/Cargo.toml | 1 + rpc-api/lib.rs | 24 +++++++++++++- 11 files changed, 178 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 26eefca..25b4cc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3643,7 +3643,7 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "plain_bitnames" -version = "0.7.0" +version = "0.7.1" dependencies = [ "addr", "anyhow", @@ -3688,7 +3688,7 @@ dependencies = [ [[package]] name = "plain_bitnames_app" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "async_zmq", @@ -3727,7 +3727,7 @@ dependencies = [ [[package]] name = "plain_bitnames_app_cli" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "bip300301", @@ -3741,11 +3741,12 @@ dependencies = [ [[package]] name = "plain_bitnames_app_rpc_api" -version = "0.7.0" +version = "0.7.1" dependencies = [ "bip300301", "jsonrpsee", "plain_bitnames", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 39efc0e..2e60e3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ members = [ [workspace.package] authors = [ "Ash Manning " ] edition = "2021" -version = "0.7.0" +version = "0.7.1" [workspace.dependencies.bip300301] git = "https://github.com/Ash-L2L/bip300301.git" diff --git a/app/app.rs b/app/app.rs index 8db5715..69a84ce 100644 --- a/app/app.rs +++ b/app/app.rs @@ -164,7 +164,7 @@ impl App { .map(|(outpoint, txid)| { let inclusions = self.node.get_tx_inclusions(txid)?; let Some(block_hash) = - inclusions.into_iter().try_find(|block_hash| { + inclusions.into_keys().try_find(|block_hash| { self.node.is_descendant(*block_hash, tip) })? else { @@ -184,7 +184,7 @@ impl App { ) }; let inclusions = self.node.get_tx_inclusions(txid)?; - let Some(block_hash) = inclusions.into_iter().try_find(|block_hash| { + let Some(block_hash) = inclusions.into_keys().try_find(|block_hash| { self.node.is_descendant(*block_hash, tip) })? else { return Ok((spent_output.inpoint, None)); diff --git a/app/rpc_server.rs b/app/rpc_server.rs index 24455ce..23b786d 100644 --- a/app/rpc_server.rs +++ b/app/rpc_server.rs @@ -15,7 +15,7 @@ use plain_bitnames::{ }, wallet, }; -use plain_bitnames_app_rpc_api::RpcServer; +use plain_bitnames_app_rpc_api::{RpcServer, TxInfo}; use crate::app::{self, App}; @@ -94,6 +94,50 @@ impl RpcServer for RpcServerImpl { self.app.get_paymail(None).map_err(convert_app_err) } + async fn get_transaction( + &self, + txid: Txid, + ) -> RpcResult> { + self.app + .node + .try_get_transaction(txid) + .map_err(convert_node_err) + } + + async fn get_transaction_info( + &self, + txid: Txid, + ) -> RpcResult> { + let Some((filled_tx, txin)) = self + .app + .node + .try_get_filled_transaction(txid) + .map_err(convert_node_err)? + else { + return Ok(None); + }; + let confirmations = match txin { + Some(txin) => { + let tip_height = + self.app.node.get_tip_height().map_err(convert_node_err)?; + let height = self + .app + .node + .get_height(txin.block_hash) + .map_err(convert_node_err)?; + Some(tip_height - height) + } + None => None, + }; + let fee_sats = filled_tx.transaction.fee().unwrap(); + let res = TxInfo { + confirmations, + fee_sats, + txin, + }; + Ok(Some(res)) + } + async fn getblockcount(&self) -> RpcResult { self.app.node.get_tip_height().map_err(convert_node_err) } diff --git a/lib/archive.rs b/lib/archive.rs index 465618b..f91a145 100644 --- a/lib/archive.rs +++ b/lib/archive.rs @@ -1,4 +1,4 @@ -use std::{cmp::Ordering, collections::BTreeSet}; +use std::{cmp::Ordering, collections::BTreeMap}; use bip300301::{ bitcoin::{self, block::Header as BitcoinHeader, hashes::Hash}, @@ -56,9 +56,9 @@ pub struct Archive { /// All ancestors of any block should always be present total_work: Database, SerdeBincode>, - /// Blocks in which a tx has been included + /// Blocks in which a tx has been included, and index within the block txid_to_inclusions: - Database, SerdeBincode>>, + Database, SerdeBincode>>, } impl Archive { @@ -255,12 +255,12 @@ impl Archive { .ok_or(Error::NoMainHeader(block_hash)) } - /// Get blocks in which a tx was included. + /// 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> { + ) -> Result, Error> { let inclusions = self .txid_to_inclusions .get(rotxn, &txid)? @@ -289,13 +289,16 @@ impl Archive { ) -> Result<(), Error> { let _header = self.get_header(rwtxn, block_hash)?; self.bodies.put(rwtxn, &block_hash, body)?; - body.transactions.iter().try_for_each(|tx| { - let txid = tx.txid(); - let mut inclusions = self.get_tx_inclusions(rwtxn, txid)?; - inclusions.insert(block_hash); - self.txid_to_inclusions.put(rwtxn, &txid, &inclusions)?; - 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 deposit info for a block diff --git a/lib/lib.rs b/lib/lib.rs index 01c9136..80cbb87 100644 --- a/lib/lib.rs +++ b/lib/lib.rs @@ -1,4 +1,5 @@ #![feature(let_chains)] +#![feature(try_find)] pub mod archive; pub mod authorization; diff --git a/lib/node.rs b/lib/node.rs index f08b6b6..952ae03 100644 --- a/lib/node.rs +++ b/lib/node.rs @@ -1,5 +1,5 @@ use std::{ - collections::{BTreeSet, HashMap, HashSet}, + collections::{BTreeMap, HashMap, HashSet}, fmt::Debug, net::SocketAddr, path::Path, @@ -35,7 +35,8 @@ use crate::{ types::{ Address, Authorized, AuthorizedTransaction, BitName, BitNameData, Block, BlockHash, Body, FilledOutput, FilledTransaction, GetValue, - Header, MerkleRoot, OutPoint, SpentOutput, Txid, WithdrawalBundle, + Header, MerkleRoot, OutPoint, SpentOutput, Transaction, TxIn, Txid, + WithdrawalBundle, }, }; @@ -771,6 +772,9 @@ impl NetTask { } } +pub type FilledTransactionWithPosition = + (Authorized, Option); + #[derive(Clone)] pub struct Node { archive: Archive, @@ -893,11 +897,11 @@ impl Node { Ok(self.archive.get_height(&rotxn, block_hash)?) } - /// Get blocks in which a tx was included. + /// Get blocks in which a tx was included, and tx index within those blocks pub fn get_tx_inclusions( &self, txid: Txid, - ) -> Result, Error> { + ) -> Result, Error> { let rotxn = self.env.read_txn()?; Ok(self.archive.get_tx_inclusions(&rotxn, txid)?) } @@ -1113,13 +1117,80 @@ impl Node { Ok((returned_transactions, fee)) } + /// get a transaction from the archive or mempool, if it exists + pub fn try_get_transaction( + &self, + txid: Txid, + ) -> Result, Error> { + let rotxn = self.env.read_txn()?; + if let Some((block_hash, txin)) = self + .archive + .get_tx_inclusions(&rotxn, txid)? + .first_key_value() + { + let body = self.archive.get_body(&rotxn, *block_hash)?; + let tx = body.transactions.into_iter().nth(*txin as usize).unwrap(); + Ok(Some(tx)) + } else if let Some(auth_tx) = + self.mempool.transactions.get(&rotxn, &txid)? + { + Ok(Some(auth_tx.transaction)) + } else { + Ok(None) + } + } + + /// get a filled transaction from the archive/state or mempool, + /// and the tx index, if the transaction exists + /// and can be filled with the current state. + /// a tx index of `None` indicates that the tx is in the mempool. + pub fn try_get_filled_transaction( + &self, + txid: Txid, + ) -> Result, Error> { + let rotxn = self.env.read_txn()?; + let tip = self.state.get_tip(&rotxn)?; + let inclusions = self.archive.get_tx_inclusions(&rotxn, txid)?; + if let Some((block_hash, idx)) = + inclusions.into_iter().try_find(|(block_hash, _)| { + self.archive.is_descendant(&rotxn, *block_hash, tip) + })? + { + let body = self.archive.get_body(&rotxn, block_hash)?; + let auth_txs = body.authorized_transactions(); + let auth_tx = auth_txs.into_iter().nth(idx as usize).unwrap(); + let filled_tx = self + .state + .fill_transaction_from_stxos(&rotxn, auth_tx.transaction)?; + let auth_tx = Authorized { + transaction: filled_tx, + authorizations: auth_tx.authorizations, + }; + let txin = TxIn { block_hash, idx }; + let res = (auth_tx, Some(txin)); + return Ok(Some(res)); + } + if let Some(auth_tx) = self.mempool.transactions.get(&rotxn, &txid)? { + match self.state.fill_authorized_transaction(&rotxn, auth_tx) { + Ok(filled_tx) => { + let res = (filled_tx, None); + Ok(Some(res)) + } + Err(state::Error::NoUtxo { .. }) => Ok(None), + Err(err) => Err(err.into()), + } + } else { + Ok(None) + } + } + pub fn get_pending_withdrawal_bundle( &self, ) -> Result, Error> { - let txn = self.env.read_txn()?; + let rotxn = self.env.read_txn()?; let bundle = self .state - .get_pending_withdrawal_bundle(&txn)? + .get_pending_withdrawal_bundle(&rotxn)? .map(|(bundle, _)| bundle); Ok(bundle) } diff --git a/lib/state.rs b/lib/state.rs index 78021c7..96f14b1 100644 --- a/lib/state.rs +++ b/lib/state.rs @@ -581,7 +581,7 @@ impl State { } /// Fill a transaction that has already been applied - fn fill_transaction_from_stxos( + pub fn fill_transaction_from_stxos( &self, rotxn: &RoTxn, tx: Transaction, diff --git a/lib/types/mod.rs b/lib/types/mod.rs index 4a2d476..49c522e 100644 --- a/lib/types/mod.rs +++ b/lib/types/mod.rs @@ -465,3 +465,10 @@ impl PartialOrd for AggregatedWithdrawal { Some(self.cmp(other)) } } + +/// Transaction index +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +pub struct TxIn { + pub block_hash: BlockHash, + pub idx: u32, +} diff --git a/rpc-api/Cargo.toml b/rpc-api/Cargo.toml index eb87b24..f1621f9 100644 --- a/rpc-api/Cargo.toml +++ b/rpc-api/Cargo.toml @@ -8,6 +8,7 @@ version.workspace = true bip300301.workspace = true jsonrpsee = { version = "0.20.0", features = ["macros"] } plain_bitnames = { path = "../lib" } +serde = { version = "1.0.179", features = ["derive"] } [lib] name = "plain_bitnames_app_rpc_api" diff --git a/rpc-api/lib.rs b/rpc-api/lib.rs index 4b74b12..781f865 100644 --- a/rpc-api/lib.rs +++ b/rpc-api/lib.rs @@ -6,8 +6,16 @@ use bip300301::bitcoin; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; use plain_bitnames::types::{ hashes::BitName, Address, BitNameData, Block, BlockHash, FilledOutput, - OutPoint, Txid, + OutPoint, Transaction, TxIn, Txid, }; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct TxInfo { + pub confirmations: Option, + pub fee_sats: u64, + pub txin: Option, +} #[rpc(client, server)] pub trait Rpc { @@ -46,6 +54,20 @@ pub trait Rpc { #[method(name = "get_paymail")] async fn get_paymail(&self) -> RpcResult>; + /// Get transaction by txid + #[method(name = "get_transaction")] + async fn get_transaction( + &self, + txid: Txid, + ) -> RpcResult>; + + /// Get information about a transaction in the current chain + #[method(name = "get_transaction_info")] + async fn get_transaction_info( + &self, + txid: Txid, + ) -> RpcResult>; + /// Get the current block count #[method(name = "getblockcount")] async fn getblockcount(&self) -> RpcResult;