From 774f9695941757253ba34c9a61c09e70d4a3149f Mon Sep 17 00:00:00 2001 From: edouard Date: Tue, 22 Nov 2022 15:18:41 +0100 Subject: [PATCH] Add listtransaction command --- doc/API.md | 17 ++++++ src/commands/mod.rs | 142 ++++++++++++++++++++++++++++++++++++++++++++ src/jsonrpc/api.rs | 22 +++++++ 3 files changed, 181 insertions(+) diff --git a/doc/API.md b/doc/API.md index db4187d9..e94b480b 100644 --- a/doc/API.md +++ b/doc/API.md @@ -16,6 +16,7 @@ Commands must be sent as valid JSONRPC 2.0 requests, ending with a `\n`. | [`broadcastspend`](#broadcastspend) | Finalize a stored Spend PSBT, and broadcast it | | [`startrescan`](#startrescan) | Start rescanning the block chain from a given date | | [`listconfirmed`](#listconfirmed) | List of confirmed transactions of incoming and outgoing funds | +| [`listtransactions`](#listtransactions) | List of transactions with the given txids | # Reference @@ -244,3 +245,19 @@ Confirmation time is based on the timestamp of blocks. | `height` | int or `null` | Block height of the transaction, `null` if the transaction is unconfirmed | | `time` | int or `null` | Block time of the transaction, `null` if the transaction is unconfirmed | | `tx` | string | hex encoded bitcoin transaction | + +### `listtransactions` + +`listtransactions` retrieves the transactions with the given txids. + +#### Request + +| Field | Type | Description | +| ------------- | --------------- | ------------------------------------- | +| `txids` | array of string | Ids of the transactions to retrieve | + +#### Response + +| Field | Type | Description | +| -------------- | ------ | ------------------------------------------------------ | +| `transactions` | array | Array of [Transaction resource](#transaction-resource) | diff --git a/src/commands/mod.rs b/src/commands/mod.rs index feed033e..5b4ea22d 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -584,6 +584,25 @@ impl DaemonControl { .collect(); ListTransactionsResult { transactions } } + + /// list_transactions retrieves the transactions with the given txids. + pub fn list_transactions(&self, txids: &[bitcoin::Txid]) -> ListTransactionsResult { + let transactions = txids + .iter() + .filter_map(|txid| { + // TODO: batch batch those calls to the Bitcoin backend + // so it can in turn optimize its queries. + self.bitcoin + .wallet_transaction(txid) + .map(|(tx, block)| TransactionInfo { + tx, + height: block.map(|b| b.height), + time: block.map(|b| b.time), + }) + }) + .collect(); + ListTransactionsResult { transactions } + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -668,6 +687,7 @@ pub struct TransactionInfo { mod tests { use super::*; use crate::{bitcoin::Block, database::SpendBlock, testutils::*}; + use bitcoin::{ blockdata::transaction::{TxIn, TxOut}, util::bip32::ChildNumber, @@ -1162,4 +1182,126 @@ mod tests { ms.shutdown(); } + + #[test] + fn list_transactions() { + let outpoint = OutPoint::new( + Txid::from_str("617eab1fc0b03ee7f82ba70166725291783461f1a0e7975eaf8b5f8f674234f3") + .unwrap(), + 0, + ); + + let tx1: Transaction = Transaction { + version: 1, + lock_time: PackedLockTime(1), + input: vec![TxIn { + witness: Witness::new(), + previous_output: outpoint, + script_sig: Script::new(), + sequence: Sequence(0), + }], + output: vec![TxOut { + script_pubkey: Script::new(), + value: 100_000_000, + }], + }; + + let tx2: Transaction = Transaction { + version: 1, + lock_time: PackedLockTime(1), + input: vec![TxIn { + witness: Witness::new(), + previous_output: outpoint, + script_sig: Script::new(), + sequence: Sequence(0), + }], + output: vec![TxOut { + script_pubkey: Script::new(), + value: 2000, + }], + }; + + let tx3: Transaction = Transaction { + version: 1, + lock_time: PackedLockTime(1), + input: vec![TxIn { + witness: Witness::new(), + previous_output: outpoint, + script_sig: Script::new(), + sequence: Sequence(0), + }], + output: vec![TxOut { + script_pubkey: Script::new(), + value: 3000, + }], + }; + + let mut btc = DummyBitcoind::new(); + btc.txs.insert( + tx1.txid(), + ( + tx1.clone(), + Some(Block { + hash: bitcoin::BlockHash::from_str( + "0000000000000000000326b8fca8d3f820647c97ea33ef722096b3c7b2c8ee94", + ) + .unwrap(), + time: 1, + height: 1, + }), + ), + ); + btc.txs.insert( + tx2.txid(), + ( + tx2.clone(), + Some(Block { + hash: bitcoin::BlockHash::from_str( + "0000000000000000000326b8fca8d3f820647c97ea33ef722096b3c7b2c8ee94", + ) + .unwrap(), + time: 2, + height: 2, + }), + ), + ); + btc.txs.insert( + tx3.txid(), + ( + tx3.clone(), + Some(Block { + hash: bitcoin::BlockHash::from_str( + "0000000000000000000326b8fca8d3f820647c97ea33ef722096b3c7b2c8ee94", + ) + .unwrap(), + time: 4, + height: 4, + }), + ), + ); + + let ms = DummyLiana::new(btc, DummyDatabase::new()); + + let control = &ms.handle.control; + + let transactions = control.list_transactions(&[tx1.txid()]).transactions; + assert_eq!(transactions.len(), 1); + assert_eq!(transactions[0].tx, tx1); + + let transactions = control + .list_transactions(&[tx1.txid(), tx2.txid(), tx3.txid()]) + .transactions; + assert_eq!(transactions.len(), 3); + + let txs: Vec = transactions + .iter() + .map(|transaction| transaction.tx.clone()) + .collect(); + + assert!(txs.contains(&tx1)); + assert!(txs.contains(&tx2)); + assert!(txs.contains(&tx3)); + + ms.shutdown(); + } } diff --git a/src/jsonrpc/api.rs b/src/jsonrpc/api.rs index 5b0985a9..82886372 100644 --- a/src/jsonrpc/api.rs +++ b/src/jsonrpc/api.rs @@ -113,6 +113,20 @@ fn list_confirmed(control: &DaemonControl, params: Params) -> Result Result { + let txids: Vec = params + .get(0, "txids") + .ok_or_else(|| Error::invalid_params("Missing 'txids' parameter."))? + .as_array() + .and_then(|arr| { + arr.iter() + .map(|entry| entry.as_str().and_then(|e| bitcoin::Txid::from_str(e).ok())) + .collect() + }) + .ok_or_else(|| Error::invalid_params("Invalid 'txids' parameter."))?; + Ok(serde_json::json!(&control.list_transactions(&txids))) +} + fn start_rescan(control: &DaemonControl, params: Params) -> Result { let timestamp: u32 = params .get(0, "timestamp") @@ -173,6 +187,14 @@ pub fn handle_request(control: &DaemonControl, req: Request) -> Result { + let params = req.params.ok_or_else(|| { + Error::invalid_params( + "The 'listtransactions' command requires 1 parameter: 'txids'", + ) + })?; + list_transactions(control, params)? + } _ => { return Err(Error::method_not_found()); }