commands: add a 'spend_info' field to the 'listcoins' entries

This commit is contained in:
Antoine Poinsot 2022-10-17 10:26:09 +02:00
parent 57add1d86b
commit 99ab0d7add
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
4 changed files with 82 additions and 11 deletions

View File

@ -81,11 +81,20 @@ This command does not take any parameter for now.
#### Response
| Field | Type | Description |
| -------------- | ------------- | ---------------------------------------------------------------- |
| `amount` | int | Value of the TxO in satoshis |
| `outpoint` | string | Transaction id and output index of this coin |
| `block_height` | int or null | Blockheight the transaction was confirmed at, or `null` |
| Field | Type | Description |
| -------------- | ------------- | ------------------------------------------------------------------------------------------------------------------ |
| `amount` | int | Value of the TxO in satoshis. |
| `outpoint` | string | Transaction id and output index of this coin. |
| `block_height` | int or null | Blockheight the transaction was confirmed at, or `null`. |
| `spend_info` | object | Information about the transaction spending this coin. See [Spending transaction info](#spending_transaction_info). |
##### Spending transaction info
| Field | Type | Description |
| ---------- | ----------- | -------------------------------------------------------------- |
| `txid` | str | Spending transaction's id. |
| `height` | int or null | Block height the spending tx was included at, if confirmed. |
### `createspend`

View File

@ -169,10 +169,7 @@ impl DaemonControl {
pub fn get_info(&self) -> GetInfoResult {
let mut db_conn = self.db.connection();
let blockheight = db_conn
.chain_tip()
.map(|tip| tip.height)
.unwrap_or(0);
let blockheight = db_conn.chain_tip().map(|tip| tip.height).unwrap_or(0);
GetInfoResult {
version: VERSION.to_string(),
network: self.config.bitcoin_config.network,
@ -211,12 +208,19 @@ impl DaemonControl {
amount,
outpoint,
block_height,
spend_txid,
spend_block,
..
} = coin;
let spend_info = spend_txid.map(|txid| LCSpendInfo {
txid,
height: spend_block.map(|b| b.height),
});
ListCoinsEntry {
amount,
outpoint,
block_height,
spend_info,
}
})
.collect();
@ -464,7 +468,14 @@ pub struct GetAddressResult {
pub address: bitcoin::Address,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct LCSpendInfo {
pub txid: bitcoin::Txid,
/// The block height this spending transaction was confirmed at.
pub height: Option<i32>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct ListCoinsEntry {
#[serde(
serialize_with = "ser_amount",
@ -473,6 +484,8 @@ pub struct ListCoinsEntry {
pub amount: bitcoin::Amount,
pub outpoint: bitcoin::OutPoint,
pub block_height: Option<i32>,
/// Information about the transaction spending this coin.
pub spend_info: Option<LCSpendInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]

View File

@ -8,6 +8,9 @@ import subprocess
import threading
import time
from io import BytesIO
from .serializations import CTransaction, PSBT
TIMEOUT = int(os.getenv("TIMEOUT", 20))
EXECUTOR_WORKERS = int(os.getenv("EXECUTOR_WORKERS", 20))
VERBOSE = os.getenv("VERBOSE", "0") == "1"
@ -43,6 +46,35 @@ def wait_for(success, timeout=TIMEOUT, debug_fn=None):
raise ValueError("Error waiting for {}", success)
def get_txid(hex_tx):
"""Get the txid (as hex) of the given (as hex) transaction."""
tx = CTransaction()
tx.deserialize(BytesIO(bytes.fromhex(hex_tx)))
return tx.txid().hex()
def spend_coins(minisafed, bitcoind, coins):
"""Spend these coins, no matter how.
This will create a single transaction spending them all at once at the minimum
feerate. This will broadcast but not confirm the transaction.
:param coins: a list of dict as returned by listcoins. The coins must all exist.
:returns: the broadcasted transaction, as hex.
"""
total_value = sum(c["amount"] for c in coins)
destinations = {
bitcoind.rpc.getnewaddress(): total_value - 11 - 31 - 300 * len(coins)
}
res = minisafed.rpc.createspend([c["outpoint"] for c in coins], destinations, 1)
psbt = PSBT()
psbt.deserialize(res["psbt"])
tx = minisafed.sign_psbt(psbt)
bitcoind.rpc.sendrawtransaction(tx)
return tx
class RpcError(ValueError):
def __init__(self, method: str, params: dict, error: str):
super(ValueError, self).__init__(

View File

@ -1,6 +1,6 @@
from fixtures import *
from test_framework.serializations import PSBT
from test_framework.utils import wait_for, COIN
from test_framework.utils import wait_for, COIN, get_txid, spend_coins
def test_getinfo(minisafed):
@ -34,6 +34,7 @@ def test_listcoins(minisafed, bitcoind):
assert txid == res[0]["outpoint"][:64]
assert res[0]["amount"] == 1 * COIN
assert res[0]["block_height"] is None
assert res[0]["spend_info"] is None
# If the coin gets confirmed, it'll be marked as such.
bitcoind.generate_block(1, wait_for_mempool=txid)
@ -42,6 +43,22 @@ def test_listcoins(minisafed, bitcoind):
lambda: minisafed.rpc.listcoins()["coins"][0]["block_height"] == block_height
)
# Same if the coin gets spent.
spend_tx = spend_coins(minisafed, bitcoind, (res[0],))
spend_txid = get_txid(spend_tx)
wait_for(lambda: minisafed.rpc.listcoins()["coins"][0]["spend_info"] is not None)
spend_info = minisafed.rpc.listcoins()["coins"][0]["spend_info"]
assert spend_info["txid"] == spend_txid
assert spend_info["height"] is None
# And if this spending tx gets confirmed.
bitcoind.generate_block(1, wait_for_mempool=spend_txid)
curr_height = bitcoind.rpc.getblockcount()
wait_for(lambda: minisafed.rpc.getinfo()["blockheight"] == curr_height)
spend_info = minisafed.rpc.listcoins()["coins"][0]["spend_info"]
assert spend_info["txid"] == spend_txid
assert spend_info["height"] == curr_height
def test_jsonrpc_server(minisafed, bitcoind):
"""Test passing parameters as a list or a mapping."""