Merge #1692: feat: add listrevealedaddresses command
3d6a22ea05df70d6f0c11f2f00c85dc16975ad28 feat: add rpc command to list revealed addresses (Michael Mallan) 4945dba7c066d1434fa38a09a955339b7182e693 feat: add command to list revealed addresses (Michael Mallan) 75f37708b3b1f6a89e0f5488c95618a07356f166 test: implement methods for getting/setting labels (Michael Mallan) 16b96b5456b46cd3703d4829b24746c8a489b3ed style: apply black formatting (Michael Mallan) Pull request description: 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). ACKs for top commit: pythcoiner: utACK 3d6a22e Tree-SHA512: 0efa93bc40d0751cbe64570f9789b3a701e0bc5aab982da9ed9f21117a2eea073a81388370d56a9021c0ff87b6cbaec7338339427e6c879e8e97eababb107f57
This commit is contained in:
commit
619ea6923f
35
doc/API.md
35
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.
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -26,12 +26,7 @@ from test_framework.serializations import (
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user