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 |
|
| 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` |
|
||||||
|
|||||||
@ -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
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() {
|
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());
|
||||||
|
|||||||
@ -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
|
||||||
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user