diff --git a/doc/API.md b/doc/API.md index 6946a928..2d57697e 100644 --- a/doc/API.md +++ b/doc/API.md @@ -11,6 +11,7 @@ Commands must be sent as valid JSONRPC 2.0 requests, ending with a `\n`. | [`getinfo`](#getinfo) | Get general information about the daemon | | [`getnewaddress`](#getnewaddress) | Get a new receiving address | | [`listspendtxs`](#listspendtxs) | List all stored Spend transactions | +| [`delspendtx`](#delspendtx) | Delete a stored Spend transaction | # Reference @@ -156,3 +157,19 @@ This command does not take any parameter for now. | -------------- | ----------------- | ----------------------------------------------------------------------- | | `psbt` | string | Base64-encoded PSBT of the Spend transaction. | | `change_index` | int or null | Index of the change output in the transaction outputs, if there is one. | + + +### `delspendtx` + +#### Request + +| Field | Type | Description | +| -------- | ------ | --------------------------------------------------- | +| `txid` | string | Hex encoded txid of the Spend transaction to delete | + +#### Response + +This command does not return anything for now. + +| Field | Type | Description | +| -------------- | --------- | ---------------------------------------------------- | diff --git a/src/commands/mod.rs b/src/commands/mod.rs index a7249f83..32487278 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -431,6 +431,11 @@ impl DaemonControl { .collect(); ListSpendResult { spend_txs } } + + pub fn delete_spend(&self, txid: &bitcoin::Txid) { + let mut db_conn = self.db.connection(); + db_conn.delete_spend(txid); + } } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/database/mod.rs b/src/database/mod.rs index e5b4c9de..fc6d4c44 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -79,6 +79,9 @@ pub trait DatabaseConnection { /// List all existing Spend transactions. fn list_spend(&mut self) -> Vec; + + /// Delete a Spend transaction from database. + fn delete_spend(&mut self, txid: &bitcoin::Txid); } // FIXME: if possible, avoid reallocating. @@ -181,6 +184,10 @@ impl DatabaseConnection for SqliteConn { .map(|db_spend| db_spend.psbt) .collect() } + + fn delete_spend(&mut self, txid: &bitcoin::Txid) { + self.delete_spend(txid) + } } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/src/database/sqlite/mod.rs b/src/database/sqlite/mod.rs index a144e84e..a076ac7a 100644 --- a/src/database/sqlite/mod.rs +++ b/src/database/sqlite/mod.rs @@ -392,6 +392,17 @@ impl SqliteConn { ) .expect("Db must not fail") } + + pub fn delete_spend(&mut self, txid: &bitcoin::Txid) { + db_exec(&mut self.conn, |db_tx| { + db_tx.execute( + "DELETE FROM spend_transactions WHERE txid = ?1", + rusqlite::params![txid.to_vec()], + )?; + Ok(()) + }) + .expect("Db must not fail"); + } } #[cfg(test)] diff --git a/src/jsonrpc/api.rs b/src/jsonrpc/api.rs index b905bf7d..9a08ad35 100644 --- a/src/jsonrpc/api.rs +++ b/src/jsonrpc/api.rs @@ -60,6 +60,18 @@ fn update_spend(control: &DaemonControl, params: Params) -> Result Result { + let txid = params + .get(0, "txid") + .ok_or(Error::invalid_params("Missing 'txid' parameter."))? + .as_str() + .and_then(|s| bitcoin::Txid::from_str(&s).ok()) + .ok_or(Error::invalid_params("Invalid 'feerate' parameter."))?; + control.delete_spend(&txid); + + Ok(serde_json::json!({})) +} + /// Handle an incoming JSONRPC2 request. pub fn handle_request(control: &DaemonControl, req: Request) -> Result { let result = match req.method.as_str() { @@ -69,6 +81,12 @@ pub fn handle_request(control: &DaemonControl, req: Request) -> Result { + let params = req + .params + .ok_or(Error::invalid_params("Missing 'txid' parameter."))?; + delete_spend(control, params)? + } "getinfo" => serde_json::json!(&control.get_info()), "getnewaddress" => serde_json::json!(&control.get_new_address()), "listcoins" => serde_json::json!(&control.list_coins()), diff --git a/src/testutils.rs b/src/testutils.rs index 563baec4..2aaa0883 100644 --- a/src/testutils.rs +++ b/src/testutils.rs @@ -180,6 +180,10 @@ impl DatabaseConnection for DummyDbConn { .cloned() .collect() } + + fn delete_spend(&mut self, txid: &bitcoin::Txid) { + self.db.write().unwrap().spend_txs.remove(txid); + } } pub struct DummyMinisafe { diff --git a/tests/test_framework/serializations.py b/tests/test_framework/serializations.py index c0cdc4f5..c6961877 100644 --- a/tests/test_framework/serializations.py +++ b/tests/test_framework/serializations.py @@ -551,6 +551,11 @@ class CTransaction(object): self.sha256 = uint256_from_str(hash256(self.serialize_without_witness())) self.hash = encode(hash256(self.serialize())[::-1], "hex_codec").decode("ascii") + def txid(self): + if self.sha256 is None: + self.calc_sha256() + return ser_uint256(self.sha256)[::-1] + def is_valid(self): self.calc_sha256() for tout in self.vout: diff --git a/tests/test_rpc.py b/tests/test_rpc.py index df69060f..89030860 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -134,6 +134,21 @@ def test_list_spend(minisafed, bitcoind): second_psbt = next(entry for entry in list_res if entry["psbt"] == res_b["psbt"]) assert second_psbt["change_index"] is None + # If we delete the first one, we'll get only the second one. + first_psbt = PSBT() + first_psbt.deserialize(res["psbt"]) + minisafed.rpc.delspendtx(first_psbt.tx.txid().hex()) + list_res = minisafed.rpc.listspendtxs()["spend_txs"] + assert len(list_res) == 1 + assert list_res[0]["psbt"] == res_b["psbt"] + + # If we delete the second one, result will be empty. + second_psbt = PSBT() + second_psbt.deserialize(res_b["psbt"]) + minisafed.rpc.delspendtx(second_psbt.tx.txid().hex()) + list_res = minisafed.rpc.listspendtxs()["spend_txs"] + assert len(list_res) == 0 + def test_update_spend(minisafed, bitcoind): # Start by creating a Spend PSBT