commands: add a 'list_coins' command.
This commit is contained in:
parent
99a9cbf0f8
commit
9d0c68dae3
20
doc/API.md
20
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` |
|
||||
|
||||
@ -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<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)]
|
||||
@ -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<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListCoinsResult {
|
||||
pub coins: Vec<ListCoinsEntry>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
16
src/commands/utils.rs
Normal file
16
src/commands/utils.rs
Normal 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))
|
||||
}
|
||||
@ -8,6 +8,7 @@ pub fn handle_request(control: &DaemonControl, req: Request) -> Result<Response,
|
||||
let result = match req.method.as_str() {
|
||||
"getinfo" => 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());
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user