diff --git a/doc/API.md b/doc/API.md index 5c2cdb9b..a9e089bc 100644 --- a/doc/API.md +++ b/doc/API.md @@ -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` diff --git a/src/commands/mod.rs b/src/commands/mod.rs index b237998e..a52e85f4 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -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, +} + +#[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, + /// Information about the transaction spending this coin. + pub spend_info: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/tests/test_framework/utils.py b/tests/test_framework/utils.py index 9b1805f9..c48b0d58 100644 --- a/tests/test_framework/utils.py +++ b/tests/test_framework/utils.py @@ -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__( diff --git a/tests/test_rpc.py b/tests/test_rpc.py index 93ec85ab..44e0c06e 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -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."""