diff --git a/doc/API.md b/doc/API.md index 0bdadcc4..0be165b2 100644 --- a/doc/API.md +++ b/doc/API.md @@ -276,7 +276,9 @@ This command does not return anything for now. ### `rbfpsbt` -Create PSBT to replace the given transaction, which must point to a PSBT in our database, using RBF. +Create PSBT to replace, using RBF, the given transaction, which must either point to a PSBT in our database +(not necessarily broadcast) or an unconfirmed spend transaction (whether or not any associated +PSBT is saved in our database). This command can be used to either: - "cancel" the transaction: the replacement will include at least one input from the previous transaction and will have only diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index 4f4c6de1..e36df88d 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -766,7 +766,8 @@ impl DaemonControl { /// Create PSBT to replace the given transaction using RBF. /// - /// `txid` must point to a PSBT in our database. + /// `txid` must either point to a PSBT in our database (not necessarily broadcast) or an + /// unconfirmed spend transaction (whether or not any associated PSBT is saved in our database). /// /// `is_cancel` indicates whether to "cancel" the transaction by including only a single (change) /// output in the replacement or otherwise to keep the same (non-change) outputs and simply @@ -798,14 +799,20 @@ impl DaemonControl { return Err(CommandError::RbfError(RbfErrorInfo::SuperfluousFeerate)); } - let prev_psbt = db_conn - .spend_tx(txid) - .ok_or(CommandError::UnknownSpend(*txid))?; - if !prev_psbt.unsigned_tx.is_explicitly_rbf() { + let prev_tx = if let Some(psbt) = db_conn.spend_tx(txid) { + psbt.unsigned_tx + } else { + db_conn + .coins(&[CoinStatus::Spending], &[]) + .into_values() + .find(|c| c.spend_txid == Some(*txid)) + .and_then(|_| tx_getter.get_tx(txid)) + .ok_or(CommandError::UnknownSpend(*txid))? + }; + if !prev_tx.is_explicitly_rbf() { return Err(CommandError::RbfError(RbfErrorInfo::NotSignaling)); } - let prev_outpoints: Vec = prev_psbt - .unsigned_tx + let prev_outpoints: Vec = prev_tx .input .iter() .map(|txin| txin.previous_output) @@ -867,8 +874,7 @@ impl DaemonControl { ))); } // Get info about prev outputs to determine replacement outputs. - let prev_derivs: Vec<_> = prev_psbt - .unsigned_tx + let prev_derivs: Vec<_> = prev_tx .output .iter() .map(|txo| { diff --git a/tests/test_rpc.py b/tests/test_rpc.py index aa43811e..8ade015f 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -1144,13 +1144,9 @@ def test_rbfpsbt_bump_fee(lianad, bitcoind): # Using a higher feerate works. lianad.rpc.rbfpsbt(first_txid, False, 2) - # But we cannot use RBF if the PSBT is no longer in the DB. + # We can still use RBF if the PSBT is no longer in the DB. lianad.rpc.delspendtx(first_txid) - with pytest.raises(RpcError, match=f"Unknown spend transaction '{first_txid}'."): - lianad.rpc.rbfpsbt(first_txid, False, 2) - - # Now re-save the PSBT in the DB. - lianad.rpc.updatespend(first_psbt.to_base64()) + lianad.rpc.rbfpsbt(first_txid, False, 2) # Let's use an even higher feerate. rbf_1_res = lianad.rpc.rbfpsbt(first_txid, False, 10) @@ -1192,9 +1188,14 @@ def test_rbfpsbt_bump_fee(lianad, bitcoind): mempool_rbf_1["fees"]["ancestor"] * COIN / mempool_rbf_1["ancestorsize"] ) assert 9.75 < rbf_1_feerate < 10.25 - # If we try to RBF the first transaction again, it will use the first RBF's - # feerate to set the min feerate, instead of 1 sat/vb of first - # transaction: + # If we try to RBF the first transaction again, it will not be possible as we + # deleted the PSBT above and the tx is no longer part of our wallet's + # spending txs (even though it's saved in the DB). + with pytest.raises(RpcError, match=f"Unknown spend transaction '{first_txid}'."): + lianad.rpc.rbfpsbt(first_txid, False, 2) + # If we resave the PSBT, then we can use RBF and it will use the first RBF's + # feerate to set the min feerate, instead of 1 sat/vb of the first transaction: + lianad.rpc.updatespend(first_psbt.to_base64()) with pytest.raises( RpcError, match=f"Feerate {int(rbf_1_feerate)} too low for minimum feerate {int(rbf_1_feerate) + 1}.",