feat: add rpc command to list revealed addresses

This commit is contained in:
Michael Mallan 2025-05-02 15:34:48 +01:00
parent 4945dba7c0
commit 3d6a22ea05
No known key found for this signature in database
GPG Key ID: 5177CDCEDB0EABEB
3 changed files with 315 additions and 0 deletions

View File

@ -12,6 +12,7 @@ Commands must be sent as valid JSONRPC 2.0 requests, ending with a `\n`.
| [`updatederivationindexes`](#updatederivationindexes) | Update last generated addresses derivation indexes |
| [`getnewaddress`](#getnewaddress) | Get a new receiving address |
| [`listaddresses`](#listaddresses) | List addresses given start_index and count |
| [`listrevealedaddresses`](#listrevealedaddresses) | List revealed addresses (both used and unused) |
| [`listcoins`](#listcoins) | List all wallet transaction outputs. |
| [`createspend`](#createspend) | Create a new Spend transaction |
| [`updatespend`](#updatespend) | Store a created Spend transaction |
@ -140,6 +141,40 @@ If no value is passed for `count` the maximum generated index between receive an
| `change` | string | Change address |
### `listrevealedaddresses`
List revealed receive or change addresses, optionally filtering for those that are unused by any of the current coins in the wallet.
Addresses are returned in order of descending derivation index.
If `start_index` is omitted or `null`, then addresses will be returned starting from the last revealed address.
Otherwise, addresses will be returned starting from the specified derivation index.
#### Request
| Field | Type | Description |
| --------------- | ----------------- | --------------------------------------------------------------------------------------- |
| `is_change` | bool | Whether to return change or otherwise receive addresses. |
| `exclude_used` | bool | Whether to exclude those addresses that have been used by a current coin in the wallet. |
| `limit` | integer | The maximum number of addresses to list. |
| `start_index` | integer(optional) | For pagination, pass the `continue_from` value from the previous response. |
#### Response
The response contains two fields:
- `addresses`: an array of revealed addresses, with the structure given below.
- `continue_from`: used for pagination of results. If not `null`, this indicates that there may be additional addresses that can be listed and
this value can be passed to the next request as `start_index` to continue with the next page of results.
Each element in the `addresses` array has the following fields:
| Field | Type | Description |
| ------------- | ---------------- | ---------------------------------------------------------------------------- |
| `index` | integer | Derivation index. |
| `address` | string | Address. |
| `used_count` | integer | The number of current coins in the wallet that are using this address. |
| `label` | string or null | Address label, if any. |
### `listcoins`
List all our transaction outputs, optionally filtered by status and/or outpoint.

View File

@ -199,6 +199,50 @@ fn list_addresses(
Ok(serde_json::json!(&res))
}
fn list_revealed_addresses(
control: &DaemonControl,
params: Params,
) -> Result<serde_json::Value, Error> {
let is_change = params
.get(0, "is_change")
.ok_or_else(|| Error::invalid_params("Missing 'is_change' parameter."))?
.as_bool()
.ok_or_else(|| Error::invalid_params("Invalid 'is_change' parameter."))?;
let exclude_used = params
.get(1, "exclude_used")
.ok_or_else(|| Error::invalid_params("Missing 'exclude_used' parameter."))?
.as_bool()
.ok_or_else(|| Error::invalid_params("Invalid 'exclude_used' parameter."))?;
let limit = params
.get(2, "limit")
.ok_or_else(|| Error::invalid_params("Missing 'limit' parameter."))?
.as_u64()
.and_then(|l| l.try_into().ok())
.ok_or_else(|| Error::invalid_params("Invalid 'limit' parameter."))?;
// A missing value and `null` are both mapped to `None`.
let start_index = if let Some(ind) = params.get(3, "start_index") {
if ind.as_null().is_some() {
None
} else {
let ind_u32: u32 = ind
.as_u64()
.and_then(|ind_u64| ind_u64.try_into().ok())
.ok_or_else(|| Error::invalid_params("Invalid 'start_index' parameter."))?;
Some(ind_u32)
}
} else {
None
};
let res = &control.list_revealed_addresses(
is_change,
exclude_used,
limit,
start_index.map(|ind| ind.into()),
)?;
Ok(serde_json::json!(&res))
}
fn update_deriv_indexes(
control: &DaemonControl,
params: Params,
@ -493,6 +537,14 @@ pub fn handle_request(control: &mut DaemonControl, req: Request) -> Result<Respo
let params = req.params;
list_addresses(control, params)?
}
"listrevealedaddresses" => {
let params = req.params.ok_or_else(|| {
Error::invalid_params(
"The 'listrevealedaddresses' command requires 3 parameters: 'is_change', 'exclude_used' and 'limit'",
)
})?;
list_revealed_addresses(control, params)?
}
"listconfirmed" => {
let params = req.params.ok_or_else(|| {
Error::invalid_params(

View File

@ -201,6 +201,234 @@ def test_listaddresses(lianad):
lianad.rpc.listaddresses(0, "blb")
def test_listrevealedaddresses(lianad, bitcoind):
# Get addresses for reference:
addresses = lianad.rpc.listaddresses(0, 10)["addresses"]
# We start with index 0 already "revealed":
list_rec = lianad.rpc.listrevealedaddresses(False, False, 10)
assert list_rec["continue_from"] is None # there are no more addresses to list
assert len(list_rec["addresses"]) == 1
assert list_rec["addresses"][0]["index"] == 0
assert list_rec["addresses"][0]["address"] == addresses[0]["receive"]
assert list_rec["addresses"][0]["used_count"] == 0
assert list_rec["addresses"][0]["label"] is None
# Generate some addresses.
addr_1 = lianad.rpc.getnewaddress()["address"]
addr_2 = lianad.rpc.getnewaddress()["address"]
addr_3 = lianad.rpc.getnewaddress()["address"]
addr_4 = lianad.rpc.getnewaddress()["address"]
addr_5 = lianad.rpc.getnewaddress()["address"]
addr_6 = lianad.rpc.getnewaddress()["address"]
addr_7 = lianad.rpc.getnewaddress()["address"]
# Last revealed receive index is 7.
assert lianad.rpc.getinfo()["receive_index"] == 7
# Set some labels
lianad.rpc.updatelabels(
{addr_1: "my test label 1", addr_5: "my test label 5"},
)
# Passing None or omitting start_index parameter is the same:
assert lianad.rpc.listrevealedaddresses(
False, False, 10
) == lianad.rpc.listrevealedaddresses(False, False, 10, None)
# If we continue_from a value above our last revealed index, we'll start from the last index.
assert lianad.rpc.listrevealedaddresses(
False, False, 10
) == lianad.rpc.listrevealedaddresses(False, False, 10, 100)
# Similarly if we start from a hardened index:
assert lianad.rpc.listrevealedaddresses(
False, False, 10
) == lianad.rpc.listrevealedaddresses(False, False, 10, 4_294_967_295)
# Get 3 addresses starting at last revealed index:
list_rec = lianad.rpc.listrevealedaddresses(False, False, 3)
assert list_rec["continue_from"] == 4
assert len(list_rec["addresses"]) == 3
assert list_rec["addresses"][0]["index"] == 7
assert list_rec["addresses"][0]["address"] == addr_7
assert list_rec["addresses"][0]["used_count"] == 0
assert list_rec["addresses"][0]["label"] is None
assert list_rec["addresses"][1]["index"] == 6
assert list_rec["addresses"][1]["address"] == addr_6
assert list_rec["addresses"][1]["used_count"] == 0
assert list_rec["addresses"][1]["label"] is None
assert list_rec["addresses"][2]["index"] == 5
assert list_rec["addresses"][2]["address"] == addr_5
assert list_rec["addresses"][2]["used_count"] == 0
assert list_rec["addresses"][2]["label"] == "my test label 5"
# Get next 3 using continue_from returned above as start_index:
list_rec = lianad.rpc.listrevealedaddresses(False, False, 3, 4)
assert list_rec["continue_from"] == 1
assert len(list_rec["addresses"]) == 3
assert list_rec["addresses"][0]["index"] == 4
assert list_rec["addresses"][0]["address"] == addr_4
assert list_rec["addresses"][0]["used_count"] == 0
assert list_rec["addresses"][0]["label"] is None
assert list_rec["addresses"][1]["index"] == 3
assert list_rec["addresses"][1]["address"] == addr_3
assert list_rec["addresses"][1]["used_count"] == 0
assert list_rec["addresses"][1]["label"] is None
assert list_rec["addresses"][2]["index"] == 2
assert list_rec["addresses"][2]["address"] == addr_2
assert list_rec["addresses"][2]["used_count"] == 0
assert list_rec["addresses"][2]["label"] is None
# Get final page of results consisting of 2 addresses:
list_rec = lianad.rpc.listrevealedaddresses(False, False, 3, 1)
assert list_rec["continue_from"] is None # final page
assert len(list_rec["addresses"]) == 2 # num addresses remaining is below limit
assert list_rec["addresses"][0]["index"] == 1
assert list_rec["addresses"][0]["address"] == addr_1
assert list_rec["addresses"][0]["used_count"] == 0
assert list_rec["addresses"][0]["label"] == "my test label 1"
assert list_rec["addresses"][1]["index"] == 0
assert list_rec["addresses"][1]["address"] == addresses[0]["receive"]
assert list_rec["addresses"][1]["used_count"] == 0
assert list_rec["addresses"][1]["label"] is None
# Receive funds at a couple of addresses.
destinations = {
addr_2: 0.003,
addr_4: 0.004,
addr_7: 0.005,
}
txid = bitcoind.rpc.sendmany("", destinations)
bitcoind.generate_block(1, wait_for_mempool=txid)
wait_for(lambda: len(lianad.rpc.listcoins(["confirmed"])["coins"]) == 3)
# The addresses are shown as used.
list_rec = lianad.rpc.listrevealedaddresses(False, False, 3)
assert list_rec["continue_from"] == 4
assert len(list_rec["addresses"]) == 3
assert list_rec["addresses"][0]["index"] == 7
assert list_rec["addresses"][0]["address"] == addr_7
assert list_rec["addresses"][0]["used_count"] == 1
assert list_rec["addresses"][0]["label"] is None
assert list_rec["addresses"][1]["index"] == 6
assert list_rec["addresses"][1]["address"] == addr_6
assert list_rec["addresses"][1]["used_count"] == 0
assert list_rec["addresses"][1]["label"] is None
assert list_rec["addresses"][2]["index"] == 5
assert list_rec["addresses"][2]["address"] == addr_5
assert list_rec["addresses"][2]["used_count"] == 0
assert list_rec["addresses"][2]["label"] == "my test label 5"
list_rec = lianad.rpc.listrevealedaddresses(False, False, 3, 4)
assert list_rec["continue_from"] == 1
assert len(list_rec["addresses"]) == 3
assert list_rec["addresses"][0]["index"] == 4
assert list_rec["addresses"][0]["address"] == addr_4
assert list_rec["addresses"][0]["used_count"] == 1
assert list_rec["addresses"][0]["label"] is None
assert list_rec["addresses"][1]["index"] == 3
assert list_rec["addresses"][1]["address"] == addr_3
assert list_rec["addresses"][1]["used_count"] == 0
assert list_rec["addresses"][1]["label"] is None
assert list_rec["addresses"][2]["index"] == 2
assert list_rec["addresses"][2]["address"] == addr_2
assert list_rec["addresses"][2]["used_count"] == 1
assert list_rec["addresses"][2]["label"] is None
# We can exclude used addresses:
list_rec = lianad.rpc.listrevealedaddresses(False, True, 3)
assert list_rec["continue_from"] == 2
assert len(list_rec["addresses"]) == 3
assert list_rec["addresses"][0]["index"] == 6
assert list_rec["addresses"][0]["address"] == addr_6
assert list_rec["addresses"][0]["used_count"] == 0
assert list_rec["addresses"][0]["label"] is None
assert list_rec["addresses"][1]["index"] == 5
assert list_rec["addresses"][1]["address"] == addr_5
assert list_rec["addresses"][1]["used_count"] == 0
assert list_rec["addresses"][1]["label"] == "my test label 5"
assert list_rec["addresses"][2]["index"] == 3 # index 4 was skipped
assert list_rec["addresses"][2]["address"] == addr_3
assert list_rec["addresses"][2]["used_count"] == 0
assert list_rec["addresses"][2]["label"] is None
# We can exclude used also if we continue from the value in the response above:
list_rec = lianad.rpc.listrevealedaddresses(False, True, 3, 2)
assert list_rec["continue_from"] is None
assert len(list_rec["addresses"]) == 2
assert list_rec["addresses"][0]["index"] == 1 # index 2 was skipped
assert list_rec["addresses"][0]["address"] == addr_1
assert list_rec["addresses"][0]["used_count"] == 0
assert list_rec["addresses"][0]["label"] == "my test label 1"
assert list_rec["addresses"][1]["index"] == 0
assert list_rec["addresses"][1]["address"] == addresses[0]["receive"]
assert list_rec["addresses"][1]["used_count"] == 0
assert list_rec["addresses"][1]["label"] is None
# Receive funds at some of the same addresses again.
destinations = {
addr_2: 0.0031,
addr_4: 0.0041,
}
txid = bitcoind.rpc.sendmany("", destinations)
bitcoind.generate_block(1, wait_for_mempool=txid)
wait_for(lambda: len(lianad.rpc.listcoins(["confirmed"])["coins"]) == 5)
# One more coin to addr_2.
destinations = {
addr_2: 0.0032,
}
txid = bitcoind.rpc.sendmany("", destinations)
bitcoind.generate_block(1, wait_for_mempool=txid)
wait_for(lambda: len(lianad.rpc.listcoins(["confirmed"])["coins"]) == 6)
# The counts have updated:
list_rec = lianad.rpc.listrevealedaddresses(False, False, 3, 4)
assert list_rec["continue_from"] == 1
assert len(list_rec["addresses"]) == 3
assert list_rec["addresses"][0]["index"] == 4
assert list_rec["addresses"][0]["address"] == addr_4
assert list_rec["addresses"][0]["used_count"] == 2
assert list_rec["addresses"][0]["label"] is None
assert list_rec["addresses"][1]["index"] == 3
assert list_rec["addresses"][1]["address"] == addr_3
assert list_rec["addresses"][1]["used_count"] == 0
assert list_rec["addresses"][1]["label"] is None
assert list_rec["addresses"][2]["index"] == 2
assert list_rec["addresses"][2]["address"] == addr_2
assert list_rec["addresses"][2]["used_count"] == 3
assert list_rec["addresses"][2]["label"] is None
# If we request limit 0, we get empty list:
list_rec = lianad.rpc.listrevealedaddresses(False, False, 0)
assert list_rec["continue_from"] == 7 # same as starting index
assert len(list_rec["addresses"]) == 0
# The poller currently sets the change index to match the receive index.
# See https://github.com/wizardsardine/liana/issues/1333.
assert lianad.rpc.getinfo()["receive_index"] == 7
assert lianad.rpc.getinfo()["change_index"] == 7
# We can get change addresses:
list_cha = lianad.rpc.listrevealedaddresses(True, False, 3)
assert list_cha["continue_from"] == 4
assert len(list_cha["addresses"]) == 3
assert list_cha["addresses"][0]["index"] == 7
assert list_cha["addresses"][0]["address"] == addresses[7]["change"]
assert list_cha["addresses"][0]["used_count"] == 0
assert list_cha["addresses"][0]["label"] is None
assert list_cha["addresses"][1]["index"] == 6
assert list_cha["addresses"][1]["address"] == addresses[6]["change"]
assert list_cha["addresses"][1]["used_count"] == 0
assert list_cha["addresses"][1]["label"] is None
assert list_cha["addresses"][2]["index"] == 5
assert list_cha["addresses"][2]["address"] == addresses[5]["change"]
assert list_cha["addresses"][2]["used_count"] == 0
assert list_cha["addresses"][2]["label"] is None
def test_listcoins(lianad, bitcoind):
# Initially empty
res = lianad.rpc.listcoins()