Skip to content

Commit

Permalink
Merge #709: Implement listadresses
Browse files Browse the repository at this point in the history
2660b77 implement listadresses (pythcoiner)

Pull request description:

  address #681

  todo:
  - [x]  implement tests
  - [x] update docs

  edit: i'm really new to rust, don't hesitate to kick my ass when i write stupid code

ACKs for top commit:
  darosior:
    ACK 2660b77 -- my requests are addressed in followup #808.

Tree-SHA512: a5fdfb4516dc0379bfec1be535e752795dec75d28cbc5b9fa4fe9898fa00b1cfaa9cee3b95f4dfd68365f4585426e1b4457a8366cc4f783600704994f879526f
  • Loading branch information
darosior committed Nov 11, 2023
2 parents 8548c62 + 2660b77 commit 479efe7
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 2 deletions.
25 changes: 24 additions & 1 deletion doc/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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.
Expand Down
136 changes: 135 additions & 1 deletion src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
},
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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"),
}
}
}
Expand Down Expand Up @@ -296,6 +300,69 @@ impl DaemonControl {
GetAddressResult::new(address)
}

/// list addresses
pub fn list_addresses(
&self,
start_index: Option<u32>,
count: Option<u32>,
) -> Result<ListAddressesResult, CommandError> {
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<AddressInfo> = 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<AddressInfo> = (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,
Expand Down Expand Up @@ -872,6 +939,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<AddressInfo>,
}

impl ListAddressesResult {
pub fn new(addresses: Vec<AddressInfo>) -> Self {
ListAddressesResult { addresses }
}
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct LCSpendInfo {
pub txid: bitcoin::Txid,
Expand Down Expand Up @@ -984,6 +1069,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(
Expand Down
24 changes: 24 additions & 0 deletions src/jsonrpc/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,26 @@ fn list_coins(control: &DaemonControl, params: Option<Params>) -> Result<serde_j
Ok(serde_json::json!(&res))
}

fn list_addresses(
control: &DaemonControl,
params: Option<Params>,
) -> Result<serde_json::Value, Error> {
let start_index: Option<u32> = 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<u32> = 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<serde_json::Value, Error> {
let start: u32 = params
.get(0, "start")
Expand Down Expand Up @@ -310,6 +330,10 @@ pub fn handle_request(control: &DaemonControl, req: Request) -> Result<Response,
let params = req.params;
list_coins(control, params)?
}
"listaddresses" => {
let params = req.params;
list_addresses(control, params)?
}
"listconfirmed" => {
let params = req.params.ok_or_else(|| {
Error::invalid_params(
Expand Down
2 changes: 2 additions & 0 deletions src/jsonrpc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ impl From<commands::CommandError> 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())
}
Expand Down
18 changes: 18 additions & 0 deletions tests/test_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 479efe7

Please sign in to comment.