commands: add a 'list_spend' command (and 'listspendtxs' RPC)

This commit is contained in:
Antoine Poinsot 2022-10-01 02:22:25 +02:00
parent 3dfc7261db
commit c6e004806a
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
7 changed files with 159 additions and 2 deletions

View File

@ -10,6 +10,7 @@ Commands must be sent as valid JSONRPC 2.0 requests, ending with a `\n`.
| [`stop`](#stop) | Stops the minisafe daemon |
| [`getinfo`](#getinfo) | Get general information about the daemon |
| [`getnewaddress`](#getnewaddress) | Get a new receiving address |
| [`listspendtxs`](#listspendtxs) | List all stored Spend transactions |
# Reference
@ -130,3 +131,27 @@ This command does not return anything for now.
| Field | Type | Description |
| -------------- | --------- | ---------------------------------------------------- |
### `listspendtxs`
List stored Spend transactions.
#### Request
This command does not take any parameter for now.
| Field | Type | Description |
| ------------- | ----------------- | ----------------------------------------------------------- |
#### Response
| Field | Type | Description |
| -------------- | ------------- | ---------------------------------------------------------------- |
| `spend_txs` | array | Array of Spend tx entries |
##### Spend tx entry
| Field | Type | Description |
| ------------- | ----------------- | ----------------------------------------------------------- |
| `psbt` | string | Base64-encoded PSBT of the Spend transaction. |

View File

@ -418,6 +418,16 @@ impl DaemonControl {
Ok(())
}
pub fn list_spend(&self) -> ListSpendResult {
let mut db_conn = self.db.connection();
let spend_txs = db_conn
.list_spend()
.into_iter()
.map(|psbt| ListSpendEntry { psbt })
.collect();
ListSpendResult { spend_txs }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -462,6 +472,17 @@ pub struct CreateSpendResult {
pub psbt: Psbt,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListSpendEntry {
#[serde(serialize_with = "ser_base64", deserialize_with = "deser_psbt_base64")]
pub psbt: Psbt,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListSpendResult {
pub spend_txs: Vec<ListSpendEntry>,
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -76,6 +76,9 @@ pub trait DatabaseConnection {
/// Insert a new Spend transaction or replace an existing one.
fn store_spend(&mut self, psbt: &Psbt);
/// List all existing Spend transactions.
fn list_spend(&mut self) -> Vec<Psbt>;
}
// FIXME: if possible, avoid reallocating.
@ -171,6 +174,13 @@ impl DatabaseConnection for SqliteConn {
fn store_spend(&mut self, psbt: &Psbt) {
self.store_spend(psbt)
}
fn list_spend(&mut self) -> Vec<Psbt> {
self.list_spend()
.into_iter()
.map(|db_spend| db_spend.psbt)
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]

View File

@ -382,6 +382,16 @@ impl SqliteConn {
})
.expect("Db must not fail");
}
pub fn list_spend(&mut self) -> Vec<DbSpendTransaction> {
db_query(
&mut self.conn,
"SELECT * FROM spend_transactions",
rusqlite::params![],
|row| row.try_into(),
)
.expect("Db must not fail")
}
}
#[cfg(test)]

View File

@ -72,6 +72,7 @@ pub fn handle_request(control: &DaemonControl, req: Request) -> Result<Response,
"getinfo" => serde_json::json!(&control.get_info()),
"getnewaddress" => serde_json::json!(&control.get_new_address()),
"listcoins" => serde_json::json!(&control.list_coins()),
"listspendtxs" => serde_json::json!(&control.list_spend()),
"stop" => serde_json::json!({}),
"updatespend" => {
let params = req

View File

@ -170,6 +170,16 @@ impl DatabaseConnection for DummyDbConn {
fn spend_tx(&mut self, txid: &bitcoin::Txid) -> Option<Psbt> {
self.db.read().unwrap().spend_txs.get(txid).cloned()
}
fn list_spend(&mut self) -> Vec<Psbt> {
self.db
.read()
.unwrap()
.spend_txs
.values()
.cloned()
.collect()
}
}
pub struct DummyMinisafe {

View File

@ -95,6 +95,41 @@ def test_create_spend(minisafed, bitcoind):
bitcoind.rpc.sendrawtransaction(signed_tx_hex)
def test_list_spend(minisafed, bitcoind):
# Start by creating two conflicting Spend PSBTs
addr = minisafed.rpc.getnewaddress()["address"]
bitcoind.rpc.sendtoaddress(addr, 0.2567)
wait_for(lambda: len(minisafed.rpc.listcoins()["coins"]) == 1)
outpoints = [c["outpoint"] for c in minisafed.rpc.listcoins()["coins"]]
destinations = {
bitcoind.rpc.getnewaddress(): 200_000,
}
res = minisafed.rpc.createspend(outpoints, destinations, 6)
assert "psbt" in res
addr = minisafed.rpc.getnewaddress()["address"]
bitcoind.rpc.sendtoaddress(addr, 0.0987)
wait_for(lambda: len(minisafed.rpc.listcoins()["coins"]) == 2)
outpoints = [c["outpoint"] for c in minisafed.rpc.listcoins()["coins"]]
destinations = {
bitcoind.rpc.getnewaddress(): 400_000,
}
res_b = minisafed.rpc.createspend(outpoints, destinations, 2)
assert "psbt" in res_b
# Store them both in DB.
assert len(minisafed.rpc.listspendtxs()["spend_txs"]) == 0
minisafed.rpc.updatespend(res["psbt"])
minisafed.rpc.updatespend(res_b["psbt"])
# Listing all Spend transactions will list them both.
list_res = minisafed.rpc.listspendtxs()["spend_txs"]
assert len(list_res) == 2
all_psbts = [entry["psbt"] for entry in list_res]
assert res["psbt"] in all_psbts
assert res_b["psbt"] in all_psbts
def test_update_spend(minisafed, bitcoind):
# Start by creating a Spend PSBT
addr = minisafed.rpc.getnewaddress()["address"]
@ -108,7 +143,52 @@ def test_update_spend(minisafed, bitcoind):
assert "psbt" in res
# Now update it
assert len(minisafed.rpc.listspendtxs()["spend_txs"]) == 0
minisafed.rpc.updatespend(res["psbt"])
list_res = minisafed.rpc.listspendtxs()["spend_txs"]
assert len(list_res) == 1
assert list_res[0]["psbt"] == res["psbt"]
# TODO: check it's stored once we implement 'listspendtxs'
# TODO: check with added signatures once we implement 'listspendtxs'
# Keep a copy for later.
psbt_no_sig = PSBT()
psbt_no_sig.deserialize(res["psbt"])
# We can add a signature and update it
psbt_sig_a = PSBT()
psbt_sig_a.deserialize(res["psbt"])
dummy_pk_a = bytes.fromhex(
"0375e00eb72e29da82b89367947f29ef34afb75e8654f6ea368e0acdfd92976b7c"
)
dummy_sig_a = bytes.fromhex(
"304402202b925395cfeaa0171a7a92982bb4891acc4a312cbe7691d8375d36796d5b570a0220378a8ab42832848e15d1aedded5fb360fedbdd6c39226144e527f0f1e19d5398"
)
psbt_sig_a.inputs[0].partial_sigs[dummy_pk_a] = dummy_sig_a
psbt_sig_a_ser = psbt_sig_a.serialize()
minisafed.rpc.updatespend(psbt_sig_a_ser)
# We'll get it when querying
list_res = minisafed.rpc.listspendtxs()["spend_txs"]
assert len(list_res) == 1
assert list_res[0]["psbt"] == psbt_sig_a_ser
# We can add another signature to the empty PSBT and update it again
psbt_sig_b = PSBT()
psbt_sig_b.deserialize(res["psbt"])
dummy_pk_b = bytes.fromhex(
"03a1b26313f430c4b15bb1fdce663207659d8cac749a0e53d70eff01874496feff"
)
dummy_sig_b = bytes.fromhex(
"3044022005aebcd649fb8965f0591710fb3704931c3e8118ee60dd44917479f63ceba6d4022018b212900e5a80e9452366894de37f0d02fb9c89f1e94f34fb6ed7fd71c15c41"
)
psbt_sig_b.inputs[0].partial_sigs[dummy_pk_b] = dummy_sig_b
psbt_sig_b_ser = psbt_sig_b.serialize()
minisafed.rpc.updatespend(psbt_sig_b_ser)
# It will have merged both.
list_res = minisafed.rpc.listspendtxs()["spend_txs"]
assert len(list_res) == 1
psbt_merged = PSBT()
psbt_merged.deserialize(list_res[0]["psbt"])
assert len(psbt_merged.inputs[0].partial_sigs) == 2
assert psbt_merged.inputs[0].partial_sigs[dummy_pk_a] == dummy_sig_a
assert psbt_merged.inputs[0].partial_sigs[dummy_pk_b] == dummy_sig_b