diff --git a/doc/API.md b/doc/API.md index e85b4f0c..0d2ca2d5 100644 --- a/doc/API.md +++ b/doc/API.md @@ -218,12 +218,14 @@ This command does not return anything for now. List stored Spend transactions. + +If `txids` is specified, only list transactions whose `txid` is in `txids`(empty list of `txids` is not allowed). + #### Request -This command does not take any parameter for now. - -| Field | Type | Description | -| ------------- | ----------------- | ----------------------------------------------------------- | +| Field | Type | Description | +| ------------- | -------------------------- | ------------------------------------ | +| `txids` | array of string (optional) | Ids of the transactions to retrieve | #### Response diff --git a/src/commands/mod.rs b/src/commands/mod.rs index bf473bf6..df1ce073 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -62,6 +62,7 @@ pub enum CommandError { /// Overflowing or unhardened derivation index. InvalidDerivationIndex, RbfError(RbfErrorInfo), + EmptyFilterList, } impl fmt::Display for CommandError { @@ -114,6 +115,7 @@ impl fmt::Display for CommandError { write!(f, "Unhardened or overflowing BIP32 derivation index.") } Self::RbfError(e) => write!(f, "RBF error: '{}'.", e), + Self::EmptyFilterList => write!(f, "Filter list is empty, should supply None instead."), } } } @@ -589,14 +591,32 @@ impl DaemonControl { } } - pub fn list_spend(&self) -> ListSpendResult { + pub fn list_spend( + &self, + txids: Option>, + ) -> Result { + if let Some(ids) = &txids { + if ids.is_empty() { + return Err(CommandError::EmptyFilterList); + } + } + let mut db_conn = self.db.connection(); - let spend_txs = db_conn - .list_spend() + let spend_psbts = db_conn.list_spend(); + + let txids_set: Option> = txids.as_ref().map(|list| list.iter().collect()); + let spend_txs = spend_psbts .into_iter() - .map(|(psbt, updated_at)| ListSpendEntry { psbt, updated_at }) + .filter_map(|(psbt, updated_at)| { + if let Some(set) = &txids_set { + if !set.contains(&psbt.unsigned_tx.txid()) { + return None; + } + } + Some(ListSpendEntry { psbt, updated_at }) + }) .collect(); - ListSpendResult { spend_txs } + Ok(ListSpendResult { spend_txs }) } pub fn delete_spend(&self, txid: &bitcoin::Txid) { diff --git a/src/jsonrpc/api.rs b/src/jsonrpc/api.rs index f99ee9ef..8bc7fcab 100644 --- a/src/jsonrpc/api.rs +++ b/src/jsonrpc/api.rs @@ -10,7 +10,7 @@ use std::{ str::FromStr, }; -use miniscript::bitcoin::{self, psbt::Psbt}; +use miniscript::bitcoin::{self, psbt::Psbt, Txid}; fn create_spend(control: &DaemonControl, params: Params) -> Result { let destinations = params @@ -226,6 +226,32 @@ fn list_confirmed(control: &DaemonControl, params: Params) -> Result, +) -> Result { + let txids: Option> = if let Some(p) = params { + let tx_ids = p.get(0, "txids"); + if let Some(ids) = tx_ids { + let ids: Vec = ids + .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."))?; + Some(ids) + } else { + None + } + } else { + None + }; + + Ok(serde_json::json!(&control.list_spend(txids)?)) +} + fn list_transactions(control: &DaemonControl, params: Params) -> Result { let txids: Vec = params .get(0, "txids") @@ -391,7 +417,7 @@ pub fn handle_request(control: &DaemonControl, req: Request) -> Result serde_json::json!(&control.list_spend()), + "listspendtxs" => list_spendtxs(control, req.params)?, "listtransactions" => { let params = req.params.ok_or_else(|| { Error::invalid_params( diff --git a/src/jsonrpc/mod.rs b/src/jsonrpc/mod.rs index e21ac0e7..131a96a6 100644 --- a/src/jsonrpc/mod.rs +++ b/src/jsonrpc/mod.rs @@ -165,6 +165,7 @@ impl From for Error { | commands::CommandError::AlreadyRescanning | commands::CommandError::InvalidDerivationIndex | commands::CommandError::RbfError(..) + | commands::CommandError::EmptyFilterList | commands::CommandError::RecoveryNotAvailable => { Error::new(ErrorCode::InvalidParams, e.to_string()) } diff --git a/tests/test_rpc.py b/tests/test_rpc.py index 904d17b8..5157f87d 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -22,7 +22,7 @@ from test_framework.utils import ( def test_getinfo(lianad): res = lianad.rpc.getinfo() - assert 'timestamp' in res.keys() + assert "timestamp" in res.keys() assert res["version"] == "4.0.0-dev" assert res["network"] == "regtest" wait_for(lambda: lianad.rpc.getinfo()["block_height"] == 101) @@ -458,6 +458,35 @@ def test_list_spend(lianad, bitcoind): lianad.rpc.updatespend(res["psbt"]) lianad.rpc.updatespend(res_b["psbt"]) + # Check 'txids' parameter + list_res = lianad.rpc.listspendtxs()["spend_txs"] + + txid = PSBT.from_base64(list_res[0]["psbt"]).tx.txid().hex() + + filtered_res = lianad.rpc.listspendtxs(txids=[txid]) + assert filtered_res["spend_txs"][0]["psbt"] == list_res[0]["psbt"] + assert len(filtered_res) == 1 + + with pytest.raises( + RpcError, match="Filter list is empty, should supply None instead." + ): + lianad.rpc.listspendtxs(txids=[]) + + with pytest.raises(RpcError, match="Invalid params: Invalid 'txids' parameter."): + lianad.rpc.listspendtxs(txids=[txid, 123]) + + with pytest.raises(RpcError, match="Invalid params: Invalid 'txids' parameter."): + lianad.rpc.listspendtxs(txids=[0]) + + with pytest.raises(RpcError, match="Invalid params: Invalid 'txids' parameter."): + lianad.rpc.listspendtxs(txids=[123]) + + with pytest.raises(RpcError, match="Invalid params: Invalid 'txids' parameter."): + lianad.rpc.listspendtxs(txids=["abc"]) + + with pytest.raises(RpcError, match="Invalid params: Invalid 'txids' parameter."): + lianad.rpc.listspendtxs(txids=["123"]) + # Listing all Spend transactions will list them both. It'll tell us which one has # change and which one doesn't. list_res = lianad.rpc.listspendtxs()["spend_txs"] @@ -1232,9 +1261,7 @@ def test_rbfpsbt_cancel(lianad, bitcoind): # But we can't set the feerate explicitly. with pytest.raises( RpcError, - match=re.escape( - "A feerate must not be provided if creating a cancel." - ), + match=re.escape("A feerate must not be provided if creating a cancel."), ): rbf_1_res = lianad.rpc.rbfpsbt(first_txid, True, 2) rbf_1_psbt = PSBT.from_base64(rbf_1_res["psbt"])