Repository: wizardsardine/liana
Pull request #1692: feat: add `listrevealedaddresses` command
Author: login: jp1ac4
Created: 2025-05-02T17:08:48Z | Merged: 2025-05-12T07:36:32Z

This implements #1678.

The command parameters differ slightly from those described in the issue:
- instead of a `keychain` enum parameter, I used  `is_change` to control whether receive or change addresses are returned.
- I used an optional `continue_from` parameter to control pagination. The value to pass in the next request is provided by the previous response. If no value is provided in the response, there are no more addresses to list. If no value is set in the request, the addresses will be returned starting from the last revealed address.

Setting the positional argument for `continue_after` as `null` will be treated the same as omitting it (for other RPC commands, a `null` value results in a parsing error).



Diff:
diff --git a/doc/API.md b/doc/API.md
index 63bfc62ff..9fbaacdb0 100644
--- a/doc/API.md
+++ b/doc/API.md
@@ -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.
diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs
index 7162b63a3..77ffcf21e 100644
--- a/lianad/src/commands/mod.rs
+++ b/lianad/src/commands/mod.rs
@@ -477,6 +477,100 @@ impl DaemonControl {
         Ok(ListAddressesResult::new(addresses?))
     }
 
+    /// List revealed addresses. Addresses will be returned in order of
+    /// descending derivation index.
+    ///
+    /// # Parameters
+    ///
+    /// - `is_change`: set to `false` to return receive addresses and `true` for change addresses.
+    ///
+    /// - `exclude_used`:  set to `true` to return only those revealed addresses that
+    ///   are unused by any coins in the wallet.
+    ///
+    /// - `limit`: the maximum number of addresses to return.
+    ///
+    /// - `start_index`: the derivation index from which to start listing addresses. As addresses are
+    ///   returned in descending order, `start_index` is the highest index that can be returned.
+    ///   If set to `None`, then addresses will be returned starting from the last revealed index.
+    ///   As there are no revealed addresses with a derivation index higher than the last revealed index,
+    ///   setting this parameter to a higher value will be the same as setting it to `None`.
+    pub fn list_revealed_addresses(
+        &self,
+        is_change: bool,
+        exclude_used: bool,
+        limit: usize,
+        start_index: Option<ChildNumber>,
+    ) -> Result<ListRevealedAddressesResult, CommandError> {
+        let mut db_conn = self.db.connection();
+
+        let (desc, last_revealed) = if is_change {
+            (
+                self.config.main_descriptor.change_descriptor(),
+                db_conn.change_index(),
+            )
+        } else {
+            (
+                self.config.main_descriptor.receive_descriptor(),
+                db_conn.receive_index(),
+            )
+        };
+
+        // Determine the index to start deriving addresses from, ensuring it is not higher than the last revealed.
+        let start_index = start_index.unwrap_or(last_revealed).min(last_revealed);
+
+        // Count how many times each (used) address has been used.
+        let mut used_counts = HashMap::<ChildNumber, u32>::new();
+        // TODO: consider adding DB method to get coins or used indices by index range.
+        for coin in db_conn.coins(&[], &[]).values() {
+            if coin.is_change == is_change && coin.derivation_index <= start_index {
+                *used_counts.entry(coin.derivation_index).or_insert(0) += 1;
+            }
+        }
+
+        let mut addresses = Vec::<_>::with_capacity(limit);
+        let mut continue_from = None;
+        // This will store (index, address) pairs.
+        let mut derived_addresses = Vec::<_>::with_capacity(limit);
+        // Iterate in descending order.
+        for i in (0..=start_index.into()).rev() {
+            let index = ChildNumber::from(i);
+            if derived_addresses.len() == limit {
+                // We've reached the limit. There may be more addresses to list using pagination.
+                continue_from = Some(index);
+                break;
+            }
+            if !exclude_used || !used_counts.contains_key(&index) {
+                let addr = desc
+                    .derive(index, &self.secp)
+                    .address(self.config.bitcoin_config.network);
+                derived_addresses.push((index, addr));
+            }
+        }
+        // Now get the labels from DB (in multiple chunks).
+        let mut labels = HashMap::<String, String>::with_capacity(derived_addresses.len());
+        const CHUNK_SIZE: usize = 100;
+        for chunk in derived_addresses.chunks(CHUNK_SIZE) {
+            let items = chunk
+                .iter()
+                .map(|(_, addr)| LabelItem::Address(addr.clone()))
+                .collect::<HashSet<_>>();
+            labels.extend(db_conn.labels(&items));
+        }
+        for (index, address) in derived_addresses {
+            let label = labels.get(&address.to_string()).cloned();
+            addresses.push(ListRevealedAddressesEntry {
+                index,
+                address,
+                label,
+                used_count: *used_counts.get(&index).unwrap_or(&0),
+            });
+        }
+        Ok(ListRevealedAddressesResult {
+            addresses,
+            continue_from,
+        })
+    }
+
     /// Get a list of all known coins, optionally by status and/or outpoint.
     pub fn list_coins(
         &self,
@@ -1317,6 +1411,37 @@ impl ListAddressesResult {
     }
 }
 
+/// A revealed address entry in the list returned by [`DaemonControl::list_revealed_addresses`].
+#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
+pub struct ListRevealedAddressesEntry {
+    /// The address's derivation index.
+    pub index: ChildNumber,
+    /// The address.
+    pub address: bitcoin::Address,
+    /// Label assigned to the address, if any.
+    pub label: Option<String>,
+    /// How many coins, including those unconfirmed, that are currently in the wallet are using this address.
+    ///
+    /// This count does not include any coins that may have been replaced or otherwise dropped
+    /// from the mempool.
+    pub used_count: u32,
+}
+
+/// Result of a [`DaemonControl::list_revealed_addresses`] request.
+#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
+pub struct ListRevealedAddressesResult {
+    /// Revealed addresses in order of descending derivation index.
+    pub addresses: Vec<ListRevealedAddressesEntry>,
+    /// `continue_from` being set to some value indicates that there may
+    /// be more addresses that can be listed with pagination. The next
+    /// [`DaemonControl::list_revealed_addresses`] request can be continued
+    /// with this value passed to `start_index`.
+    ///
+    /// If `continue_from` is `None`, then there are no further
+    /// addresses to be listed.
+    pub continue_from: Option<ChildNumber>,
+}
+
 #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
 pub struct LCSpendInfo {
     pub txid: bitcoin::Txid,
@@ -1565,6 +1690,315 @@ mod tests {
         ms.shutdown();
     }
 
+    #[test]
+    fn list_revealed_addresses() {
+        let ms = DummyLiana::new(DummyBitcoind::new(), DummyDatabase::new());
+
+        let control = &ms.control();
+        let mut db_conn = control.db().lock().unwrap().connection();
+
+        // $ bitcoin-cli deriveaddresses "wsh(or_d(pk([aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/0/*),and_v(v:pkh([aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/0/*),older(10000))))#wx6v3mks" 0
+        // [
+        //   "bc1q9ksrc647hx8zp2cewl8p5f487dgux3777yees8rjcx46t4daqzzqt7yga8"
+        // ]
+        let addr0 = bitcoin::Address::from_str(
+            "bc1q9ksrc647hx8zp2cewl8p5f487dgux3777yees8rjcx46t4daqzzqt7yga8",
+        )
+        .unwrap()
+        .assume_checked();
+
+        // The wallet starts with index 0 already revealed:
+        let list = control
+            .list_revealed_addresses(false, false, 3, None)
+            .unwrap();
+        assert_eq!(list.addresses.len(), 1);
+        assert!(list.continue_from.is_none());
+        let revealed = list.addresses.first().unwrap();
+        assert_eq!(revealed.index, ChildNumber::from(0));
+        assert_eq!(revealed.address, addr0);
+        assert_eq!(revealed.used_count, 0);
+        assert!(revealed.label.is_none());
+
+        // Generate new addresses up to and including index 7:
+        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 addr5 = control.get_new_address().address;
+        let addr6 = control.get_new_address().address;
+        let addr7 = control.get_new_address().address;
+        assert_eq!(control.get_info().receive_index, 7);
+
+        // Set some labels.
+        db_conn.update_labels(&HashMap::from([
+            (
+                LabelItem::Address(addr1.clone()),
+                Some("my test label 1".to_string()),
+            ),
+            (
+                LabelItem::Address(addr5.clone()),
+                Some("my test label 5".to_string()),
+            ),
+        ]));
+
+        // If we continue_from a value above our last index, we'll start from the last index.
+        let list = control
+            .list_revealed_addresses(false, false, 3, Some(ChildNumber::from(100)))
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(4)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(7)); // this is our last revealed index
+        assert_eq!(list.addresses[0].address, addr7);
+        assert_eq!(list.addresses[0].used_count, 0);
+        assert!(list.addresses[0].label.is_none());
+        assert_eq!(list.addresses[1].index, ChildNumber::from(6));
+        assert_eq!(list.addresses[1].address, addr6);
+        assert_eq!(list.addresses[1].used_count, 0);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(5));
+        assert_eq!(list.addresses[2].address, addr5);
+        assert_eq!(list.addresses[2].used_count, 0);
+        assert_eq!(list.addresses[2].label, Some("my test label 5".to_string()));
+
+        // If we start from a hardened index, we'll get the same result again:
+        assert_eq!(
+            list,
+            control
+                .list_revealed_addresses(false, false, 3, Some(ChildNumber::from(u32::MAX)))
+                .unwrap()
+        );
+
+        // Passing `None` for `continue_from` will also start from the last revealed index:
+        let list = control
+            .list_revealed_addresses(false, false, 3, None)
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(4)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(7));
+        assert_eq!(list.addresses[0].address, addr7);
+        assert_eq!(list.addresses[0].used_count, 0);
+        assert!(list.addresses[0].label.is_none());
+        assert_eq!(list.addresses[1].index, ChildNumber::from(6));
+        assert_eq!(list.addresses[1].address, addr6);
+        assert_eq!(list.addresses[1].used_count, 0);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(5));
+        assert_eq!(list.addresses[2].address, addr5);
+        assert_eq!(list.addresses[2].used_count, 0);
+        assert_eq!(list.addresses[2].label, Some("my test label 5".to_string()));
+
+        // Now continue pagination using the `continue_from` value from the result above:
+        let list = control
+            .list_revealed_addresses(false, false, 3, Some(ChildNumber::from(4)))
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(1)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(4));
+        assert_eq!(list.addresses[0].address, addr4);
+        assert_eq!(list.addresses[0].used_count, 0);
+        assert!(list.addresses[0].label.is_none());
+        assert_eq!(list.addresses[1].index, ChildNumber::from(3));
+        assert_eq!(list.addresses[1].address, addr3);
+        assert_eq!(list.addresses[1].used_count, 0);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(2));
+        assert_eq!(list.addresses[2].address, addr2);
+        assert_eq!(list.addresses[2].used_count, 0);
+        assert!(list.addresses[2].label.is_none());
+
+        // This is the final page:
+        let list = control
+            .list_revealed_addresses(false, false, 3, Some(ChildNumber::from(1)))
+            .unwrap();
+        assert_eq!(list.addresses.len(), 2); // only two addresses even though limit was 3
+        assert!(list.continue_from.is_none()); // there are no more addresses to derive
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(1));
+        assert_eq!(list.addresses[0].address, addr1);
+        assert_eq!(list.addresses[0].used_count, 0);
+        assert_eq!(list.addresses[0].label, Some("my test label 1".to_string()));
+        assert_eq!(list.addresses[1].index, ChildNumber::from(0));
+        assert_eq!(list.addresses[1].address, addr0);
+        assert_eq!(list.addresses[1].used_count, 0);
+        assert!(list.addresses[1].label.is_none());
+
+        // Add a coin so that address with index 5 is used:
+        db_conn.new_unspent_coins(&[Coin {
+            outpoint: OutPoint::new(
+                Txid::from_str("617eab1fc0b03ee7f82ba70166725291783461f1a0e7975eaf8b5f8f674234f3")
+                    .unwrap(),
+                0,
+            ),
+            is_immature: false,
+            block_info: None,
+            amount: bitcoin::Amount::from_sat(80_000),
+            derivation_index: ChildNumber::from(5),
+            is_change: false,
+            spend_txid: None,
+            spend_block: None,
+            is_from_self: true,
+        }]);
+
+        // If we don't exclude used, results will be same as before, except index 5 is marked as used:
+        let list = control
+            .list_revealed_addresses(false, false, 3, None)
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(4)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(7));
+        assert_eq!(list.addresses[0].address, addr7);
+        assert_eq!(list.addresses[0].used_count, 0);
+        assert!(list.addresses[0].label.is_none());
+        assert_eq!(list.addresses[1].index, ChildNumber::from(6));
+        assert_eq!(list.addresses[1].address, addr6);
+        assert_eq!(list.addresses[1].used_count, 0);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(5));
+        assert_eq!(list.addresses[2].address, addr5);
+        assert_eq!(list.addresses[2].used_count, 1); // used
+        assert_eq!(list.addresses[2].label, Some("my test label 5".to_string()));
+
+        // If we exclude used, index 5 will be skipped:
+        let list = control
+            .list_revealed_addresses(false, true, 3, None)
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(3)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(7));
+        assert_eq!(list.addresses[0].address, addr7);
+        assert_eq!(list.addresses[0].used_count, 0);
+        assert!(list.addresses[0].label.is_none());
+        assert_eq!(list.addresses[1].index, ChildNumber::from(6));
+        assert_eq!(list.addresses[1].address, addr6);
+        assert_eq!(list.addresses[1].used_count, 0);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(4));
+        assert_eq!(list.addresses[2].address, addr4);
+        assert_eq!(list.addresses[2].used_count, 0);
+        assert!(list.addresses[2].label.is_none());
+
+        // Similar behaviour if we continue from index 5. First without excluding used:
+        let list = control
+            .list_revealed_addresses(false, false, 3, Some(ChildNumber::from(5)))
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(2)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(5));
+        assert_eq!(list.addresses[0].address, addr5);
+        assert_eq!(list.addresses[0].used_count, 1); // used
+        assert_eq!(list.addresses[0].label, Some("my test label 5".to_string()));
+        assert_eq!(list.addresses[1].index, ChildNumber::from(4));
+        assert_eq!(list.addresses[1].address, addr4);
+        assert_eq!(list.addresses[1].used_count, 0);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(3));
+        assert_eq!(list.addresses[2].address, addr3);
+        assert_eq!(list.addresses[2].used_count, 0);
+        assert!(list.addresses[2].label.is_none());
+
+        // Now excluding used:
+        let list = control
+            .list_revealed_addresses(false, true, 3, Some(ChildNumber::from(5)))
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(1)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(4));
+        assert_eq!(list.addresses[0].address, addr4);
+        assert_eq!(list.addresses[0].used_count, 0);
+        assert!(list.addresses[0].label.is_none());
+        assert_eq!(list.addresses[1].index, ChildNumber::from(3));
+        assert_eq!(list.addresses[1].address, addr3);
+        assert_eq!(list.addresses[1].used_count, 0);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(2));
+        assert_eq!(list.addresses[2].address, addr2);
+        assert_eq!(list.addresses[2].used_count, 0);
+        assert!(list.addresses[2].label.is_none());
+
+        // If we add another coin using the same derivation index, the count will increase:
+        db_conn.new_unspent_coins(&[Coin {
+            outpoint: OutPoint::new(
+                Txid::from_str("617eab1fc0b03ee7f82ba70166725291783461f1a0e7975eaf8b5f8f674234f3")
+                    .unwrap(),
+                1,
+            ),
+            is_immature: false,
+            block_info: None,
+            amount: bitcoin::Amount::from_sat(80_000),
+            derivation_index: ChildNumber::from(5),
+            is_change: false,
+            spend_txid: None,
+            spend_block: None,
+            is_from_self: true,
+        }]);
+
+        let list = control
+            .list_revealed_addresses(false, false, 3, None)
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(4)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(7));
+        assert_eq!(list.addresses[0].address, addr7);
+        assert_eq!(list.addresses[0].used_count, 0);
+        assert!(list.addresses[0].label.is_none());
+        assert_eq!(list.addresses[1].index, ChildNumber::from(6));
+        assert_eq!(list.addresses[1].address, addr6);
+        assert_eq!(list.addresses[1].used_count, 0);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(5));
+        assert_eq!(list.addresses[2].address, addr5);
+        assert_eq!(list.addresses[2].used_count, 2); // count updated.
+        assert_eq!(list.addresses[2].label, Some("my test label 5".to_string()));
+
+        // Check change address
+        // $ bitcoin-cli deriveaddresses "wsh(or_d(pk([aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/1/*),and_v(v:pkh([aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/1/*),older(10000))))#sqk8au7v" 0
+        // [
+        //   "bc1qd5m23jemr8mfj8x4q482zpspnxehg0jg0pyzrhxfg8xrrtyqewvqjrq3x6"
+        // ]
+        let change_addr0 = bitcoin::Address::from_str(
+            "bc1qd5m23jemr8mfj8x4q482zpspnxehg0jg0pyzrhxfg8xrrtyqewvqjrq3x6",
+        )
+        .unwrap()
+        .assume_checked();
+
+        // If we ask for change addresses, we only have the initial one revealed:
+        let list = control
+            .list_revealed_addresses(true, false, 3, None)
+            .unwrap();
+        assert_eq!(list.addresses.len(), 1);
+        assert!(list.continue_from.is_none());
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(0));
+        assert_eq!(list.addresses[0].address, change_addr0);
+        assert_eq!(list.addresses[0].used_count, 0);
+        assert!(list.addresses[0].label.is_none());
+
+        // Finally, passing limit 0 returns an empty vector and the continue_from is same as it started with.
+        // First, passing None for continue from:
+        let list = control
+            .list_revealed_addresses(false, false, 0, None)
+            .unwrap();
+        assert_eq!(list.addresses.len(), 0);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(7)));
+
+        // Now, continuing from 4:
+        let list = control
+            .list_revealed_addresses(false, false, 0, Some(ChildNumber::from(4)))
+            .unwrap();
+        assert_eq!(list.addresses.len(), 0);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(4)));
+
+        ms.shutdown();
+    }
+
     #[test]
     fn create_spend() {
         let dummy_tx = bitcoin::Transaction {
diff --git a/lianad/src/jsonrpc/api.rs b/lianad/src/jsonrpc/api.rs
index dc2cf5a03..7f6082e6d 100644
--- a/lianad/src/jsonrpc/api.rs
+++ b/lianad/src/jsonrpc/api.rs
@@ -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(
diff --git a/lianad/src/testutils.rs b/lianad/src/testutils.rs
index 2f4a11bae..5b24aeb8a 100644
--- a/lianad/src/testutils.rs
+++ b/lianad/src/testutils.rs
@@ -152,6 +152,7 @@ struct DummyDbState {
     coins: HashMap<bitcoin::OutPoint, Coin>,
     txs: HashMap<bitcoin::Txid, bitcoin::Transaction>,
     spend_txs: HashMap<bitcoin::Txid, (Psbt, Option<u32>)>,
+    labels: HashMap<LabelItem, String>,
     timestamp: u32,
     rescan_timestamp: Option<u32>,
     last_poll_timestamp: Option<u32>,
@@ -186,6 +187,7 @@ impl DummyDatabase {
                 coins: HashMap::new(),
                 txs: HashMap::new(),
                 spend_txs: HashMap::new(),
+                labels: HashMap::new(),
                 timestamp: now,
                 rescan_timestamp: None,
                 last_poll_timestamp: None,
@@ -438,12 +440,32 @@ impl DatabaseConnection for DummyDatabase {
         self.db.write().unwrap().last_poll_timestamp = Some(timestamp);
     }
 
-    fn update_labels(&mut self, _items: &HashMap<LabelItem, Option<String>>) {
-        todo!()
+    fn update_labels(&mut self, items: &HashMap<LabelItem, Option<String>>) {
+        for (lab_item, lab_val) in items {
+            if let Some(val) = lab_val {
+                self.db
+                    .write()
+                    .unwrap()
+                    .labels
+                    .insert(lab_item.clone(), val.clone());
+            } else {
+                self.db.write().unwrap().labels.remove_entry(lab_item);
+            }
+        }
     }
 
-    fn labels(&mut self, _items: &HashSet<LabelItem>) -> HashMap<String, String> {
-        todo!()
+    fn labels(&mut self, items: &HashSet<LabelItem>) -> HashMap<String, String> {
+        self.db
+            .read()
+            .unwrap()
+            .labels
+            .iter()
+            .filter_map(|(lab_item, lab_val)| {
+                items
+                    .contains(lab_item)
+                    .then_some((lab_item.to_string(), lab_val.clone()))
+            })
+            .collect()
     }
 
     fn list_txids(&mut self, start: u32, end: u32, limit: u64) -> Vec<bitcoin::Txid> {
diff --git a/tests/fixtures.py b/tests/fixtures.py
index 9985b2c6b..691e80559 100644
--- a/tests/fixtures.py
+++ b/tests/fixtures.py
@@ -308,6 +308,7 @@ def lianad_multisig(bitcoin_backend, directory):
 
     lianad.cleanup()
 
+
 @pytest.fixture
 def lianad_multisig_legacy_datadir(bitcoin_backend, directory):
     datadir = os.path.join(directory, "lianad")
@@ -318,13 +319,7 @@ def lianad_multisig_legacy_datadir(bitcoin_backend, directory):
     signer = MultiSigner(4, {csv_value: 5}, is_taproot=USE_TAPROOT)
     main_desc = Descriptor.from_str(multisig_desc(signer, csv_value, USE_TAPROOT, 3, 2))
 
-    lianad = Lianad(
-        datadir,
-        signer,
-        main_desc,
-        bitcoin_backend,
-        legacy_datadir=True
-    )
+    lianad = Lianad(datadir, signer, main_desc, bitcoin_backend, legacy_datadir=True)
 
     try:
         lianad.start()
diff --git a/tests/test_framework/lianad.py b/tests/test_framework/lianad.py
index 9e70f8549..3e7c64832 100644
--- a/tests/test_framework/lianad.py
+++ b/tests/test_framework/lianad.py
@@ -26,12 +26,7 @@
 
 class Lianad(TailableProc):
     def __init__(
-        self,
-        datadir,
-        signer,
-        multi_desc,
-        bitcoin_backend,
-        legacy_datadir=False
+        self, datadir, signer, multi_desc, bitcoin_backend, legacy_datadir=False
     ):
         TailableProc.__init__(self, datadir, verbose=VERBOSE)
 
diff --git a/tests/test_rpc.py b/tests/test_rpc.py
index 5334c4edc..29bf6c4ca 100644
--- a/tests/test_rpc.py
+++ b/tests/test_rpc.py
@@ -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()


Reviews:
user: login: pythcoiner
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T04:46:08Z
user: login: jp1ac4
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T07:56:20Z
user: login: jp1ac4
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T08:04:45Z
user: login: jp1ac4
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T08:06:14Z
user: login: jp1ac4
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T08:11:43Z
user: login: jp1ac4
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T08:14:26Z
user: login: jp1ac4
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T08:21:03Z
user: login: jp1ac4
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T08:25:03Z
user: login: jp1ac4
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T08:25:35Z
user: login: jp1ac4
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T08:26:42Z
user: login: jp1ac4
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T08:27:22Z
user: login: jp1ac4
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T13:04:03Z
user: login: jp1ac4
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T13:04:25Z
user: login: jp1ac4
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T13:04:54Z
user: login: jp1ac4
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T13:05:04Z
user: login: jp1ac4
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T13:05:16Z
user: login: jp1ac4
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T13:05:27Z
user: login: jp1ac4
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T13:05:40Z
user: login: jp1ac4
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T13:06:21Z
user: login: jp1ac4
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T13:07:25Z
user: login: jp1ac4
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T13:08:49Z
user: login: jp1ac4
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T13:09:06Z
user: login: pythcoiner
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-06T07:09:40Z
user: login: jp1ac4
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T07:50:43Z

Review comments:
diff_hunk: @@ -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 `continue_from` 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.                                                |
+| `continue_from` | integer(optional) | For pagination, pass the corresponding 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 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.                                                                     |
+| `is_used`     | bool             | Whether the address has been used by any of the current coins in the wallet. |
path: doc/API.md
user: login: pythcoiner
body: do we have a way to determine the usage count of this address and return it as an integer? I think it should be nice to hint that an address have been reused
created_at: 2025-05-05T03:09:40Z
author_association: COLLABORATOR
diff_hunk: @@ -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 `continue_from` 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.                                                |
+| `continue_from` | integer(optional) | For pagination, pass the corresponding value from the previous response.                |
path: doc/API.md
user: login: pythcoiner
body: we lack of consistency in the API args naming (mainly my bad), we already use `offset` / `start` / `start_index`, should'nt we use one of the previous one?
created_at: 2025-05-05T03:14:25Z
author_association: COLLABORATOR
diff_hunk: @@ -477,6 +477,94 @@ impl DaemonControl {
         Ok(ListAddressesResult::new(addresses?))
     }
 
+    /// List revealed addresses. Addresses will be returned in order of
+    /// descending derivation index.
+    ///
+    /// `is_change` controls whether to return receive or change addresses.
+    ///
+    /// If `exclude_used` is `true`, then only those revealed addresses that
+    /// are unused by any coins in the wallet will be returned.
+    ///
+    /// `limit` gives the maximum number of addresses to return.
+    ///
+    /// If `continue_from` is `None`, then addresses will be returned starting
+    /// from the last revealed derivation index. Otherwise, they will start from
+    /// the specified index (unless the last revealed index is lower).
path: lianad/src/commands/mod.rs
user: login: pythcoiner
body: ```suggestion
    /// List revealed addresses. Addresses will be returned in order of
    /// descending derivation index.
    ///
    /// # Parameters
    ///
    /// - `is_change`: controls whether to return receive or change addresses.
    ///
    /// - `exclude_used`: if `true`, then only those revealed addresses that
    /// are unused by any coins in the wallet will be returned.
    ///
    /// - `limit`: gives the maximum number of addresses to return.
    ///
    /// - `continue_from`: if `None`, then addresses will be returned starting
    /// from the last revealed derivation index. Otherwise, they will start from
    /// the specified index (unless the last revealed index is lower).
```

I really like we supply docstrings w/ new implemented functions, but we should do it in the [expected format](https://docs.rs/docstrings/latest/docstrings/)
created_at: 2025-05-05T03:28:15Z
author_association: COLLABORATOR
diff_hunk: @@ -1317,6 +1405,37 @@ impl ListAddressesResult {
     }
 }
 
+/// A revealed address entry in the list returned by `list_revealed_addresses`.
path: lianad/src/commands/mod.rs
user: login: pythcoiner
body: ```suggestion
/// A revealed address entry in the list returned by [`DaemonControl::list_revealed_addresses`].
```

this syntax will add a direct redirection to the method in auto generated documentation
created_at: 2025-05-05T03:33:10Z
author_association: COLLABORATOR
diff_hunk: @@ -1317,6 +1405,37 @@ impl ListAddressesResult {
     }
 }
 
+/// A revealed address entry in the list returned by `list_revealed_addresses`.
+#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
+pub struct ListRevealedAddressesEntry {
+    /// The address's derivation index.
+    pub index: ChildNumber,
+    /// The address.
+    pub address: bitcoin::Address,
+    /// Label assigned to the address, if any.
+    pub label: Option<String>,
+    /// Whether the address has been used by any coins in the wallet as per the
+    /// current best chain. This also takes into account any coins that are
+    /// currently unconfirmed, but ignores any coins that may have been replaced
+    /// or otherwise dropped from the mempool.
+    pub is_used: bool,
+}
+
+/// Result of a `list_revealed_addresses` request.
path: lianad/src/commands/mod.rs
user: login: pythcoiner
body: ```suggestion
/// Result of a [`DaemonControl::list_revealed_addresses`] request.
```
idem
created_at: 2025-05-05T03:34:25Z
author_association: COLLABORATOR
diff_hunk: @@ -1317,6 +1405,37 @@ impl ListAddressesResult {
     }
 }
 
+/// A revealed address entry in the list returned by `list_revealed_addresses`.
+#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
+pub struct ListRevealedAddressesEntry {
+    /// The address's derivation index.
+    pub index: ChildNumber,
+    /// The address.
+    pub address: bitcoin::Address,
+    /// Label assigned to the address, if any.
+    pub label: Option<String>,
+    /// Whether the address has been used by any coins in the wallet as per the
+    /// current best chain. This also takes into account any coins that are
+    /// currently unconfirmed, but ignores any coins that may have been replaced
+    /// or otherwise dropped from the mempool.
+    pub is_used: bool,
+}
+
+/// Result of a `list_revealed_addresses` request.
+#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
+pub struct ListRevealedAddressesResult {
+    /// Revealed addresses in order of descending derivation index.
+    pub addresses: Vec<ListRevealedAddressesEntry>,
+    /// `continue_from` being set to some value indicates that there may
+    /// be more addresses that can be listed with pagination. The next
+    /// `list_revealed_addresses` request can be continued with this
path: lianad/src/commands/mod.rs
user: login: pythcoiner
body: ```suggestion
    /// [`DaemonControl::list_revealed_addresses`] request can be continued with this
```
idem
created_at: 2025-05-05T03:35:06Z
author_association: COLLABORATOR
diff_hunk: @@ -477,6 +477,94 @@ impl DaemonControl {
         Ok(ListAddressesResult::new(addresses?))
     }
 
+    /// List revealed addresses. Addresses will be returned in order of
+    /// descending derivation index.
+    ///
+    /// `is_change` controls whether to return receive or change addresses.
+    ///
+    /// If `exclude_used` is `true`, then only those revealed addresses that
+    /// are unused by any coins in the wallet will be returned.
+    ///
+    /// `limit` gives the maximum number of addresses to return.
+    ///
+    /// If `continue_from` is `None`, then addresses will be returned starting
+    /// from the last revealed derivation index. Otherwise, they will start from
+    /// the specified index (unless the last revealed index is lower).
+    pub fn list_revealed_addresses(
+        &self,
+        is_change: bool,
+        exclude_used: bool,
+        limit: usize,
+        continue_from: Option<ChildNumber>,
+    ) -> Result<ListRevealedAddressesResult, CommandError> {
+        let mut db_conn = self.db.connection();
+
+        let (desc, last_revealed) = if is_change {
+            (
+                self.config.main_descriptor.change_descriptor(),
+                db_conn.change_index(),
+            )
+        } else {
+            (
+                self.config.main_descriptor.receive_descriptor(),
+                db_conn.receive_index(),
+            )
+        };
+        // TODO: consider adding DB method to get coins by index range,
+        // possibly returning only the indices and not the whole coin.
+        let used_indices: HashSet<_> = db_conn
+            .coins(&[], &[])
+            .iter()
+            .filter_map(|(_, c)| (c.is_change == is_change).then_some(c.derivation_index))
+            .collect();
+
+        // Determine the start index depending on whether `continue_from` is set.
+        let start_index = continue_from.unwrap_or(last_revealed).min(last_revealed);
+
+        let mut revelaed_addresses = Vec::<_>::with_capacity(limit); // we could possibly use a lower capacity depending on start index
path: lianad/src/commands/mod.rs
user: login: pythcoiner
body: ```suggestion
        let mut revealed_addresses = Vec::<_>::with_capacity(limit); // we could possibly use a lower capacity depending on start index
```

about the comment, I think allocating w/ `limit` is good enough
created_at: 2025-05-05T03:39:58Z
author_association: COLLABORATOR
diff_hunk: @@ -477,6 +477,94 @@ impl DaemonControl {
         Ok(ListAddressesResult::new(addresses?))
     }
 
+    /// List revealed addresses. Addresses will be returned in order of
+    /// descending derivation index.
+    ///
+    /// `is_change` controls whether to return receive or change addresses.
+    ///
+    /// If `exclude_used` is `true`, then only those revealed addresses that
+    /// are unused by any coins in the wallet will be returned.
+    ///
+    /// `limit` gives the maximum number of addresses to return.
+    ///
+    /// If `continue_from` is `None`, then addresses will be returned starting
+    /// from the last revealed derivation index. Otherwise, they will start from
+    /// the specified index (unless the last revealed index is lower).
+    pub fn list_revealed_addresses(
+        &self,
+        is_change: bool,
+        exclude_used: bool,
+        limit: usize,
+        continue_from: Option<ChildNumber>,
+    ) -> Result<ListRevealedAddressesResult, CommandError> {
+        let mut db_conn = self.db.connection();
+
+        let (desc, last_revealed) = if is_change {
+            (
+                self.config.main_descriptor.change_descriptor(),
+                db_conn.change_index(),
+            )
+        } else {
+            (
+                self.config.main_descriptor.receive_descriptor(),
+                db_conn.receive_index(),
+            )
+        };
+        // TODO: consider adding DB method to get coins by index range,
+        // possibly returning only the indices and not the whole coin.
+        let used_indices: HashSet<_> = db_conn
+            .coins(&[], &[])
+            .iter()
+            .filter_map(|(_, c)| (c.is_change == is_change).then_some(c.derivation_index))
+            .collect();
+
+        // Determine the start index depending on whether `continue_from` is set.
+        let start_index = continue_from.unwrap_or(last_revealed).min(last_revealed);
+
+        let mut revelaed_addresses = Vec::<_>::with_capacity(limit); // we could possibly use a lower capacity depending on start index
+        let mut next_continue_from = None;
+        // This will store (index, address) pairs.
+        let mut derived_addresses = Vec::<_>::with_capacity(limit);
+        // Iterate in descending order.
+        for i in (0..=start_index.into()).rev() {
+            let index = ChildNumber::from(i);
path: lianad/src/commands/mod.rs
user: login: pythcoiner
body: ```suggestion
            let index = ChildNumber::from_normal_idx(i);
```
I think else an hardened ChildNumber can be hit and we should fail later at derivation time
created_at: 2025-05-05T03:49:04Z
author_association: COLLABORATOR
diff_hunk: @@ -477,6 +477,94 @@ impl DaemonControl {
         Ok(ListAddressesResult::new(addresses?))
     }
 
+    /// List revealed addresses. Addresses will be returned in order of
+    /// descending derivation index.
+    ///
+    /// `is_change` controls whether to return receive or change addresses.
+    ///
+    /// If `exclude_used` is `true`, then only those revealed addresses that
+    /// are unused by any coins in the wallet will be returned.
+    ///
+    /// `limit` gives the maximum number of addresses to return.
+    ///
+    /// If `continue_from` is `None`, then addresses will be returned starting
+    /// from the last revealed derivation index. Otherwise, they will start from
+    /// the specified index (unless the last revealed index is lower).
+    pub fn list_revealed_addresses(
+        &self,
+        is_change: bool,
+        exclude_used: bool,
+        limit: usize,
+        continue_from: Option<ChildNumber>,
+    ) -> Result<ListRevealedAddressesResult, CommandError> {
+        let mut db_conn = self.db.connection();
+
+        let (desc, last_revealed) = if is_change {
+            (
+                self.config.main_descriptor.change_descriptor(),
+                db_conn.change_index(),
+            )
+        } else {
+            (
+                self.config.main_descriptor.receive_descriptor(),
+                db_conn.receive_index(),
+            )
+        };
+        // TODO: consider adding DB method to get coins by index range,
+        // possibly returning only the indices and not the whole coin.
+        let used_indices: HashSet<_> = db_conn
+            .coins(&[], &[])
+            .iter()
+            .filter_map(|(_, c)| (c.is_change == is_change).then_some(c.derivation_index))
+            .collect();
+
+        // Determine the start index depending on whether `continue_from` is set.
+        let start_index = continue_from.unwrap_or(last_revealed).min(last_revealed);
+
+        let mut revelaed_addresses = Vec::<_>::with_capacity(limit); // we could possibly use a lower capacity depending on start index
+        let mut next_continue_from = None;
+        // This will store (index, address) pairs.
+        let mut derived_addresses = Vec::<_>::with_capacity(limit);
+        // Iterate in descending order.
+        for i in (0..=start_index.into()).rev() {
+            let index = ChildNumber::from(i);
+            if derived_addresses.len() == limit {
+                // We've reached the limit. There may be more addresses to list using pagination.
+                next_continue_from = Some(index);
+                break;
+            }
+            if !exclude_used || !used_indices.contains(&index) {
+                let addr = desc
+                    .derive(index, &self.secp)
+                    .address(self.config.bitcoin_config.network);
+                derived_addresses.push((index, addr));
+            }
+        }
+        // Now get the labels from DB (in multiple chunks).
+        let mut labels = HashMap::<String, String>::with_capacity(derived_addresses.len());
+        const CHUNK_SIZE: usize = 100;
path: lianad/src/commands/mod.rs
user: login: pythcoiner
body: there is already [`DUMP_LABELS_LIMIT`](https://github.com/wizardsardine/liana/blob/473dd425cce26ae1d783cf257410b3c67b9027c1/liana-gui/src/export.rs#L52), shouldn't we reuse it (and possibly move it to the backend client, since it'll not only be used for export?
created_at: 2025-05-05T04:00:46Z
author_association: COLLABORATOR
diff_hunk: @@ -477,6 +477,94 @@ impl DaemonControl {
         Ok(ListAddressesResult::new(addresses?))
     }
 
+    /// List revealed addresses. Addresses will be returned in order of
+    /// descending derivation index.
+    ///
+    /// `is_change` controls whether to return receive or change addresses.
+    ///
+    /// If `exclude_used` is `true`, then only those revealed addresses that
+    /// are unused by any coins in the wallet will be returned.
+    ///
+    /// `limit` gives the maximum number of addresses to return.
+    ///
+    /// If `continue_from` is `None`, then addresses will be returned starting
+    /// from the last revealed derivation index. Otherwise, they will start from
+    /// the specified index (unless the last revealed index is lower).
+    pub fn list_revealed_addresses(
+        &self,
+        is_change: bool,
+        exclude_used: bool,
+        limit: usize,
+        continue_from: Option<ChildNumber>,
+    ) -> Result<ListRevealedAddressesResult, CommandError> {
+        let mut db_conn = self.db.connection();
+
+        let (desc, last_revealed) = if is_change {
+            (
+                self.config.main_descriptor.change_descriptor(),
+                db_conn.change_index(),
+            )
+        } else {
+            (
+                self.config.main_descriptor.receive_descriptor(),
+                db_conn.receive_index(),
+            )
+        };
+        // TODO: consider adding DB method to get coins by index range,
+        // possibly returning only the indices and not the whole coin.
+        let used_indices: HashSet<_> = db_conn
+            .coins(&[], &[])
+            .iter()
+            .filter_map(|(_, c)| (c.is_change == is_change).then_some(c.derivation_index))
+            .collect();
path: lianad/src/commands/mod.rs
user: login: pythcoiner
body: I think here we should be able to "count" usage of an index, it can be useful to show in the gui when an address have been reused
created_at: 2025-05-05T04:10:11Z
author_association: COLLABORATOR
diff_hunk: @@ -1565,6 +1684,271 @@ mod tests {
         ms.shutdown();
     }
 
+    #[test]
+    fn list_revealed_addresses() {
+        let ms = DummyLiana::new(DummyBitcoind::new(), DummyDatabase::new());
+
+        let control = &ms.control();
+        let mut db_conn = control.db().lock().unwrap().connection();
+
+        // $ bitcoin-cli deriveaddresses "wsh(or_d(pk([aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/0/*),and_v(v:pkh([aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/0/*),older(10000))))#wx6v3mks" 0
+        // [
+        //   "bc1q9ksrc647hx8zp2cewl8p5f487dgux3777yees8rjcx46t4daqzzqt7yga8"
+        // ]
+        let addr0 = bitcoin::Address::from_str(
+            "bc1q9ksrc647hx8zp2cewl8p5f487dgux3777yees8rjcx46t4daqzzqt7yga8",
+        )
+        .unwrap()
+        .assume_checked();
+
+        // The wallet starts with index 0 already revealed:
+        let list = control
+            .list_revealed_addresses(false, false, 3, None)
+            .unwrap();
+        assert_eq!(list.addresses.len(), 1);
+        assert!(list.continue_from.is_none());
+        let revealed = list.addresses.first().unwrap();
+        assert_eq!(revealed.index, ChildNumber::from(0));
+        assert_eq!(revealed.address, addr0);
+        assert!(!revealed.is_used);
+        assert!(revealed.label.is_none());
+
+        // Generate new addresses up to and including index 7:
+        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 addr5 = control.get_new_address().address;
+        let addr6 = control.get_new_address().address;
+        let addr7 = control.get_new_address().address;
+        assert_eq!(control.get_info().receive_index, 7);
+
+        // Set some labels.
+        db_conn.update_labels(&HashMap::from([
+            (
+                LabelItem::Address(addr1.clone()),
+                Some("my test label 1".to_string()),
+            ),
+            (
+                LabelItem::Address(addr5.clone()),
+                Some("my test label 5".to_string()),
+            ),
+        ]));
+
+        // If we continue_from a value above our last index, we'll start from the last index.
+        let list = control
+            .list_revealed_addresses(false, false, 3, Some(ChildNumber::from(100)))
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(4)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(7)); // this is our last revealed index
+        assert_eq!(list.addresses[0].address, addr7);
+        assert!(!list.addresses[0].is_used);
+        assert!(list.addresses[0].label.is_none());
+        assert_eq!(list.addresses[1].index, ChildNumber::from(6));
+        assert_eq!(list.addresses[1].address, addr6);
+        assert!(!list.addresses[1].is_used);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(5));
+        assert_eq!(list.addresses[2].address, addr5);
+        assert!(!list.addresses[2].is_used);
+        assert_eq!(list.addresses[2].label, Some("my test label 5".to_string()));
+
+        // Passing `None` for `continue_from` will also start from the last revealed index:
+        let list = control
+            .list_revealed_addresses(false, false, 3, None)
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(4)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(7));
+        assert_eq!(list.addresses[0].address, addr7);
+        assert!(!list.addresses[0].is_used);
+        assert!(list.addresses[0].label.is_none());
+        assert_eq!(list.addresses[1].index, ChildNumber::from(6));
+        assert_eq!(list.addresses[1].address, addr6);
+        assert!(!list.addresses[1].is_used);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(5));
+        assert_eq!(list.addresses[2].address, addr5);
+        assert!(!list.addresses[2].is_used);
+        assert_eq!(list.addresses[2].label, Some("my test label 5".to_string()));
+
+        // Now continue pagination using the `continue_from` value from the result above:
+        let list = control
+            .list_revealed_addresses(false, false, 3, Some(ChildNumber::from(4)))
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(1)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(4));
+        assert_eq!(list.addresses[0].address, addr4);
+        assert!(!list.addresses[0].is_used);
+        assert!(list.addresses[0].label.is_none());
+        assert_eq!(list.addresses[1].index, ChildNumber::from(3));
+        assert_eq!(list.addresses[1].address, addr3);
+        assert!(!list.addresses[1].is_used);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(2));
+        assert_eq!(list.addresses[2].address, addr2);
+        assert!(!list.addresses[2].is_used);
+        assert!(list.addresses[2].label.is_none());
+
+        // This is the final page:
+        let list = control
+            .list_revealed_addresses(false, false, 3, Some(ChildNumber::from(1)))
+            .unwrap();
+        assert_eq!(list.addresses.len(), 2); // only two addresses even though limit was 3
+        assert!(list.continue_from.is_none()); // there are no more addresses to derive
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(1));
+        assert_eq!(list.addresses[0].address, addr1);
+        assert!(!list.addresses[0].is_used);
+        assert_eq!(list.addresses[0].label, Some("my test label 1".to_string()));
+        assert_eq!(list.addresses[1].index, ChildNumber::from(0));
+        assert_eq!(list.addresses[1].address, addr0);
+        assert!(!list.addresses[1].is_used);
+        assert!(list.addresses[1].label.is_none());
+
+        // Add a coin so that address with index 5 is used:
+        db_conn.new_unspent_coins(&[Coin {
+            outpoint: OutPoint::new(
+                Txid::from_str("617eab1fc0b03ee7f82ba70166725291783461f1a0e7975eaf8b5f8f674234f3")
+                    .unwrap(),
+                0,
+            ),
+            is_immature: false,
+            block_info: None,
+            amount: bitcoin::Amount::from_sat(80_000),
+            derivation_index: ChildNumber::from(5),
+            is_change: false,
+            spend_txid: None,
+            spend_block: None,
+            is_from_self: true,
+        }]);
+
+        // If we don't exclude used, results will be same as before, except index 5 is marked as used:
+        let list = control
+            .list_revealed_addresses(false, false, 3, None)
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(4)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(7));
+        assert_eq!(list.addresses[0].address, addr7);
+        assert!(!list.addresses[0].is_used);
+        assert!(list.addresses[0].label.is_none());
+        assert_eq!(list.addresses[1].index, ChildNumber::from(6));
+        assert_eq!(list.addresses[1].address, addr6);
+        assert!(!list.addresses[1].is_used);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(5));
+        assert_eq!(list.addresses[2].address, addr5);
+        assert!(list.addresses[2].is_used); // used
+        assert_eq!(list.addresses[2].label, Some("my test label 5".to_string()));
+
+        // If we exclude used, index 5 will be skipped:
+        let list = control
+            .list_revealed_addresses(false, true, 3, None)
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(3)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(7));
+        assert_eq!(list.addresses[0].address, addr7);
+        assert!(!list.addresses[0].is_used);
+        assert!(list.addresses[0].label.is_none());
+        assert_eq!(list.addresses[1].index, ChildNumber::from(6));
+        assert_eq!(list.addresses[1].address, addr6);
+        assert!(!list.addresses[1].is_used);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(4));
+        assert_eq!(list.addresses[2].address, addr4);
+        assert!(!list.addresses[2].is_used);
+        assert!(list.addresses[2].label.is_none());
+
+        // Similar behaviour if we continue from index 5. First without excluding used:
+        let list = control
+            .list_revealed_addresses(false, false, 3, Some(ChildNumber::from(5)))
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(2)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(5));
+        assert_eq!(list.addresses[0].address, addr5);
+        assert!(list.addresses[0].is_used); // used
+        assert_eq!(list.addresses[0].label, Some("my test label 5".to_string()));
+        assert_eq!(list.addresses[1].index, ChildNumber::from(4));
+        assert_eq!(list.addresses[1].address, addr4);
+        assert!(!list.addresses[1].is_used);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(3));
+        assert_eq!(list.addresses[2].address, addr3);
+        assert!(!list.addresses[2].is_used);
+        assert!(list.addresses[2].label.is_none());
+
+        // Now excluding used:
+        let list = control
+            .list_revealed_addresses(false, true, 3, Some(ChildNumber::from(5)))
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(1)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(4));
+        assert_eq!(list.addresses[0].address, addr4);
+        assert!(!list.addresses[0].is_used);
+        assert!(list.addresses[0].label.is_none());
+        assert_eq!(list.addresses[1].index, ChildNumber::from(3));
+        assert_eq!(list.addresses[1].address, addr3);
+        assert!(!list.addresses[1].is_used);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(2));
+        assert_eq!(list.addresses[2].address, addr2);
+        assert!(!list.addresses[2].is_used);
+        assert!(list.addresses[2].label.is_none());
+
+        // Check change address
+        // $ bitcoin-cli deriveaddresses "wsh(or_d(pk([aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/1/*),and_v(v:pkh([aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/1/*),older(10000))))#sqk8au7v" 0
+        // [
+        //   "bc1qd5m23jemr8mfj8x4q482zpspnxehg0jg0pyzrhxfg8xrrtyqewvqjrq3x6"
+        // ]
+        let change_addr0 = bitcoin::Address::from_str(
+            "bc1qd5m23jemr8mfj8x4q482zpspnxehg0jg0pyzrhxfg8xrrtyqewvqjrq3x6",
+        )
+        .unwrap()
+        .assume_checked();
+
+        // If we ask for change addresses, we only have the initial one revealed:
+        let list = control
+            .list_revealed_addresses(true, false, 3, None)
+            .unwrap();
+        assert_eq!(list.addresses.len(), 1);
+        assert!(list.continue_from.is_none());
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(0));
+        assert_eq!(list.addresses[0].address, change_addr0);
+        assert!(!list.addresses[0].is_used);
+        assert!(list.addresses[0].label.is_none());
+
+        // Finally, passing limit 0 returns an empty vector and the continue_from is same as it started with.
path: lianad/src/commands/mod.rs
user: login: pythcoiner
body: hum does it make sense to accept a request that obviouly leads to an empty list response? shouldn't we error or interpret as a "no limit"
created_at: 2025-05-05T04:35:11Z
author_association: COLLABORATOR
diff_hunk: @@ -26,12 +26,7 @@
 
 class Lianad(TailableProc):
     def __init__(
-        self,
-        datadir,
-        signer,
-        multi_desc,
-        bitcoin_backend,
-        legacy_datadir=False
+        self, datadir, signer, multi_desc, bitcoin_backend, legacy_datadir=False
path: tests/test_framework/lianad.py
user: login: pythcoiner
body: is there any reason this formating to change?
created_at: 2025-05-05T04:39:12Z
author_association: COLLABORATOR
diff_hunk: @@ -318,13 +319,7 @@ def lianad_multisig_legacy_datadir(bitcoin_backend, directory):
     signer = MultiSigner(4, {csv_value: 5}, is_taproot=USE_TAPROOT)
     main_desc = Descriptor.from_str(multisig_desc(signer, csv_value, USE_TAPROOT, 3, 2))
 
-    lianad = Lianad(
-        datadir,
-        signer,
-        main_desc,
-        bitcoin_backend,
-        legacy_datadir=True
-    )
+    lianad = Lianad(datadir, signer, main_desc, bitcoin_backend, legacy_datadir=True)
path: tests/fixtures.py
user: login: pythcoiner
body: is there any reason this formating to change?
created_at: 2025-05-05T04:40:10Z
author_association: COLLABORATOR
diff_hunk: @@ -308,6 +308,7 @@ def lianad_multisig(bitcoin_backend, directory):
 
     lianad.cleanup()
 
+
path: tests/fixtures.py
user: login: pythcoiner
body: is this newline intended?
created_at: 2025-05-05T04:40:34Z
author_association: COLLABORATOR
diff_hunk: @@ -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 `continue_from` 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.                                                |
+| `continue_from` | integer(optional) | For pagination, pass the corresponding 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 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.                                                                     |
+| `is_used`     | bool             | Whether the address has been used by any of the current coins in the wallet. |
path: doc/API.md
user: login: jp1ac4
body: Good idea. This should be fairly simple to change.
created_at: 2025-05-05T07:56:20Z
author_association: COLLABORATOR
diff_hunk: @@ -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 `continue_from` 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.                                                |
+| `continue_from` | integer(optional) | For pagination, pass the corresponding value from the previous response.                |
path: doc/API.md
user: login: jp1ac4
body: That's true. I think using `start_index` here would be better for consistency.

I considered using an `offset` parameter initially, but in the case of excluding used addresses, getting the start index to begin iterating from based on the offset would have required re-deriving addresses, and so for performance reasons, it was better to pass the start index explicitly.
created_at: 2025-05-05T08:04:45Z
author_association: COLLABORATOR
diff_hunk: @@ -477,6 +477,94 @@ impl DaemonControl {
         Ok(ListAddressesResult::new(addresses?))
     }
 
+    /// List revealed addresses. Addresses will be returned in order of
+    /// descending derivation index.
+    ///
+    /// `is_change` controls whether to return receive or change addresses.
+    ///
+    /// If `exclude_used` is `true`, then only those revealed addresses that
+    /// are unused by any coins in the wallet will be returned.
+    ///
+    /// `limit` gives the maximum number of addresses to return.
+    ///
+    /// If `continue_from` is `None`, then addresses will be returned starting
+    /// from the last revealed derivation index. Otherwise, they will start from
+    /// the specified index (unless the last revealed index is lower).
+    pub fn list_revealed_addresses(
+        &self,
+        is_change: bool,
+        exclude_used: bool,
+        limit: usize,
+        continue_from: Option<ChildNumber>,
+    ) -> Result<ListRevealedAddressesResult, CommandError> {
+        let mut db_conn = self.db.connection();
+
+        let (desc, last_revealed) = if is_change {
+            (
+                self.config.main_descriptor.change_descriptor(),
+                db_conn.change_index(),
+            )
+        } else {
+            (
+                self.config.main_descriptor.receive_descriptor(),
+                db_conn.receive_index(),
+            )
+        };
+        // TODO: consider adding DB method to get coins by index range,
+        // possibly returning only the indices and not the whole coin.
+        let used_indices: HashSet<_> = db_conn
+            .coins(&[], &[])
+            .iter()
+            .filter_map(|(_, c)| (c.is_change == is_change).then_some(c.derivation_index))
+            .collect();
+
+        // Determine the start index depending on whether `continue_from` is set.
+        let start_index = continue_from.unwrap_or(last_revealed).min(last_revealed);
+
+        let mut revelaed_addresses = Vec::<_>::with_capacity(limit); // we could possibly use a lower capacity depending on start index
path: lianad/src/commands/mod.rs
user: login: jp1ac4
body: Yeah, I should probably remove the comment.
created_at: 2025-05-05T08:06:14Z
author_association: COLLABORATOR
diff_hunk: @@ -477,6 +477,94 @@ impl DaemonControl {
         Ok(ListAddressesResult::new(addresses?))
     }
 
+    /// List revealed addresses. Addresses will be returned in order of
+    /// descending derivation index.
+    ///
+    /// `is_change` controls whether to return receive or change addresses.
+    ///
+    /// If `exclude_used` is `true`, then only those revealed addresses that
+    /// are unused by any coins in the wallet will be returned.
+    ///
+    /// `limit` gives the maximum number of addresses to return.
+    ///
+    /// If `continue_from` is `None`, then addresses will be returned starting
+    /// from the last revealed derivation index. Otherwise, they will start from
+    /// the specified index (unless the last revealed index is lower).
+    pub fn list_revealed_addresses(
+        &self,
+        is_change: bool,
+        exclude_used: bool,
+        limit: usize,
+        continue_from: Option<ChildNumber>,
+    ) -> Result<ListRevealedAddressesResult, CommandError> {
+        let mut db_conn = self.db.connection();
+
+        let (desc, last_revealed) = if is_change {
+            (
+                self.config.main_descriptor.change_descriptor(),
+                db_conn.change_index(),
+            )
+        } else {
+            (
+                self.config.main_descriptor.receive_descriptor(),
+                db_conn.receive_index(),
+            )
+        };
+        // TODO: consider adding DB method to get coins by index range,
+        // possibly returning only the indices and not the whole coin.
+        let used_indices: HashSet<_> = db_conn
+            .coins(&[], &[])
+            .iter()
+            .filter_map(|(_, c)| (c.is_change == is_change).then_some(c.derivation_index))
+            .collect();
+
+        // Determine the start index depending on whether `continue_from` is set.
+        let start_index = continue_from.unwrap_or(last_revealed).min(last_revealed);
+
+        let mut revelaed_addresses = Vec::<_>::with_capacity(limit); // we could possibly use a lower capacity depending on start index
+        let mut next_continue_from = None;
+        // This will store (index, address) pairs.
+        let mut derived_addresses = Vec::<_>::with_capacity(limit);
+        // Iterate in descending order.
+        for i in (0..=start_index.into()).rev() {
+            let index = ChildNumber::from(i);
path: lianad/src/commands/mod.rs
user: login: jp1ac4
body: I don't think this is strictly necessary as the start index is `min(last_used, continue_from)` and so we never iterate over an index above our last used.
created_at: 2025-05-05T08:11:43Z
author_association: COLLABORATOR
diff_hunk: @@ -477,6 +477,94 @@ impl DaemonControl {
         Ok(ListAddressesResult::new(addresses?))
     }
 
+    /// List revealed addresses. Addresses will be returned in order of
+    /// descending derivation index.
+    ///
+    /// `is_change` controls whether to return receive or change addresses.
+    ///
+    /// If `exclude_used` is `true`, then only those revealed addresses that
+    /// are unused by any coins in the wallet will be returned.
+    ///
+    /// `limit` gives the maximum number of addresses to return.
+    ///
+    /// If `continue_from` is `None`, then addresses will be returned starting
+    /// from the last revealed derivation index. Otherwise, they will start from
+    /// the specified index (unless the last revealed index is lower).
+    pub fn list_revealed_addresses(
+        &self,
+        is_change: bool,
+        exclude_used: bool,
+        limit: usize,
+        continue_from: Option<ChildNumber>,
+    ) -> Result<ListRevealedAddressesResult, CommandError> {
+        let mut db_conn = self.db.connection();
+
+        let (desc, last_revealed) = if is_change {
+            (
+                self.config.main_descriptor.change_descriptor(),
+                db_conn.change_index(),
+            )
+        } else {
+            (
+                self.config.main_descriptor.receive_descriptor(),
+                db_conn.receive_index(),
+            )
+        };
+        // TODO: consider adding DB method to get coins by index range,
+        // possibly returning only the indices and not the whole coin.
+        let used_indices: HashSet<_> = db_conn
+            .coins(&[], &[])
+            .iter()
+            .filter_map(|(_, c)| (c.is_change == is_change).then_some(c.derivation_index))
+            .collect();
+
+        // Determine the start index depending on whether `continue_from` is set.
+        let start_index = continue_from.unwrap_or(last_revealed).min(last_revealed);
+
+        let mut revelaed_addresses = Vec::<_>::with_capacity(limit); // we could possibly use a lower capacity depending on start index
+        let mut next_continue_from = None;
+        // This will store (index, address) pairs.
+        let mut derived_addresses = Vec::<_>::with_capacity(limit);
+        // Iterate in descending order.
+        for i in (0..=start_index.into()).rev() {
+            let index = ChildNumber::from(i);
+            if derived_addresses.len() == limit {
+                // We've reached the limit. There may be more addresses to list using pagination.
+                next_continue_from = Some(index);
+                break;
+            }
+            if !exclude_used || !used_indices.contains(&index) {
+                let addr = desc
+                    .derive(index, &self.secp)
+                    .address(self.config.bitcoin_config.network);
+                derived_addresses.push((index, addr));
+            }
+        }
+        // Now get the labels from DB (in multiple chunks).
+        let mut labels = HashMap::<String, String>::with_capacity(derived_addresses.len());
+        const CHUNK_SIZE: usize = 100;
path: lianad/src/commands/mod.rs
user: login: jp1ac4
body: Ah, thanks for pointing that out. Hmm, I think I may leave that for a follow-up refactoring.
created_at: 2025-05-05T08:14:26Z
author_association: COLLABORATOR
diff_hunk: @@ -26,12 +26,7 @@
 
 class Lianad(TailableProc):
     def __init__(
-        self,
-        datadir,
-        signer,
-        multi_desc,
-        bitcoin_backend,
-        legacy_datadir=False
+        self, datadir, signer, multi_desc, bitcoin_backend, legacy_datadir=False
path: tests/test_framework/lianad.py
user: login: jp1ac4
body: This and the other two formatting changes below were made by running the black formatter and are part of their own commit.
created_at: 2025-05-05T08:21:02Z
author_association: COLLABORATOR
diff_hunk: @@ -1565,6 +1684,271 @@ mod tests {
         ms.shutdown();
     }
 
+    #[test]
+    fn list_revealed_addresses() {
+        let ms = DummyLiana::new(DummyBitcoind::new(), DummyDatabase::new());
+
+        let control = &ms.control();
+        let mut db_conn = control.db().lock().unwrap().connection();
+
+        // $ bitcoin-cli deriveaddresses "wsh(or_d(pk([aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/0/*),and_v(v:pkh([aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/0/*),older(10000))))#wx6v3mks" 0
+        // [
+        //   "bc1q9ksrc647hx8zp2cewl8p5f487dgux3777yees8rjcx46t4daqzzqt7yga8"
+        // ]
+        let addr0 = bitcoin::Address::from_str(
+            "bc1q9ksrc647hx8zp2cewl8p5f487dgux3777yees8rjcx46t4daqzzqt7yga8",
+        )
+        .unwrap()
+        .assume_checked();
+
+        // The wallet starts with index 0 already revealed:
+        let list = control
+            .list_revealed_addresses(false, false, 3, None)
+            .unwrap();
+        assert_eq!(list.addresses.len(), 1);
+        assert!(list.continue_from.is_none());
+        let revealed = list.addresses.first().unwrap();
+        assert_eq!(revealed.index, ChildNumber::from(0));
+        assert_eq!(revealed.address, addr0);
+        assert!(!revealed.is_used);
+        assert!(revealed.label.is_none());
+
+        // Generate new addresses up to and including index 7:
+        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 addr5 = control.get_new_address().address;
+        let addr6 = control.get_new_address().address;
+        let addr7 = control.get_new_address().address;
+        assert_eq!(control.get_info().receive_index, 7);
+
+        // Set some labels.
+        db_conn.update_labels(&HashMap::from([
+            (
+                LabelItem::Address(addr1.clone()),
+                Some("my test label 1".to_string()),
+            ),
+            (
+                LabelItem::Address(addr5.clone()),
+                Some("my test label 5".to_string()),
+            ),
+        ]));
+
+        // If we continue_from a value above our last index, we'll start from the last index.
+        let list = control
+            .list_revealed_addresses(false, false, 3, Some(ChildNumber::from(100)))
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(4)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(7)); // this is our last revealed index
+        assert_eq!(list.addresses[0].address, addr7);
+        assert!(!list.addresses[0].is_used);
+        assert!(list.addresses[0].label.is_none());
+        assert_eq!(list.addresses[1].index, ChildNumber::from(6));
+        assert_eq!(list.addresses[1].address, addr6);
+        assert!(!list.addresses[1].is_used);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(5));
+        assert_eq!(list.addresses[2].address, addr5);
+        assert!(!list.addresses[2].is_used);
+        assert_eq!(list.addresses[2].label, Some("my test label 5".to_string()));
+
+        // Passing `None` for `continue_from` will also start from the last revealed index:
+        let list = control
+            .list_revealed_addresses(false, false, 3, None)
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(4)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(7));
+        assert_eq!(list.addresses[0].address, addr7);
+        assert!(!list.addresses[0].is_used);
+        assert!(list.addresses[0].label.is_none());
+        assert_eq!(list.addresses[1].index, ChildNumber::from(6));
+        assert_eq!(list.addresses[1].address, addr6);
+        assert!(!list.addresses[1].is_used);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(5));
+        assert_eq!(list.addresses[2].address, addr5);
+        assert!(!list.addresses[2].is_used);
+        assert_eq!(list.addresses[2].label, Some("my test label 5".to_string()));
+
+        // Now continue pagination using the `continue_from` value from the result above:
+        let list = control
+            .list_revealed_addresses(false, false, 3, Some(ChildNumber::from(4)))
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(1)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(4));
+        assert_eq!(list.addresses[0].address, addr4);
+        assert!(!list.addresses[0].is_used);
+        assert!(list.addresses[0].label.is_none());
+        assert_eq!(list.addresses[1].index, ChildNumber::from(3));
+        assert_eq!(list.addresses[1].address, addr3);
+        assert!(!list.addresses[1].is_used);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(2));
+        assert_eq!(list.addresses[2].address, addr2);
+        assert!(!list.addresses[2].is_used);
+        assert!(list.addresses[2].label.is_none());
+
+        // This is the final page:
+        let list = control
+            .list_revealed_addresses(false, false, 3, Some(ChildNumber::from(1)))
+            .unwrap();
+        assert_eq!(list.addresses.len(), 2); // only two addresses even though limit was 3
+        assert!(list.continue_from.is_none()); // there are no more addresses to derive
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(1));
+        assert_eq!(list.addresses[0].address, addr1);
+        assert!(!list.addresses[0].is_used);
+        assert_eq!(list.addresses[0].label, Some("my test label 1".to_string()));
+        assert_eq!(list.addresses[1].index, ChildNumber::from(0));
+        assert_eq!(list.addresses[1].address, addr0);
+        assert!(!list.addresses[1].is_used);
+        assert!(list.addresses[1].label.is_none());
+
+        // Add a coin so that address with index 5 is used:
+        db_conn.new_unspent_coins(&[Coin {
+            outpoint: OutPoint::new(
+                Txid::from_str("617eab1fc0b03ee7f82ba70166725291783461f1a0e7975eaf8b5f8f674234f3")
+                    .unwrap(),
+                0,
+            ),
+            is_immature: false,
+            block_info: None,
+            amount: bitcoin::Amount::from_sat(80_000),
+            derivation_index: ChildNumber::from(5),
+            is_change: false,
+            spend_txid: None,
+            spend_block: None,
+            is_from_self: true,
+        }]);
+
+        // If we don't exclude used, results will be same as before, except index 5 is marked as used:
+        let list = control
+            .list_revealed_addresses(false, false, 3, None)
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(4)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(7));
+        assert_eq!(list.addresses[0].address, addr7);
+        assert!(!list.addresses[0].is_used);
+        assert!(list.addresses[0].label.is_none());
+        assert_eq!(list.addresses[1].index, ChildNumber::from(6));
+        assert_eq!(list.addresses[1].address, addr6);
+        assert!(!list.addresses[1].is_used);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(5));
+        assert_eq!(list.addresses[2].address, addr5);
+        assert!(list.addresses[2].is_used); // used
+        assert_eq!(list.addresses[2].label, Some("my test label 5".to_string()));
+
+        // If we exclude used, index 5 will be skipped:
+        let list = control
+            .list_revealed_addresses(false, true, 3, None)
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(3)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(7));
+        assert_eq!(list.addresses[0].address, addr7);
+        assert!(!list.addresses[0].is_used);
+        assert!(list.addresses[0].label.is_none());
+        assert_eq!(list.addresses[1].index, ChildNumber::from(6));
+        assert_eq!(list.addresses[1].address, addr6);
+        assert!(!list.addresses[1].is_used);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(4));
+        assert_eq!(list.addresses[2].address, addr4);
+        assert!(!list.addresses[2].is_used);
+        assert!(list.addresses[2].label.is_none());
+
+        // Similar behaviour if we continue from index 5. First without excluding used:
+        let list = control
+            .list_revealed_addresses(false, false, 3, Some(ChildNumber::from(5)))
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(2)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(5));
+        assert_eq!(list.addresses[0].address, addr5);
+        assert!(list.addresses[0].is_used); // used
+        assert_eq!(list.addresses[0].label, Some("my test label 5".to_string()));
+        assert_eq!(list.addresses[1].index, ChildNumber::from(4));
+        assert_eq!(list.addresses[1].address, addr4);
+        assert!(!list.addresses[1].is_used);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(3));
+        assert_eq!(list.addresses[2].address, addr3);
+        assert!(!list.addresses[2].is_used);
+        assert!(list.addresses[2].label.is_none());
+
+        // Now excluding used:
+        let list = control
+            .list_revealed_addresses(false, true, 3, Some(ChildNumber::from(5)))
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(1)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(4));
+        assert_eq!(list.addresses[0].address, addr4);
+        assert!(!list.addresses[0].is_used);
+        assert!(list.addresses[0].label.is_none());
+        assert_eq!(list.addresses[1].index, ChildNumber::from(3));
+        assert_eq!(list.addresses[1].address, addr3);
+        assert!(!list.addresses[1].is_used);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(2));
+        assert_eq!(list.addresses[2].address, addr2);
+        assert!(!list.addresses[2].is_used);
+        assert!(list.addresses[2].label.is_none());
+
+        // Check change address
+        // $ bitcoin-cli deriveaddresses "wsh(or_d(pk([aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/1/*),and_v(v:pkh([aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/1/*),older(10000))))#sqk8au7v" 0
+        // [
+        //   "bc1qd5m23jemr8mfj8x4q482zpspnxehg0jg0pyzrhxfg8xrrtyqewvqjrq3x6"
+        // ]
+        let change_addr0 = bitcoin::Address::from_str(
+            "bc1qd5m23jemr8mfj8x4q482zpspnxehg0jg0pyzrhxfg8xrrtyqewvqjrq3x6",
+        )
+        .unwrap()
+        .assume_checked();
+
+        // If we ask for change addresses, we only have the initial one revealed:
+        let list = control
+            .list_revealed_addresses(true, false, 3, None)
+            .unwrap();
+        assert_eq!(list.addresses.len(), 1);
+        assert!(list.continue_from.is_none());
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(0));
+        assert_eq!(list.addresses[0].address, change_addr0);
+        assert!(!list.addresses[0].is_used);
+        assert!(list.addresses[0].label.is_none());
+
+        // Finally, passing limit 0 returns an empty vector and the continue_from is same as it started with.
path: lianad/src/commands/mod.rs
user: login: jp1ac4
body: I considered excluding that case, but in the end felt that this would require more code and that in principle the same logic should apply for a limit of 0 and simply return an empty list.
created_at: 2025-05-05T08:25:02Z
author_association: COLLABORATOR
diff_hunk: @@ -1565,6 +1684,271 @@ mod tests {
         ms.shutdown();
     }
 
+    #[test]
+    fn list_revealed_addresses() {
+        let ms = DummyLiana::new(DummyBitcoind::new(), DummyDatabase::new());
+
+        let control = &ms.control();
+        let mut db_conn = control.db().lock().unwrap().connection();
+
+        // $ bitcoin-cli deriveaddresses "wsh(or_d(pk([aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/0/*),and_v(v:pkh([aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/0/*),older(10000))))#wx6v3mks" 0
+        // [
+        //   "bc1q9ksrc647hx8zp2cewl8p5f487dgux3777yees8rjcx46t4daqzzqt7yga8"
+        // ]
+        let addr0 = bitcoin::Address::from_str(
+            "bc1q9ksrc647hx8zp2cewl8p5f487dgux3777yees8rjcx46t4daqzzqt7yga8",
+        )
+        .unwrap()
+        .assume_checked();
+
+        // The wallet starts with index 0 already revealed:
+        let list = control
+            .list_revealed_addresses(false, false, 3, None)
+            .unwrap();
+        assert_eq!(list.addresses.len(), 1);
+        assert!(list.continue_from.is_none());
+        let revealed = list.addresses.first().unwrap();
+        assert_eq!(revealed.index, ChildNumber::from(0));
+        assert_eq!(revealed.address, addr0);
+        assert!(!revealed.is_used);
+        assert!(revealed.label.is_none());
+
+        // Generate new addresses up to and including index 7:
+        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 addr5 = control.get_new_address().address;
+        let addr6 = control.get_new_address().address;
+        let addr7 = control.get_new_address().address;
+        assert_eq!(control.get_info().receive_index, 7);
+
+        // Set some labels.
+        db_conn.update_labels(&HashMap::from([
+            (
+                LabelItem::Address(addr1.clone()),
+                Some("my test label 1".to_string()),
+            ),
+            (
+                LabelItem::Address(addr5.clone()),
+                Some("my test label 5".to_string()),
+            ),
+        ]));
+
+        // If we continue_from a value above our last index, we'll start from the last index.
+        let list = control
+            .list_revealed_addresses(false, false, 3, Some(ChildNumber::from(100)))
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(4)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(7)); // this is our last revealed index
+        assert_eq!(list.addresses[0].address, addr7);
+        assert!(!list.addresses[0].is_used);
+        assert!(list.addresses[0].label.is_none());
+        assert_eq!(list.addresses[1].index, ChildNumber::from(6));
+        assert_eq!(list.addresses[1].address, addr6);
+        assert!(!list.addresses[1].is_used);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(5));
+        assert_eq!(list.addresses[2].address, addr5);
+        assert!(!list.addresses[2].is_used);
+        assert_eq!(list.addresses[2].label, Some("my test label 5".to_string()));
+
+        // Passing `None` for `continue_from` will also start from the last revealed index:
+        let list = control
+            .list_revealed_addresses(false, false, 3, None)
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(4)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(7));
+        assert_eq!(list.addresses[0].address, addr7);
+        assert!(!list.addresses[0].is_used);
+        assert!(list.addresses[0].label.is_none());
+        assert_eq!(list.addresses[1].index, ChildNumber::from(6));
+        assert_eq!(list.addresses[1].address, addr6);
+        assert!(!list.addresses[1].is_used);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(5));
+        assert_eq!(list.addresses[2].address, addr5);
+        assert!(!list.addresses[2].is_used);
+        assert_eq!(list.addresses[2].label, Some("my test label 5".to_string()));
+
+        // Now continue pagination using the `continue_from` value from the result above:
+        let list = control
+            .list_revealed_addresses(false, false, 3, Some(ChildNumber::from(4)))
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(1)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(4));
+        assert_eq!(list.addresses[0].address, addr4);
+        assert!(!list.addresses[0].is_used);
+        assert!(list.addresses[0].label.is_none());
+        assert_eq!(list.addresses[1].index, ChildNumber::from(3));
+        assert_eq!(list.addresses[1].address, addr3);
+        assert!(!list.addresses[1].is_used);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(2));
+        assert_eq!(list.addresses[2].address, addr2);
+        assert!(!list.addresses[2].is_used);
+        assert!(list.addresses[2].label.is_none());
+
+        // This is the final page:
+        let list = control
+            .list_revealed_addresses(false, false, 3, Some(ChildNumber::from(1)))
+            .unwrap();
+        assert_eq!(list.addresses.len(), 2); // only two addresses even though limit was 3
+        assert!(list.continue_from.is_none()); // there are no more addresses to derive
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(1));
+        assert_eq!(list.addresses[0].address, addr1);
+        assert!(!list.addresses[0].is_used);
+        assert_eq!(list.addresses[0].label, Some("my test label 1".to_string()));
+        assert_eq!(list.addresses[1].index, ChildNumber::from(0));
+        assert_eq!(list.addresses[1].address, addr0);
+        assert!(!list.addresses[1].is_used);
+        assert!(list.addresses[1].label.is_none());
+
+        // Add a coin so that address with index 5 is used:
+        db_conn.new_unspent_coins(&[Coin {
+            outpoint: OutPoint::new(
+                Txid::from_str("617eab1fc0b03ee7f82ba70166725291783461f1a0e7975eaf8b5f8f674234f3")
+                    .unwrap(),
+                0,
+            ),
+            is_immature: false,
+            block_info: None,
+            amount: bitcoin::Amount::from_sat(80_000),
+            derivation_index: ChildNumber::from(5),
+            is_change: false,
+            spend_txid: None,
+            spend_block: None,
+            is_from_self: true,
+        }]);
+
+        // If we don't exclude used, results will be same as before, except index 5 is marked as used:
+        let list = control
+            .list_revealed_addresses(false, false, 3, None)
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(4)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(7));
+        assert_eq!(list.addresses[0].address, addr7);
+        assert!(!list.addresses[0].is_used);
+        assert!(list.addresses[0].label.is_none());
+        assert_eq!(list.addresses[1].index, ChildNumber::from(6));
+        assert_eq!(list.addresses[1].address, addr6);
+        assert!(!list.addresses[1].is_used);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(5));
+        assert_eq!(list.addresses[2].address, addr5);
+        assert!(list.addresses[2].is_used); // used
+        assert_eq!(list.addresses[2].label, Some("my test label 5".to_string()));
+
+        // If we exclude used, index 5 will be skipped:
+        let list = control
+            .list_revealed_addresses(false, true, 3, None)
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(3)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(7));
+        assert_eq!(list.addresses[0].address, addr7);
+        assert!(!list.addresses[0].is_used);
+        assert!(list.addresses[0].label.is_none());
+        assert_eq!(list.addresses[1].index, ChildNumber::from(6));
+        assert_eq!(list.addresses[1].address, addr6);
+        assert!(!list.addresses[1].is_used);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(4));
+        assert_eq!(list.addresses[2].address, addr4);
+        assert!(!list.addresses[2].is_used);
+        assert!(list.addresses[2].label.is_none());
+
+        // Similar behaviour if we continue from index 5. First without excluding used:
+        let list = control
+            .list_revealed_addresses(false, false, 3, Some(ChildNumber::from(5)))
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(2)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(5));
+        assert_eq!(list.addresses[0].address, addr5);
+        assert!(list.addresses[0].is_used); // used
+        assert_eq!(list.addresses[0].label, Some("my test label 5".to_string()));
+        assert_eq!(list.addresses[1].index, ChildNumber::from(4));
+        assert_eq!(list.addresses[1].address, addr4);
+        assert!(!list.addresses[1].is_used);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(3));
+        assert_eq!(list.addresses[2].address, addr3);
+        assert!(!list.addresses[2].is_used);
+        assert!(list.addresses[2].label.is_none());
+
+        // Now excluding used:
+        let list = control
+            .list_revealed_addresses(false, true, 3, Some(ChildNumber::from(5)))
+            .unwrap();
+        assert_eq!(list.addresses.len(), 3);
+        assert_eq!(list.continue_from, Some(ChildNumber::from(1)));
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(4));
+        assert_eq!(list.addresses[0].address, addr4);
+        assert!(!list.addresses[0].is_used);
+        assert!(list.addresses[0].label.is_none());
+        assert_eq!(list.addresses[1].index, ChildNumber::from(3));
+        assert_eq!(list.addresses[1].address, addr3);
+        assert!(!list.addresses[1].is_used);
+        assert!(list.addresses[1].label.is_none());
+        assert_eq!(list.addresses[2].index, ChildNumber::from(2));
+        assert_eq!(list.addresses[2].address, addr2);
+        assert!(!list.addresses[2].is_used);
+        assert!(list.addresses[2].label.is_none());
+
+        // Check change address
+        // $ bitcoin-cli deriveaddresses "wsh(or_d(pk([aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/1/*),and_v(v:pkh([aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/1/*),older(10000))))#sqk8au7v" 0
+        // [
+        //   "bc1qd5m23jemr8mfj8x4q482zpspnxehg0jg0pyzrhxfg8xrrtyqewvqjrq3x6"
+        // ]
+        let change_addr0 = bitcoin::Address::from_str(
+            "bc1qd5m23jemr8mfj8x4q482zpspnxehg0jg0pyzrhxfg8xrrtyqewvqjrq3x6",
+        )
+        .unwrap()
+        .assume_checked();
+
+        // If we ask for change addresses, we only have the initial one revealed:
+        let list = control
+            .list_revealed_addresses(true, false, 3, None)
+            .unwrap();
+        assert_eq!(list.addresses.len(), 1);
+        assert!(list.continue_from.is_none());
+
+        assert_eq!(list.addresses[0].index, ChildNumber::from(0));
+        assert_eq!(list.addresses[0].address, change_addr0);
+        assert!(!list.addresses[0].is_used);
+        assert!(list.addresses[0].label.is_none());
+
+        // Finally, passing limit 0 returns an empty vector and the continue_from is same as it started with.
path: lianad/src/commands/mod.rs
user: login: jp1ac4
body: In a similar way, you can do a SQL query with `LIMIT 0` and return an empty result set.
created_at: 2025-05-05T08:25:35Z
author_association: COLLABORATOR
diff_hunk: @@ -477,6 +477,94 @@ impl DaemonControl {
         Ok(ListAddressesResult::new(addresses?))
     }
 
+    /// List revealed addresses. Addresses will be returned in order of
+    /// descending derivation index.
+    ///
+    /// `is_change` controls whether to return receive or change addresses.
+    ///
+    /// If `exclude_used` is `true`, then only those revealed addresses that
+    /// are unused by any coins in the wallet will be returned.
+    ///
+    /// `limit` gives the maximum number of addresses to return.
+    ///
+    /// If `continue_from` is `None`, then addresses will be returned starting
+    /// from the last revealed derivation index. Otherwise, they will start from
+    /// the specified index (unless the last revealed index is lower).
path: lianad/src/commands/mod.rs
user: login: jp1ac4
body: OK thanks for pointing this out, will take a look and update.
created_at: 2025-05-05T08:26:42Z
author_association: COLLABORATOR
diff_hunk: @@ -1317,6 +1405,37 @@ impl ListAddressesResult {
     }
 }
 
+/// A revealed address entry in the list returned by `list_revealed_addresses`.
path: lianad/src/commands/mod.rs
user: login: jp1ac4
body: Good idea, I'll make this and the other similar changes.
created_at: 2025-05-05T08:27:22Z
author_association: COLLABORATOR
diff_hunk: @@ -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 `continue_from` 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.                                                |
+| `continue_from` | integer(optional) | For pagination, pass the corresponding 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 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.                                                                     |
+| `is_used`     | bool             | Whether the address has been used by any of the current coins in the wallet. |
path: doc/API.md
user: login: jp1ac4
body: I've changed this field to `used_count: u32`.
created_at: 2025-05-05T13:04:03Z
author_association: COLLABORATOR
diff_hunk: @@ -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 `continue_from` 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.                                                |
+| `continue_from` | integer(optional) | For pagination, pass the corresponding value from the previous response.                |
path: doc/API.md
user: login: jp1ac4
body: I've changed the parameter name to `start_index`.
created_at: 2025-05-05T13:04:25Z
author_association: COLLABORATOR
diff_hunk: @@ -477,6 +477,94 @@ impl DaemonControl {
         Ok(ListAddressesResult::new(addresses?))
     }
 
+    /// List revealed addresses. Addresses will be returned in order of
+    /// descending derivation index.
+    ///
+    /// `is_change` controls whether to return receive or change addresses.
+    ///
+    /// If `exclude_used` is `true`, then only those revealed addresses that
+    /// are unused by any coins in the wallet will be returned.
+    ///
+    /// `limit` gives the maximum number of addresses to return.
+    ///
+    /// If `continue_from` is `None`, then addresses will be returned starting
+    /// from the last revealed derivation index. Otherwise, they will start from
+    /// the specified index (unless the last revealed index is lower).
path: lianad/src/commands/mod.rs
user: login: jp1ac4
body: I've added a parameters section and used bullet points for each parameter.
created_at: 2025-05-05T13:04:54Z
author_association: COLLABORATOR
diff_hunk: @@ -1317,6 +1405,37 @@ impl ListAddressesResult {
     }
 }
 
+/// A revealed address entry in the list returned by `list_revealed_addresses`.
path: lianad/src/commands/mod.rs
user: login: jp1ac4
body: Done.
created_at: 2025-05-05T13:05:04Z
author_association: COLLABORATOR
diff_hunk: @@ -1317,6 +1405,37 @@ impl ListAddressesResult {
     }
 }
 
+/// A revealed address entry in the list returned by `list_revealed_addresses`.
+#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
+pub struct ListRevealedAddressesEntry {
+    /// The address's derivation index.
+    pub index: ChildNumber,
+    /// The address.
+    pub address: bitcoin::Address,
+    /// Label assigned to the address, if any.
+    pub label: Option<String>,
+    /// Whether the address has been used by any coins in the wallet as per the
+    /// current best chain. This also takes into account any coins that are
+    /// currently unconfirmed, but ignores any coins that may have been replaced
+    /// or otherwise dropped from the mempool.
+    pub is_used: bool,
+}
+
+/// Result of a `list_revealed_addresses` request.
+#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
+pub struct ListRevealedAddressesResult {
+    /// Revealed addresses in order of descending derivation index.
+    pub addresses: Vec<ListRevealedAddressesEntry>,
+    /// `continue_from` being set to some value indicates that there may
+    /// be more addresses that can be listed with pagination. The next
+    /// `list_revealed_addresses` request can be continued with this
path: lianad/src/commands/mod.rs
user: login: jp1ac4
body: Done.
created_at: 2025-05-05T13:05:16Z
author_association: COLLABORATOR
diff_hunk: @@ -477,6 +477,94 @@ impl DaemonControl {
         Ok(ListAddressesResult::new(addresses?))
     }
 
+    /// List revealed addresses. Addresses will be returned in order of
+    /// descending derivation index.
+    ///
+    /// `is_change` controls whether to return receive or change addresses.
+    ///
+    /// If `exclude_used` is `true`, then only those revealed addresses that
+    /// are unused by any coins in the wallet will be returned.
+    ///
+    /// `limit` gives the maximum number of addresses to return.
+    ///
+    /// If `continue_from` is `None`, then addresses will be returned starting
+    /// from the last revealed derivation index. Otherwise, they will start from
+    /// the specified index (unless the last revealed index is lower).
+    pub fn list_revealed_addresses(
+        &self,
+        is_change: bool,
+        exclude_used: bool,
+        limit: usize,
+        continue_from: Option<ChildNumber>,
+    ) -> Result<ListRevealedAddressesResult, CommandError> {
+        let mut db_conn = self.db.connection();
+
+        let (desc, last_revealed) = if is_change {
+            (
+                self.config.main_descriptor.change_descriptor(),
+                db_conn.change_index(),
+            )
+        } else {
+            (
+                self.config.main_descriptor.receive_descriptor(),
+                db_conn.receive_index(),
+            )
+        };
+        // TODO: consider adding DB method to get coins by index range,
+        // possibly returning only the indices and not the whole coin.
+        let used_indices: HashSet<_> = db_conn
+            .coins(&[], &[])
+            .iter()
+            .filter_map(|(_, c)| (c.is_change == is_change).then_some(c.derivation_index))
+            .collect();
+
+        // Determine the start index depending on whether `continue_from` is set.
+        let start_index = continue_from.unwrap_or(last_revealed).min(last_revealed);
+
+        let mut revelaed_addresses = Vec::<_>::with_capacity(limit); // we could possibly use a lower capacity depending on start index
path: lianad/src/commands/mod.rs
user: login: jp1ac4
body: Comment removed.
created_at: 2025-05-05T13:05:26Z
author_association: COLLABORATOR
diff_hunk: @@ -1317,6 +1405,37 @@ impl ListAddressesResult {
     }
 }
 
+/// A revealed address entry in the list returned by `list_revealed_addresses`.
+#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
+pub struct ListRevealedAddressesEntry {
+    /// The address's derivation index.
+    pub index: ChildNumber,
+    /// The address.
+    pub address: bitcoin::Address,
+    /// Label assigned to the address, if any.
+    pub label: Option<String>,
+    /// Whether the address has been used by any coins in the wallet as per the
+    /// current best chain. This also takes into account any coins that are
+    /// currently unconfirmed, but ignores any coins that may have been replaced
+    /// or otherwise dropped from the mempool.
+    pub is_used: bool,
+}
+
+/// Result of a `list_revealed_addresses` request.
path: lianad/src/commands/mod.rs
user: login: jp1ac4
body: Done.
created_at: 2025-05-05T13:05:40Z
author_association: COLLABORATOR
diff_hunk: @@ -477,6 +477,94 @@ impl DaemonControl {
         Ok(ListAddressesResult::new(addresses?))
     }
 
+    /// List revealed addresses. Addresses will be returned in order of
+    /// descending derivation index.
+    ///
+    /// `is_change` controls whether to return receive or change addresses.
+    ///
+    /// If `exclude_used` is `true`, then only those revealed addresses that
+    /// are unused by any coins in the wallet will be returned.
+    ///
+    /// `limit` gives the maximum number of addresses to return.
+    ///
+    /// If `continue_from` is `None`, then addresses will be returned starting
+    /// from the last revealed derivation index. Otherwise, they will start from
+    /// the specified index (unless the last revealed index is lower).
+    pub fn list_revealed_addresses(
+        &self,
+        is_change: bool,
+        exclude_used: bool,
+        limit: usize,
+        continue_from: Option<ChildNumber>,
+    ) -> Result<ListRevealedAddressesResult, CommandError> {
+        let mut db_conn = self.db.connection();
+
+        let (desc, last_revealed) = if is_change {
+            (
+                self.config.main_descriptor.change_descriptor(),
+                db_conn.change_index(),
+            )
+        } else {
+            (
+                self.config.main_descriptor.receive_descriptor(),
+                db_conn.receive_index(),
+            )
+        };
+        // TODO: consider adding DB method to get coins by index range,
+        // possibly returning only the indices and not the whole coin.
+        let used_indices: HashSet<_> = db_conn
+            .coins(&[], &[])
+            .iter()
+            .filter_map(|(_, c)| (c.is_change == is_change).then_some(c.derivation_index))
+            .collect();
+
+        // Determine the start index depending on whether `continue_from` is set.
+        let start_index = continue_from.unwrap_or(last_revealed).min(last_revealed);
+
+        let mut revelaed_addresses = Vec::<_>::with_capacity(limit); // we could possibly use a lower capacity depending on start index
+        let mut next_continue_from = None;
+        // This will store (index, address) pairs.
+        let mut derived_addresses = Vec::<_>::with_capacity(limit);
+        // Iterate in descending order.
+        for i in (0..=start_index.into()).rev() {
+            let index = ChildNumber::from(i);
path: lianad/src/commands/mod.rs
user: login: jp1ac4
body: I've included a test that passes a hardened index.
created_at: 2025-05-05T13:06:21Z
author_association: COLLABORATOR
diff_hunk: @@ -477,6 +477,94 @@ impl DaemonControl {
         Ok(ListAddressesResult::new(addresses?))
     }
 
+    /// List revealed addresses. Addresses will be returned in order of
+    /// descending derivation index.
+    ///
+    /// `is_change` controls whether to return receive or change addresses.
+    ///
+    /// If `exclude_used` is `true`, then only those revealed addresses that
+    /// are unused by any coins in the wallet will be returned.
+    ///
+    /// `limit` gives the maximum number of addresses to return.
+    ///
+    /// If `continue_from` is `None`, then addresses will be returned starting
+    /// from the last revealed derivation index. Otherwise, they will start from
+    /// the specified index (unless the last revealed index is lower).
+    pub fn list_revealed_addresses(
+        &self,
+        is_change: bool,
+        exclude_used: bool,
+        limit: usize,
+        continue_from: Option<ChildNumber>,
+    ) -> Result<ListRevealedAddressesResult, CommandError> {
+        let mut db_conn = self.db.connection();
+
+        let (desc, last_revealed) = if is_change {
+            (
+                self.config.main_descriptor.change_descriptor(),
+                db_conn.change_index(),
+            )
+        } else {
+            (
+                self.config.main_descriptor.receive_descriptor(),
+                db_conn.receive_index(),
+            )
+        };
+        // TODO: consider adding DB method to get coins by index range,
+        // possibly returning only the indices and not the whole coin.
+        let used_indices: HashSet<_> = db_conn
+            .coins(&[], &[])
+            .iter()
+            .filter_map(|(_, c)| (c.is_change == is_change).then_some(c.derivation_index))
+            .collect();
path: lianad/src/commands/mod.rs
user: login: jp1ac4
body: Done. I also skip any coins that have a derivation index higher than the start index.
created_at: 2025-05-05T13:07:25Z
author_association: COLLABORATOR
diff_hunk: @@ -318,13 +319,7 @@ def lianad_multisig_legacy_datadir(bitcoin_backend, directory):
     signer = MultiSigner(4, {csv_value: 5}, is_taproot=USE_TAPROOT)
     main_desc = Descriptor.from_str(multisig_desc(signer, csv_value, USE_TAPROOT, 3, 2))
 
-    lianad = Lianad(
-        datadir,
-        signer,
-        main_desc,
-        bitcoin_backend,
-        legacy_datadir=True
-    )
+    lianad = Lianad(datadir, signer, main_desc, bitcoin_backend, legacy_datadir=True)
path: tests/fixtures.py
user: login: jp1ac4
body: As per https://github.com/wizardsardine/liana/pull/1692#discussion_r2073032721.
created_at: 2025-05-05T13:08:49Z
author_association: COLLABORATOR
diff_hunk: @@ -308,6 +308,7 @@ def lianad_multisig(bitcoin_backend, directory):
 
     lianad.cleanup()
 
+
path: tests/fixtures.py
user: login: jp1ac4
body: As per https://github.com/wizardsardine/liana/pull/1692#discussion_r2073032721.
created_at: 2025-05-05T13:09:06Z
author_association: COLLABORATOR
diff_hunk: @@ -477,6 +477,100 @@ impl DaemonControl {
         Ok(ListAddressesResult::new(addresses?))
     }
 
+    /// List revealed addresses. Addresses will be returned in order of
+    /// descending derivation index.
+    ///
+    /// # Parameters
+    ///
+    /// - `is_change`: set to `false` to return receive addresses and `true` for change addresses.
+    ///
+    /// - `exclude_used`:  set to `true` to return only those revealed addresses that
+    ///   are unused by any coins in the wallet.
+    ///
+    /// - `limit`: the maximum number of addresses to return.
+    ///
+    /// - `start_index`: the derivation index from which to start listing addresses. As addresses are
+    ///   returned in descending order, `start_index` is the highest index that can be returned.
+    ///   If set to `None`, then addresses will be returned starting from the last revealed index.
+    ///   As there are no revealed addresses with a derivation index higher than the last revealed index,
+    ///   setting this parameter to a higher value will be the same as setting it to `None`.
+    pub fn list_revealed_addresses(
+        &self,
+        is_change: bool,
+        exclude_used: bool,
+        limit: usize,
+        start_index: Option<ChildNumber>,
+    ) -> Result<ListRevealedAddressesResult, CommandError> {
+        let mut db_conn = self.db.connection();
+
+        let (desc, last_revealed) = if is_change {
+            (
+                self.config.main_descriptor.change_descriptor(),
+                db_conn.change_index(),
+            )
+        } else {
+            (
+                self.config.main_descriptor.receive_descriptor(),
+                db_conn.receive_index(),
+            )
+        };
+
+        // Determine the index to start deriving addresses from, ensuring it is not higher than the last revealed.
+        let start_index = start_index.unwrap_or(last_revealed).min(last_revealed);
+
+        // Count how many times each (used) address has been used.
+        let mut used_counts = HashMap::<ChildNumber, u32>::new();
+        // TODO: consider adding DB method to get coins or used indices by index range.
+        for coin in db_conn.coins(&[], &[]).values() {
+            if coin.is_change == is_change && coin.derivation_index <= start_index {
+                *used_counts.entry(coin.derivation_index).or_insert(0) += 1;
+            }
+        }
+
+        let mut revelaed_addresses = Vec::<_>::with_capacity(limit);
path: lianad/src/commands/mod.rs
user: login: pythcoiner
body: nit
```suggestion
        let mut revealed_addresses = Vec::<_>::with_capacity(limit);
```
created_at: 2025-05-06T07:09:32Z
author_association: COLLABORATOR
diff_hunk: @@ -477,6 +477,100 @@ impl DaemonControl {
         Ok(ListAddressesResult::new(addresses?))
     }
 
+    /// List revealed addresses. Addresses will be returned in order of
+    /// descending derivation index.
+    ///
+    /// # Parameters
+    ///
+    /// - `is_change`: set to `false` to return receive addresses and `true` for change addresses.
+    ///
+    /// - `exclude_used`:  set to `true` to return only those revealed addresses that
+    ///   are unused by any coins in the wallet.
+    ///
+    /// - `limit`: the maximum number of addresses to return.
+    ///
+    /// - `start_index`: the derivation index from which to start listing addresses. As addresses are
+    ///   returned in descending order, `start_index` is the highest index that can be returned.
+    ///   If set to `None`, then addresses will be returned starting from the last revealed index.
+    ///   As there are no revealed addresses with a derivation index higher than the last revealed index,
+    ///   setting this parameter to a higher value will be the same as setting it to `None`.
+    pub fn list_revealed_addresses(
+        &self,
+        is_change: bool,
+        exclude_used: bool,
+        limit: usize,
+        start_index: Option<ChildNumber>,
+    ) -> Result<ListRevealedAddressesResult, CommandError> {
+        let mut db_conn = self.db.connection();
+
+        let (desc, last_revealed) = if is_change {
+            (
+                self.config.main_descriptor.change_descriptor(),
+                db_conn.change_index(),
+            )
+        } else {
+            (
+                self.config.main_descriptor.receive_descriptor(),
+                db_conn.receive_index(),
+            )
+        };
+
+        // Determine the index to start deriving addresses from, ensuring it is not higher than the last revealed.
+        let start_index = start_index.unwrap_or(last_revealed).min(last_revealed);
+
+        // Count how many times each (used) address has been used.
+        let mut used_counts = HashMap::<ChildNumber, u32>::new();
+        // TODO: consider adding DB method to get coins or used indices by index range.
+        for coin in db_conn.coins(&[], &[]).values() {
+            if coin.is_change == is_change && coin.derivation_index <= start_index {
+                *used_counts.entry(coin.derivation_index).or_insert(0) += 1;
+            }
+        }
+
+        let mut revelaed_addresses = Vec::<_>::with_capacity(limit);
path: lianad/src/commands/mod.rs
user: login: jp1ac4
body: Thanks for spotting. I decided just to rename it to `addresses`.
created_at: 2025-05-07T07:50:43Z
author_association: COLLABORATOR

PR comments:
user: login: jp1ac4
created_at: 2025-05-02T17:28:38Z
body: Note the `continue_from` in the response is for the convenience of the user, but the user could otherwise determine the value to use for the next request from the previous result set (if addresses returned equals limit and lowest index > 0, then lowest index - 1).
author_association: COLLABORATOR
user: login: pythcoiner
created_at: 2025-05-05T04:46:58Z
body: I think we should also have a test that confirm we cannot pass an Hardened child number as arg
author_association: COLLABORATOR
user: login: jp1ac4
created_at: 2025-05-05T08:23:37Z
body: > I think we should also have a test that confirm we cannot pass an Hardened child number as arg

I'll add a test for that. But note that it would not error but would simply return addresses starting from the last revealed.
author_association: COLLABORATOR
user: login: jp1ac4
created_at: 2025-05-05T13:10:17Z
body: I've updated based on the comments.
author_association: COLLABORATOR
user: login: pythcoiner
created_at: 2025-05-06T07:10:39Z
body: utACK b2b78d96
author_association: COLLABORATOR
user: login: pythcoiner
created_at: 2025-05-08T14:17:58Z
body: utACK 3d6a22e
author_association: COLLABORATOR