rpc: allow to choose outpoints in createrecovery
To maintain backwards compatibility, the `outpoints` parameter is the final positional argument and can be omitted entirely.
This commit is contained in:
parent
8cc723fb3b
commit
d4151d88d6
21
doc/API.md
21
doc/API.md
@ -406,8 +406,13 @@ Confirmation time is based on the timestamp of blocks.
|
||||
|
||||
### `createrecovery`
|
||||
|
||||
Create a transaction that sweeps all coins for which a timelocked recovery path is
|
||||
currently available to a provided address with the provided feerate.
|
||||
Create a transaction that sweeps coins using a timelocked recovery path to a provided address
|
||||
with the provided feerate.
|
||||
|
||||
If `outpoints` is empty or missing, then all coins for which the given recovery path is currently
|
||||
available will be used. Otherwise, only those specified will be considered. An error will
|
||||
be returned if any coins specified by `outpoints` are unknown, already spent or otherwise
|
||||
not currently recoverable using the given recovery path.
|
||||
|
||||
The `timelock` parameter can be used to specify which recovery path to use. By default,
|
||||
we'll use the first recovery path available. If created for a later timelock a recovery
|
||||
@ -421,11 +426,13 @@ cover the requested feerate.
|
||||
|
||||
#### Request
|
||||
|
||||
| Field | Type | Description |
|
||||
| ---------- | ----------------- | ----------------------------------------------------------------------------------------- |
|
||||
| `address` | str | The Bitcoin address to sweep the coins to. |
|
||||
| `feerate` | integer | Target feerate for the transaction, in satoshis per virtual byte. |
|
||||
| `timelock` | int or `null` | Recovery path to be used, identified by the number of blocks after which it is available. |
|
||||
| Field | Type | Description |
|
||||
| ---------- | ---------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| `address` | str | The Bitcoin address to sweep the coins to. |
|
||||
| `feerate` | integer | Target feerate for the transaction, in satoshis per virtual byte. |
|
||||
| `timelock` | int or `null` | Recovery path to be used, identified by the number of blocks after which it is available. |
|
||||
| `outpoints`| list of str (optional) | List of the coins to be recovered, as `txid:vout`. |
|
||||
|
||||
|
||||
#### Response
|
||||
|
||||
|
||||
@ -185,6 +185,7 @@ impl<C: Client + Send + Sync + Debug> Daemon for Lianad<C> {
|
||||
feerate_vb: u64,
|
||||
sequence: Option<u16>,
|
||||
) -> Result<Psbt, DaemonError> {
|
||||
// The `outpoints` parameter is omitted, which means all recoverable coins will be used.
|
||||
let res: CreateRecoveryResult = self.call(
|
||||
"createrecovery",
|
||||
Some(vec![json!(address), json!(feerate_vb), json!(sequence)]),
|
||||
|
||||
@ -342,8 +342,26 @@ fn create_recovery(control: &DaemonControl, params: Params) -> Result<serde_json
|
||||
.ok_or_else(|| Error::invalid_params("Invalid 'timelock' parameter."))
|
||||
})
|
||||
.transpose()?;
|
||||
let outpoints = params
|
||||
.get(3, "outpoints")
|
||||
.map(|param| {
|
||||
param
|
||||
.as_array()
|
||||
.and_then(|arr| {
|
||||
arr.iter()
|
||||
.map(|entry| {
|
||||
entry
|
||||
.as_str()
|
||||
.and_then(|e| bitcoin::OutPoint::from_str(e).ok())
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()
|
||||
})
|
||||
.ok_or_else(|| Error::invalid_params("Invalid 'outpoints' parameter."))
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or_default(); // missing is same as empty array
|
||||
|
||||
let res = control.create_recovery(address, &[], feerate, timelock)?;
|
||||
let res = control.create_recovery(address, &outpoints, feerate, timelock)?;
|
||||
Ok(serde_json::json!(&res))
|
||||
}
|
||||
|
||||
|
||||
@ -1037,6 +1037,7 @@ def test_create_recovery(lianad, bitcoind):
|
||||
wait_for(
|
||||
lambda: lianad.rpc.getinfo()["block_height"] == bitcoind.rpc.getblockcount()
|
||||
)
|
||||
first_outpoints = [c["outpoint"] for c in lianad.rpc.listcoins()["coins"]]
|
||||
|
||||
# There's nothing to sweep
|
||||
with pytest.raises(
|
||||
@ -1044,6 +1045,26 @@ def test_create_recovery(lianad, bitcoind):
|
||||
match="No coin currently spendable through this timelocked recovery path",
|
||||
):
|
||||
lianad.rpc.createrecovery(bitcoind.rpc.getnewaddress(), 2)
|
||||
# Same if we specify timelock:
|
||||
with pytest.raises(
|
||||
RpcError,
|
||||
match="No coin currently spendable through this timelocked recovery path",
|
||||
):
|
||||
lianad.rpc.createrecovery(bitcoind.rpc.getnewaddress(), 2, 10)
|
||||
# And if we use empty array for outpoints:
|
||||
with pytest.raises(
|
||||
RpcError,
|
||||
match="No coin currently spendable through this timelocked recovery path",
|
||||
):
|
||||
lianad.rpc.createrecovery(bitcoind.rpc.getnewaddress(), 2, 10, [])
|
||||
# If we specify a coin, the error will be different:
|
||||
with pytest.raises(
|
||||
RpcError,
|
||||
match=f"Coin at '{first_outpoints[0]}' is not recoverable with timelock '10'",
|
||||
):
|
||||
lianad.rpc.createrecovery(
|
||||
bitcoind.rpc.getnewaddress(), 2, 10, [f"{first_outpoints[0]}"]
|
||||
)
|
||||
|
||||
# Receive another coin, it will be one block after the others
|
||||
txid = bitcoind.rpc.sendtoaddress(lianad.rpc.getnewaddress()["address"], 0.4)
|
||||
@ -1055,9 +1076,17 @@ def test_create_recovery(lianad, bitcoind):
|
||||
wait_for(
|
||||
lambda: lianad.rpc.getinfo()["block_height"] == bitcoind.rpc.getblockcount()
|
||||
)
|
||||
res = lianad.rpc.createrecovery(bitcoind.rpc.getnewaddress(), 18)
|
||||
new_outpoint = [
|
||||
c["outpoint"] for c in lianad.rpc.listcoins()["coins"] if txid in c["outpoint"]
|
||||
][0]
|
||||
reco_address = bitcoind.rpc.getnewaddress()
|
||||
res = lianad.rpc.createrecovery(reco_address, 18)
|
||||
reco_psbt = PSBT.from_base64(res["psbt"])
|
||||
|
||||
# Do the same passing all three coins explicitly:
|
||||
res_op = lianad.rpc.createrecovery(reco_address, 18, 10, first_outpoints)
|
||||
reco_psbt_op = PSBT.from_base64(res_op["psbt"])
|
||||
|
||||
# Check locktime being set correctly.
|
||||
tip_height = bitcoind.rpc.getblockcount()
|
||||
assert tip_height > 100
|
||||
@ -1065,8 +1094,34 @@ def test_create_recovery(lianad, bitcoind):
|
||||
assert tip_height - 100 <= locktime <= tip_height
|
||||
|
||||
assert len(reco_psbt.tx.vin) == 3, "The last coin's timelock hasn't matured yet"
|
||||
assert len(reco_psbt.tx.vout) == 1
|
||||
assert len(reco_psbt.tx.vout) == len(reco_psbt_op.tx.vout) == 1
|
||||
# The inputs are the same for both explicit and implicit outpoints:
|
||||
assert sorted(i.prevout.serialize() for i in reco_psbt.tx.vin) == sorted(
|
||||
i.prevout.serialize() for i in reco_psbt_op.tx.vin
|
||||
)
|
||||
assert reco_psbt.tx.vout[0].nValue == reco_psbt_op.tx.vout[0].nValue
|
||||
assert reco_psbt.tx.vout[0].scriptPubKey == reco_psbt_op.tx.vout[0].scriptPubKey
|
||||
assert int(0.5999 * COIN) < int(reco_psbt.tx.vout[0].nValue) < int(0.6 * COIN)
|
||||
|
||||
# Now use only 2 of the 3 coins:
|
||||
res_op_2 = lianad.rpc.createrecovery(
|
||||
bitcoind.rpc.getnewaddress(), 18, 10, first_outpoints[:2]
|
||||
)
|
||||
reco_psbt_op_2 = PSBT.from_base64(res_op_2["psbt"])
|
||||
assert len(reco_psbt_op_2.tx.vin) == 2
|
||||
assert sorted(
|
||||
f"{i.prevout.hash:064x}:{i.prevout.n}" for i in reco_psbt_op_2.tx.vin
|
||||
) == sorted(first_outpoints[:2])
|
||||
|
||||
# If we try to include the newest coin, an error will be returned:
|
||||
with pytest.raises(
|
||||
RpcError,
|
||||
match=f"Coin at '{new_outpoint}' is not recoverable with timelock '10'",
|
||||
):
|
||||
lianad.rpc.createrecovery(
|
||||
bitcoind.rpc.getnewaddress(), 2, 10, [first_outpoints[0], new_outpoint]
|
||||
)
|
||||
|
||||
txid = sign_and_broadcast(lianad, bitcoind, reco_psbt, recovery=True)
|
||||
|
||||
# And by mining one more block we'll be able to sweep the last coin.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user