commands: add a 'list_coins' command.

This commit is contained in:
Antoine Poinsot 2022-08-16 16:27:57 +02:00
parent 99a9cbf0f8
commit 9d0c68dae3
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
5 changed files with 110 additions and 1 deletions

View File

@ -64,3 +64,23 @@ This command does not take any parameter for now.
| Field | Type | Description | | Field | Type | Description |
| ------------- | ------ | ------------------ | | ------------- | ------ | ------------------ |
| `address` | string | A Bitcoin address | | `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` |

View File

@ -2,7 +2,14 @@
//! //!
//! External interface to the Minisafe daemon. //! 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::{ use miniscript::{
bitcoin, bitcoin,
@ -43,6 +50,30 @@ impl DaemonControl {
.expect("It's a wsh() descriptor"); .expect("It's a wsh() descriptor");
GetAddressResult { address } 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<ListCoinsEntry> = 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)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -65,6 +96,22 @@ pub struct GetAddressResult {
pub address: bitcoin::Address, 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<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListCoinsResult {
pub coins: Vec<ListCoinsEntry>,
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

16
src/commands/utils.rs Normal file
View File

@ -0,0 +1,16 @@
use miniscript::bitcoin;
use serde::{Deserialize, Deserializer, Serializer};
/// Serialize an amount as sats
pub fn ser_amount<S: Serializer>(amount: &bitcoin::Amount, s: S) -> Result<S::Ok, S::Error> {
s.serialize_u64(amount.as_sat())
}
/// Deserialize an amount from sats
pub fn deser_amount_from_sats<'de, D>(deserializer: D) -> Result<bitcoin::Amount, D::Error>
where
D: Deserializer<'de>,
{
let a = u64::deserialize(deserializer)?;
Ok(bitcoin::Amount::from_sat(a))
}

View File

@ -8,6 +8,7 @@ pub fn handle_request(control: &DaemonControl, req: Request) -> Result<Response,
let result = match req.method.as_str() { let result = match req.method.as_str() {
"getinfo" => serde_json::json!(&control.get_info()), "getinfo" => serde_json::json!(&control.get_info()),
"getnewaddress" => serde_json::json!(&control.get_new_address()), "getnewaddress" => serde_json::json!(&control.get_new_address()),
"listcoins" => serde_json::json!(&control.list_coins()),
"stop" => serde_json::json!({}), "stop" => serde_json::json!({}),
_ => { _ => {
return Err(Error::method_not_found()); return Err(Error::method_not_found());

View File

@ -1,4 +1,5 @@
from fixtures import * from fixtures import *
from test_framework.utils import wait_for, COIN
def test_getinfo(minisafed): def test_getinfo(minisafed):
@ -15,3 +16,27 @@ def test_getaddress(minisafed):
assert "address" in res assert "address" in res
# We'll get a new one at every call # We'll get a new one at every call
assert res["address"] != minisafed.rpc.getnewaddress()["address"] 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
)