From 2660b77487d63218019413a4ca33b3a9629fbfc8 Mon Sep 17 00:00:00 2001 From: pythcoiner Date: Sat, 7 Oct 2023 16:19:05 +0200 Subject: [PATCH] implement listadresses --- doc/API.md | 25 +++++++- src/commands/mod.rs | 136 +++++++++++++++++++++++++++++++++++++++++++- src/jsonrpc/api.rs | 24 ++++++++ src/jsonrpc/mod.rs | 2 + tests/test_rpc.py | 18 ++++++ 5 files changed, 203 insertions(+), 2 deletions(-) diff --git a/doc/API.md b/doc/API.md index 501562c1..29cec398 100644 --- a/doc/API.md +++ b/doc/API.md @@ -7,9 +7,10 @@ Commands must be sent as valid JSONRPC 2.0 requests, ending with a `\n`. | Command | Description | | ----------------------------------------------------------- | ---------------------------------------------------- | -| [`stop`](#stop) | Stops liana daemon | +| [`stop`](#stop) | Stops liana daemon | | [`getinfo`](#getinfo) | Get general information about the daemon | | [`getnewaddress`](#getnewaddress) | Get a new receiving address | +| [`listaddresses`](#listaddresses) | List addresses given start_index and count | | [`listcoins`](#listcoins) | List all wallet transaction outputs. | | [`createspend`](#createspend) | Create a new Spend transaction | | [`updatespend`](#updatespend) | Store a created Spend transaction | @@ -79,6 +80,28 @@ This command does not take any parameter for now. | `address` | string | A Bitcoin address | +### `listaddresses` + +List receive and change addresses given start_index and count. Both arguments are optional. +Default value for `start_index` is 0. +If no value is passed for `count` the maximum generated index between receive and change is selected. + +#### Request + +| Field | Type | Description | +| ------------- | ----------------- | ----------------------------------------------------------- | +| `start_index` | integer(optional) | Index of the first address to list | +| `count` | integer(optional) | Number of addresses to list | + +#### Response + +| Field | Type | Description | +| ------------- | ----------------- | ----------------------------------------------------------- | +| `index` | integer | Derivation index | +| `receive` | string | Receive address | +| `change` | string | Change address | + + ### `listcoins` List all our transaction outputs, optionally filtered by status and/or outpoint. diff --git a/src/commands/mod.rs b/src/commands/mod.rs index fb2c2b22..f5ab4210 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -25,7 +25,7 @@ use std::{ use miniscript::{ bitcoin::{ - self, address, + self, address, bip32, locktime::absolute, psbt::{Input as PsbtIn, Output as PsbtOut, PartiallySignedTransaction as Psbt}, }, @@ -72,6 +72,8 @@ pub enum CommandError { /// An error that might occur in the racy rescan triggering logic. RescanTrigger(String), RecoveryNotAvailable, + InvalidAddressCount, + InvalidAddressIndex, } impl fmt::Display for CommandError { @@ -136,6 +138,8 @@ impl fmt::Display for CommandError { f, "No coin currently spendable through this timelocked recovery path." ), + Self::InvalidAddressCount => write!(f, "Invalid address count, should be under 2^31-1"), + Self::InvalidAddressIndex => write!(f, "Invalid address index, should be under 2^31-1"), } } } @@ -289,6 +293,69 @@ impl DaemonControl { GetAddressResult::new(address) } + /// list addresses + pub fn list_addresses( + &self, + start_index: Option, + count: Option, + ) -> Result { + let mut db_conn = self.db.connection(); + let receive_index: u32 = db_conn.receive_index().into(); + let change_index: u32 = db_conn.change_index().into(); + + let start_index = start_index.unwrap_or(0); + + if start_index > (2u32.pow(31) - 1) { + return Err(CommandError::InvalidAddressIndex); + } + + let count = count.unwrap_or_else(|| receive_index.max(change_index) - start_index); + + if count == 0 { + let out: Vec = Vec::new(); + return Ok(ListAddressesResult::new(out)); + } + + let index = start_index + .checked_add(count) + .and_then(|index| index.checked_sub(1)) + .and_then(|index| { + if index > (2u32.pow(31) - 1) { + None + } else { + Some(index) + } + }) + .ok_or(CommandError::InvalidAddressCount)?; + + let addresses: Vec = (start_index..=index) + .map(|index| { + let child = bip32::ChildNumber::from_normal_idx(index).expect("Cannot fail here"); + + let receive = self + .config + .main_descriptor + .receive_descriptor() + .derive(child, &self.secp) + .address(self.config.bitcoin_config.network); + + let change = self + .config + .main_descriptor + .change_descriptor() + .derive(child, &self.secp) + .address(self.config.bitcoin_config.network); + + AddressInfo { + index, + receive, + change, + } + }) + .collect(); + Ok(ListAddressesResult::new(addresses)) + } + /// Get a list of all known coins, optionally by status and/or outpoint. pub fn list_coins( &self, @@ -860,6 +927,24 @@ impl GetAddressResult { } } +#[derive(Debug, Clone, Serialize)] +pub struct AddressInfo { + index: u32, + receive: bitcoin::Address, + change: bitcoin::Address, +} + +#[derive(Debug, Clone, Serialize)] +pub struct ListAddressesResult { + addresses: Vec, +} + +impl ListAddressesResult { + pub fn new(addresses: Vec) -> Self { + ListAddressesResult { addresses } + } +} + #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct LCSpendInfo { pub txid: bitcoin::Txid, @@ -972,6 +1057,55 @@ mod tests { ms.shutdown(); } + #[test] + fn listaddresses() { + let ms = DummyLiana::new(DummyBitcoind::new(), DummyDatabase::new()); + + let control = &ms.handle.control; + + let list = control.list_addresses(Some(2), Some(5)).unwrap(); + + assert_eq!(list.addresses[0].index, 2); + assert_eq!(list.addresses.last().unwrap().index, 6); + + let addr0 = control.get_new_address().address; + let addr1 = control.get_new_address().address; + let _addr2 = control.get_new_address().address; + let addr3 = control.get_new_address().address; + let addr4 = control.get_new_address().address; + + let list = control.list_addresses(Some(0), None).unwrap(); + + assert_eq!(list.addresses[0].index, 0); + assert_eq!(list.addresses[0].receive, addr0); + assert_eq!(list.addresses.last().unwrap().index, 4); + assert_eq!(list.addresses.last().unwrap().receive, addr4); + + let list = control.list_addresses(None, None).unwrap(); + + assert_eq!(list.addresses[0].index, 0); + assert_eq!(list.addresses[0].receive, addr0); + assert_eq!(list.addresses.last().unwrap().index, 4); + assert_eq!(list.addresses.last().unwrap().receive, addr4); + + let list = control.list_addresses(Some(1), Some(3)).unwrap(); + + assert_eq!(list.addresses[0].index, 1); + assert_eq!(list.addresses[0].receive, addr1); + assert_eq!(list.addresses.last().unwrap().index, 3); + assert_eq!(list.addresses.last().unwrap().receive, addr3); + + let addr5 = control.get_new_address().address; + let list = control.list_addresses(Some(5), None).unwrap(); + + assert_eq!(list.addresses[0].index, 5); + assert_eq!(list.addresses[0].receive, addr5); + assert_eq!(list.addresses.last().unwrap().index, 5); + assert_eq!(list.addresses.last().unwrap().receive, addr5); + + ms.shutdown(); + } + #[test] fn create_spend() { let dummy_op = bitcoin::OutPoint::from_str( diff --git a/src/jsonrpc/api.rs b/src/jsonrpc/api.rs index 3c2424d5..67e527e8 100644 --- a/src/jsonrpc/api.rs +++ b/src/jsonrpc/api.rs @@ -136,6 +136,26 @@ fn list_coins(control: &DaemonControl, params: Option) -> Result, +) -> Result { + let start_index: Option = params + .as_ref() + .and_then(|p| p.get(0, "start_index")) + .and_then(|i| i.as_u64()) + .and_then(|i| i.try_into().ok()); + + let count: Option = params + .as_ref() + .and_then(|p| p.get(1, "count")) + .and_then(|c| c.as_u64()) + .and_then(|i| i.try_into().ok()); + + let res = &control.list_addresses(start_index, count)?; + Ok(serde_json::json!(&res)) +} + fn list_confirmed(control: &DaemonControl, params: Params) -> Result { let start: u32 = params .get(0, "start") @@ -311,6 +331,10 @@ pub fn handle_request(control: &DaemonControl, req: Request) -> Result { + let params = req.params; + list_addresses(control, params)? + } "listconfirmed" => { let params = req.params.ok_or_else(|| { Error::invalid_params( diff --git a/src/jsonrpc/mod.rs b/src/jsonrpc/mod.rs index 6af7b9e8..f9bb9e78 100644 --- a/src/jsonrpc/mod.rs +++ b/src/jsonrpc/mod.rs @@ -164,6 +164,8 @@ impl From for Error { | commands::CommandError::SpendFinalization(..) | commands::CommandError::InsaneRescanTimestamp(..) | commands::CommandError::AlreadyRescanning + | commands::CommandError::InvalidAddressCount + | commands::CommandError::InvalidAddressIndex | commands::CommandError::RecoveryNotAvailable => { Error::new(ErrorCode::InvalidParams, e.to_string()) } diff --git a/tests/test_rpc.py b/tests/test_rpc.py index 3a2dcc2e..f0102749 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -37,6 +37,24 @@ def test_getaddress(lianad): assert res["address"] != lianad.rpc.getnewaddress()["address"] +def test_listadresses(lianad): + list = lianad.rpc.listaddresses(2, 5) + list2 = lianad.rpc.listaddresses(start_index=2, count=5) + assert list == list2 + assert "addresses" in list + addr = list["addresses"] + assert addr[0]["index"] == 2 + assert addr[-1]["index"] == 6 + + list3 = lianad.rpc.listaddresses() # start_index = 0, receive_index = 0 + _ = lianad.rpc.getnewaddress() # start_index = 0, receive_index = 1 + _ = lianad.rpc.getnewaddress() # start_index = 0, receive_index = 2 + list4 = lianad.rpc.listaddresses() + assert len(list4["addresses"]) == len(list3["addresses"]) + 2 == 2 + list5 = lianad.rpc.listaddresses(0) + assert list4 == list5 + + def test_listcoins(lianad, bitcoind): # Initially empty res = lianad.rpc.listcoins()