From 9d0c68dae34f96a96d318f75cbbb5dbefffdd3d8 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Tue, 16 Aug 2022 16:27:57 +0200 Subject: [PATCH] commands: add a 'list_coins' command. --- doc/API.md | 20 ++++++++++++++++++ src/commands/mod.rs | 49 ++++++++++++++++++++++++++++++++++++++++++- src/commands/utils.rs | 16 ++++++++++++++ src/jsonrpc/api.rs | 1 + tests/test_rpc.py | 25 ++++++++++++++++++++++ 5 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 src/commands/utils.rs diff --git a/doc/API.md b/doc/API.md index 3370b8c4..4d4f6997 100644 --- a/doc/API.md +++ b/doc/API.md @@ -64,3 +64,23 @@ This command does not take any parameter for now. | Field | Type | Description | | ------------- | ------ | ------------------ | | `address` | string | A Bitcoin address | + + +### `listcoins` + +List our current Unspent Transaction Outputs. + +#### Request + +This command does not take any parameter for now. + +| Field | Type | Description | +| ------------- | ----------------- | ----------------------------------------------------------- | + +#### Response + +| Field | Type | Description | +| -------------- | ------------- | ---------------------------------------------------------------- | +| `amount` | int | Value of the UTxO 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` | diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 1ecef7be..6b1ef3f0 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -2,7 +2,14 @@ //! //! External interface to the Minisafe daemon. -use crate::{bitcoin::BitcoinInterface, database::DatabaseInterface, DaemonControl, VERSION}; +mod utils; + +use crate::{ + bitcoin::BitcoinInterface, + database::{Coin, DatabaseInterface}, + DaemonControl, VERSION, +}; +use utils::{deser_amount_from_sats, ser_amount}; use miniscript::{ bitcoin, @@ -43,6 +50,30 @@ impl DaemonControl { .expect("It's a wsh() descriptor"); GetAddressResult { address } } + + /// Get a list of all currently unspent coins. + pub fn list_coins(&self) -> ListCoinsResult { + let mut db_conn = self.db.connection(); + let coins: Vec = db_conn + .unspent_coins() + // Can't use into_values as of Rust 1.48 + .into_iter() + .map(|(_, coin)| { + let Coin { + amount, + outpoint, + block_height, + .. + } = coin; + ListCoinsEntry { + amount, + outpoint, + block_height, + } + }) + .collect(); + ListCoinsResult { coins } + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -65,6 +96,22 @@ pub struct GetAddressResult { pub address: bitcoin::Address, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListCoinsEntry { + #[serde( + serialize_with = "ser_amount", + deserialize_with = "deser_amount_from_sats" + )] + pub amount: bitcoin::Amount, + pub outpoint: bitcoin::OutPoint, + pub block_height: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListCoinsResult { + pub coins: Vec, +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/commands/utils.rs b/src/commands/utils.rs new file mode 100644 index 00000000..807aef6d --- /dev/null +++ b/src/commands/utils.rs @@ -0,0 +1,16 @@ +use miniscript::bitcoin; +use serde::{Deserialize, Deserializer, Serializer}; + +/// Serialize an amount as sats +pub fn ser_amount(amount: &bitcoin::Amount, s: S) -> Result { + s.serialize_u64(amount.as_sat()) +} + +/// Deserialize an amount from sats +pub fn deser_amount_from_sats<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let a = u64::deserialize(deserializer)?; + Ok(bitcoin::Amount::from_sat(a)) +} diff --git a/src/jsonrpc/api.rs b/src/jsonrpc/api.rs index 82fe1a42..b748063a 100644 --- a/src/jsonrpc/api.rs +++ b/src/jsonrpc/api.rs @@ -8,6 +8,7 @@ pub fn handle_request(control: &DaemonControl, req: Request) -> Result serde_json::json!(&control.get_info()), "getnewaddress" => serde_json::json!(&control.get_new_address()), + "listcoins" => serde_json::json!(&control.list_coins()), "stop" => serde_json::json!({}), _ => { return Err(Error::method_not_found()); diff --git a/tests/test_rpc.py b/tests/test_rpc.py index de67b85f..97ade6c1 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -1,4 +1,5 @@ from fixtures import * +from test_framework.utils import wait_for, COIN def test_getinfo(minisafed): @@ -15,3 +16,27 @@ def test_getaddress(minisafed): assert "address" in res # We'll get a new one at every call assert res["address"] != minisafed.rpc.getnewaddress()["address"] + + +def test_listcoins(minisafed, bitcoind): + # Initially empty + res = minisafed.rpc.listcoins() + assert "coins" in res + assert len(res["coins"]) == 0 + + # If we send a coin, we'll get a new entry. Note we monitor for unconfirmed + # funds as well. + addr = minisafed.rpc.getnewaddress()["address"] + txid = bitcoind.rpc.sendtoaddress(addr, 1) + wait_for(lambda: len(minisafed.rpc.listcoins()["coins"]) == 1) + res = minisafed.rpc.listcoins()["coins"] + assert txid == res[0]["outpoint"][:64] + assert res[0]["amount"] == 1 * COIN + assert res[0]["block_height"] is None + + # If the coin gets confirmed, it'll be marked as such. + bitcoind.generate_block(1, wait_for_mempool=txid) + block_height = bitcoind.rpc.getblockcount() + wait_for( + lambda: minisafed.rpc.listcoins()["coins"][0]["block_height"] == block_height + )