Merge #605: Add labels support to lianad

204c160c4d01e5d12130a89347923e8d7d4f0f57 tests: test the RPC interface for managing labels (Antoine Poinsot)
bf3eb33900d52cd43e7c7cae2e42dc2075a6b249 lianad api: expose coin address (edouard)
7338e6f988a553d022903e7fd478e134be905762 Add labels to lianad (edouard)

Pull request description:

ACKs for top commit:
  darosior:
    ACK 204c160c4d01e5d12130a89347923e8d7d4f0f57

Tree-SHA512: 24ff9ea9ee5df0458534dd28a40d485f8bf9e110463faf78450f48ffbd18137f74f73aecf8234021d03a374879a6dd1c7188f162d81d7539cd790845a2855a1f
This commit is contained in:
Antoine Poinsot 2023-08-28 14:37:04 +02:00
commit cf17cc2cd6
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
9 changed files with 540 additions and 10 deletions

View File

@ -20,6 +20,8 @@ Commands must be sent as valid JSONRPC 2.0 requests, ending with a `\n`.
| [`listconfirmed`](#listconfirmed) | List of confirmed transactions of incoming and outgoing funds |
| [`listtransactions`](#listtransactions) | List of transactions with the given txids |
| [`createrecovery`](#createrecovery) | Create a recovery transaction to sweep expired coins |
| [`updatelabels`](#updatelabels) | Update the labels |
| [`getlabels`](#getlabels) | Get the labels for the given addresses, txids and outpoints |
# Reference
@ -92,6 +94,7 @@ This command does not take any parameter for now.
| Field | Type | Description |
| -------------- | ------------- | ------------------------------------------------------------------------------------------------------------------ |
| `address` | string | Address containing the script pubkey of the coin |
| `amount` | int | Value of the TxO in satoshis. |
| `outpoint` | string | Transaction id and output index of this coin. |
| `block_height` | int or null | Block height the transaction was confirmed at, or `null`. |
@ -300,3 +303,31 @@ cover the requested feerate.
| Field | Type | Description |
| -------------- | --------- | ---------------------------------------------------- |
| `psbt` | string | PSBT of the recovery transaction, encoded as base64. |
### `updatelabels`
Update the labels from a given map of key/value, with the labelled bitcoin addresses, txids and outpoints as keys
and the label as value. If a label already exist for the given item, the new label overrides the previous one.
#### Request
| Field | Type | Description |
| -------- | ------ | --------------------------------------------------------------------------------------------------------------------- |
| `labels` | object | A mapping from an item to be labelled (an address, a txid or an outpoint) to a label string (at most 100 chars long). |
### `getlabels`
Retrieve a map of items and their respective labels from a list of addresses, txids and outpoints.
Items without labels are not present in the response map.
#### Request
| Field | Type | Description |
| --------| ------------ | -------------------------------------------------------------- |
| `items` | string array | Items (address, txid or outpoint) of which to fetch the label. |
#### Response
| Field | Type | Description |
| -------- | ------ | -------------------------------------------------------------------------------- |
| `labels` | object | A mapping of bitcoin addresses, txids and oupoints as keys, and string as values |

View File

@ -10,13 +10,15 @@ use crate::{
descriptors, DaemonControl, VERSION,
};
pub use crate::database::LabelItem;
use utils::{
deser_addr_assume_checked, deser_amount_from_sats, deser_fromstr, deser_hex, ser_amount,
ser_hex, ser_to_string,
};
use std::{
collections::{hash_map, BTreeMap, HashMap},
collections::{hash_map, BTreeMap, HashMap, HashSet},
convert::TryInto,
fmt,
};
@ -308,7 +310,11 @@ impl DaemonControl {
height: spend_block.map(|b| b.height),
});
let block_height = block_info.map(|b| b.height);
let address = self
.derived_desc(&coin)
.address(self.config.bitcoin_config.network);
ListCoinsEntry {
address,
amount,
outpoint,
block_height,
@ -575,6 +581,18 @@ impl DaemonControl {
Ok(())
}
pub fn update_labels(&self, items: &HashMap<LabelItem, String>) {
let mut db_conn = self.db.connection();
db_conn.update_labels(items);
}
pub fn get_labels(&self, items: &HashSet<LabelItem>) -> GetLabelsResult {
let mut db_conn = self.db.connection();
GetLabelsResult {
labels: db_conn.labels(items),
}
}
pub fn list_spend(&self) -> ListSpendResult {
let mut db_conn = self.db.connection();
let spend_txs = db_conn
@ -820,6 +838,11 @@ pub struct GetAddressResult {
address: bitcoin::Address,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetLabelsResult {
pub labels: HashMap<String, String>,
}
impl GetAddressResult {
pub fn new(address: bitcoin::Address) -> Self {
Self { address }
@ -837,7 +860,7 @@ pub struct LCSpendInfo {
pub height: Option<i32>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListCoinsEntry {
#[serde(
serialize_with = "ser_amount",
@ -845,6 +868,11 @@ pub struct ListCoinsEntry {
)]
pub amount: bitcoin::Amount,
pub outpoint: bitcoin::OutPoint,
#[serde(
serialize_with = "ser_to_string",
deserialize_with = "deser_addr_assume_checked"
)]
pub address: bitcoin::Address,
pub block_height: Option<i32>,
/// Information about the transaction spending this coin.
pub spend_info: Option<LCSpendInfo>,

View File

@ -12,7 +12,13 @@ use crate::{
},
};
use std::{collections::HashMap, sync};
use std::{
collections::{HashMap, HashSet},
fmt::Display,
iter::FromIterator,
str::FromStr,
sync,
};
use miniscript::bitcoin::{self, bip32, psbt::PartiallySignedTransaction as Psbt, secp256k1};
@ -119,6 +125,10 @@ pub trait DatabaseConnection {
/// Delete a Spend transaction from database.
fn delete_spend(&mut self, txid: &bitcoin::Txid);
fn update_labels(&mut self, items: &HashMap<LabelItem, String>);
fn labels(&mut self, labels: &HashSet<LabelItem>) -> HashMap<String, String>;
/// Mark the given tip as the new best seen block. Update stored data accordingly.
fn rollback_tip(&mut self, new_tip: &BlockChainTip);
@ -257,6 +267,15 @@ impl DatabaseConnection for SqliteConn {
self.delete_spend(txid)
}
fn update_labels(&mut self, items: &HashMap<LabelItem, String>) {
self.update_labels(items)
}
fn labels(&mut self, items: &HashSet<LabelItem>) -> HashMap<String, String> {
let labels = self.db_labels(items);
HashMap::from_iter(labels.into_iter().map(|label| (label.item, label.value)))
}
fn rollback_tip(&mut self, new_tip: &BlockChainTip) {
self.rollback_tip(new_tip)
}
@ -335,3 +354,56 @@ pub enum CoinType {
Unspent,
Spent,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum LabelItem {
Address(bitcoin::Address),
Txid(bitcoin::Txid),
OutPoint(bitcoin::OutPoint),
}
impl From<bitcoin::Address> for LabelItem {
fn from(value: bitcoin::Address) -> Self {
Self::Address(value)
}
}
impl From<bitcoin::Txid> for LabelItem {
fn from(value: bitcoin::Txid) -> Self {
Self::Txid(value)
}
}
impl From<bitcoin::OutPoint> for LabelItem {
fn from(value: bitcoin::OutPoint) -> Self {
Self::OutPoint(value)
}
}
impl Display for LabelItem {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
LabelItem::Address(a) => write!(f, "{}", a),
LabelItem::Txid(a) => write!(f, "{}", a),
LabelItem::OutPoint(a) => write!(f, "{}", a),
}
}
}
impl LabelItem {
pub fn from_str(s: &str, network: bitcoin::Network) -> Option<LabelItem> {
if let Ok(addr) = bitcoin::Address::from_str(s) {
if !addr.is_valid_for_network(network) {
None
} else {
Some(LabelItem::Address(addr.assume_checked()))
}
} else if let Ok(txid) = bitcoin::Txid::from_str(s) {
Some(LabelItem::Txid(txid))
} else if let Ok(outpoint) = bitcoin::OutPoint::from_str(s) {
Some(LabelItem::OutPoint(outpoint))
} else {
None
}
}
}

View File

@ -14,18 +14,26 @@ use crate::{
bitcoin::BlockChainTip,
database::{
sqlite::{
schema::{DbAddress, DbCoin, DbSpendTransaction, DbTip, DbWallet, SCHEMA},
schema::{
DbAddress, DbCoin, DbLabel, DbLabelledKind, DbSpendTransaction, DbTip, DbWallet,
SCHEMA,
},
utils::{
create_fresh_db, curr_timestamp, db_exec, db_query, db_tx_query, db_version,
maybe_apply_migration, LOOK_AHEAD_LIMIT,
},
},
Coin, CoinType,
Coin, CoinType, LabelItem,
},
descriptors::LianaDescriptor,
};
use std::{cmp, convert::TryInto, fmt, io, path};
use std::{
cmp,
collections::{HashMap, HashSet},
convert::TryInto,
fmt, io, path,
};
use miniscript::bitcoin::{
self, bip32,
@ -35,7 +43,7 @@ use miniscript::bitcoin::{
secp256k1,
};
const DB_VERSION: i64 = 2;
const DB_VERSION: i64 = 3;
#[derive(Debug)]
pub enum SqliteDbError {
@ -554,6 +562,43 @@ impl SqliteConn {
.expect("Db must not fail")
}
pub fn update_labels(&mut self, items: &HashMap<LabelItem, String>) {
db_exec(&mut self.conn, |db_tx| {
for (labelled, kind, value) in items
.iter()
.map(|(a, v)| {
match a {
LabelItem::Address(a) =>(a.to_string(), DbLabelledKind::Address, v),
LabelItem::Txid(a) =>(a.to_string(), DbLabelledKind::Txid, v),
LabelItem::OutPoint(a) =>(a.to_string(), DbLabelledKind::OutPoint, v),
}
}) {
db_tx.execute(
"INSERT INTO labels (wallet_id, item, item_kind, value) VALUES (?1, ?2, ?3, ?4) \
ON CONFLICT DO UPDATE SET value=excluded.value",
rusqlite::params![WALLET_ID, labelled, kind as i64, value],
)?;
}
Ok(())
})
.expect("Db must not fail")
}
pub fn db_labels(&mut self, items: &HashSet<LabelItem>) -> Vec<DbLabel> {
let query = format!(
"SELECT * FROM labels where item in ({})",
items
.iter()
.map(|a| format!("'{}'", a))
.collect::<Vec<String>>()
.join(",")
);
db_query(&mut self.conn, &query, rusqlite::params![], |row| {
row.try_into()
})
.expect("Db must not fail")
}
/// Retrieves a limited and ordered list of transactions ids that happened during the given
/// range.
pub fn db_list_txids(&mut self, start: u32, end: u32, limit: u64) -> Vec<bitcoin::Txid> {
@ -812,6 +857,38 @@ CREATE TABLE spend_transactions (
fs::remove_dir_all(tmp_dir).unwrap();
}
#[test]
fn db_labels_update() {
let (tmp_dir, _, _, db) = dummy_db();
{
let txid_str = "0c62a990d20d54429e70859292e82374ba6b1b951a3ab60f26bb65fee5724ff7";
let txid = LabelItem::from_str(txid_str, bitcoin::Network::Bitcoin).unwrap();
let mut items = HashSet::new();
items.insert(txid.clone());
let mut conn = db.connection().unwrap();
let db_labels = conn.db_labels(&items);
assert!(db_labels.is_empty());
let mut txids_labels = HashMap::new();
txids_labels.insert(txid.clone(), "hello".to_string());
conn.update_labels(&txids_labels);
let db_labels = conn.db_labels(&items);
assert_eq!(db_labels[0].value, "hello");
txids_labels.insert(txid, "hello again".to_string());
conn.update_labels(&txids_labels);
let db_labels = conn.db_labels(&items);
assert_eq!(db_labels[0].value, "hello again");
}
fs::remove_dir_all(tmp_dir).unwrap();
}
#[test]
fn db_coins_update() {
let (tmp_dir, _, _, db) = dummy_db();
@ -1639,4 +1716,42 @@ CREATE TABLE spend_transactions (
fs::remove_dir_all(tmp_dir).unwrap();
}
#[test]
fn v0_to_v3_migration() {
let secp = secp256k1::Secp256k1::verification_only();
// Create a database with version 0, using the old schema.
let tmp_dir = tmp_dir();
fs::create_dir_all(&tmp_dir).unwrap();
let db_path: path::PathBuf = [tmp_dir.as_path(), path::Path::new("lianad_v0.sqlite3")]
.iter()
.collect();
let mut options = dummy_options();
options.schema = V0_SCHEMA;
options.version = 0;
create_fresh_db(&db_path, options, &secp).unwrap();
// SqliteDb new is doing the migration.
let db = SqliteDb::new(db_path, None, &secp).unwrap();
{
let mut conn = db.connection().unwrap();
let version = conn.db_version();
assert_eq!(version, 3);
let txid_str = "0c62a990d20d54429e70859292e82374ba6b1b951a3ab60f26bb65fee5724ff7";
let txid = LabelItem::from_str(txid_str, bitcoin::Network::Bitcoin).unwrap();
let mut txids_labels = HashMap::new();
txids_labels.insert(txid.clone(), "hello".to_string());
conn.update_labels(&txids_labels);
let mut items = HashSet::new();
items.insert(txid);
let db_labels = conn.db_labels(&items);
assert_eq!(db_labels[0].value, "hello");
}
fs::remove_dir_all(tmp_dir).unwrap();
}
}

View File

@ -81,6 +81,15 @@ CREATE TABLE spend_transactions (
txid BLOB UNIQUE NOT NULL,
updated_at INTEGER
);
/* Labels applied on addresses (0), outpoints (1), txids (2) */
CREATE TABLE labels (
id INTEGER PRIMARY KEY NOT NULL,
wallet_id INTEGER NOT NULL,
item_kind INTEGER NOT NULL CHECK (item_kind IN (0,1,2)),
item TEXT UNIQUE NOT NULL,
value TEXT NOT NULL
);
";
/// A row in the "tip" table.
@ -293,3 +302,54 @@ impl TryFrom<&rusqlite::Row<'_>> for DbSpendTransaction {
})
}
}
/// A row in the "labels" table
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DbLabel {
pub id: i64,
pub wallet_id: i64,
pub item_kind: DbLabelledKind,
pub item: String,
pub value: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(i64)]
pub enum DbLabelledKind {
Address = 0,
OutPoint = 1,
Txid = 2,
}
impl From<i64> for DbLabelledKind {
fn from(value: i64) -> Self {
if value == 0 {
Self::Address
} else if value == 1 {
Self::OutPoint
} else {
assert_eq!(value, 2);
Self::Txid
}
}
}
impl TryFrom<&rusqlite::Row<'_>> for DbLabel {
type Error = rusqlite::Error;
fn try_from(row: &rusqlite::Row) -> Result<Self, Self::Error> {
let id: i64 = row.get(0)?;
let wallet_id: i64 = row.get(1)?;
let item_kind: i64 = row.get(2)?;
let item: String = row.get(3)?;
let value: String = row.get(4)?;
Ok(DbLabel {
id,
wallet_id,
item_kind: item_kind.into(),
item,
value,
})
}
}

View File

@ -180,6 +180,20 @@ fn migrate_v1_to_v2(conn: &mut rusqlite::Connection) -> Result<(), SqliteDbError
Ok(())
}
// After Liana 1.1 we upgraded the schema to add the labels table.
fn migrate_v2_to_v3(conn: &mut rusqlite::Connection) -> Result<(), SqliteDbError> {
db_exec(conn, |tx| {
tx.execute(
"CREATE TABLE labels (id INTEGER PRIMARY KEY NOT NULL, wallet_id INTEGER NOT NULL, item_kind INTEGER NOT NULL CHECK (item_kind IN (0,1,2)), item TEXT UNIQUE NOT NULL, value TEXT NOT NULL)",
rusqlite::params![],
)?;
tx.execute("UPDATE version SET version = 3", rusqlite::params![])?;
Ok(())
})?;
Ok(())
}
/// Check the database version and if necessary apply the migrations to upgrade it to the current
/// one.
pub fn maybe_apply_migration(db_path: &path::Path) -> Result<(), SqliteDbError> {
@ -203,6 +217,11 @@ pub fn maybe_apply_migration(db_path: &path::Path) -> Result<(), SqliteDbError>
migrate_v1_to_v2(&mut conn)?;
log::warn!("Migration from database version 1 to version 2 successful.");
}
2 => {
log::warn!("Upgrading database from version 2 to version 3.");
migrate_v2_to_v3(&mut conn)?;
log::warn!("Migration from database version 2 to version 3 successful.");
}
_ => return Err(SqliteDbError::UnsupportedVersion(version)),
}
}

View File

@ -1,9 +1,14 @@
use crate::{
commands::LabelItem,
jsonrpc::{Error, Params, Request, Response},
DaemonControl,
};
use std::{collections::HashMap, convert::TryInto, str::FromStr};
use std::{
collections::{HashMap, HashSet},
convert::TryInto,
str::FromStr,
};
use miniscript::bitcoin::{self, psbt::PartiallySignedTransaction as Psbt};
@ -160,6 +165,68 @@ fn create_recovery(control: &DaemonControl, params: Params) -> Result<serde_json
Ok(serde_json::json!(&res))
}
fn update_labels(control: &DaemonControl, params: Params) -> Result<serde_json::Value, Error> {
let mut items = HashMap::new();
for (item, value) in params
.get(0, "labels")
.ok_or_else(|| Error::invalid_params("Missing 'labels' parameter."))?
.as_object()
.ok_or_else(|| Error::invalid_params("Invalid 'labels' parameter."))?
.iter()
{
let value = value
.as_str()
.map(|s| s.to_string())
.ok_or_else(|| Error::invalid_params(format!("Invalid 'labels.{}' value.", item)))?;
if value.len() > 100 {
return Err(Error::invalid_params(format!(
"Invalid 'labels.{}' value length: must be less or equal than 100 characters",
item
)));
}
let item =
LabelItem::from_str(item, control.config.bitcoin_config.network).ok_or_else(|| {
Error::invalid_params(format!(
"Invalid 'labels.{}' parameter: must be an address, a txid or an outpoint",
item
))
})?;
items.insert(item, value);
}
control.update_labels(&items);
Ok(serde_json::json!({}))
}
fn get_labels(control: &DaemonControl, params: Params) -> Result<serde_json::Value, Error> {
let mut items = HashSet::new();
for item in params
.get(0, "items")
.ok_or_else(|| Error::invalid_params("Missing 'items' parameter."))?
.as_array()
.ok_or_else(|| Error::invalid_params("Invalid 'items' parameter."))?
.iter()
{
let item = item.as_str().ok_or_else(|| {
Error::invalid_params(format!(
"Invalid item {} format: must be an address, a txid or an outpoint",
item
))
})?;
let item =
LabelItem::from_str(item, control.config.bitcoin_config.network).ok_or_else(|| {
Error::invalid_params(format!(
"Invalid item {} format: must be an address, a txid or an outpoint",
item
))
})?;
items.insert(item);
}
Ok(serde_json::json!(control.get_labels(&items)))
}
/// Handle an incoming JSONRPC2 request.
pub fn handle_request(control: &DaemonControl, req: Request) -> Result<Response, Error> {
let result = match req.method.as_str() {
@ -222,6 +289,18 @@ pub fn handle_request(control: &DaemonControl, req: Request) -> Result<Response,
.ok_or_else(|| Error::invalid_params("Missing 'psbt' parameter."))?;
update_spend(control, params)?
}
"updatelabels" => {
let params = req
.params
.ok_or_else(|| Error::invalid_params("Missing 'labels' parameter."))?;
update_labels(control, params)?
}
"getlabels" => {
let params = req
.params
.ok_or_else(|| Error::invalid_params("Missing 'items' parameter."))?;
get_labels(control, params)?
}
_ => {
return Err(Error::method_not_found());
}

View File

@ -1,11 +1,16 @@
use crate::{
bitcoin::{BitcoinInterface, Block, BlockChainTip, UTxO},
config::{BitcoinConfig, Config},
database::{BlockInfo, Coin, CoinType, DatabaseConnection, DatabaseInterface},
database::{BlockInfo, Coin, CoinType, DatabaseConnection, DatabaseInterface, LabelItem},
descriptors, DaemonHandle,
};
use std::{collections::HashMap, env, fs, io, path, process, str::FromStr, sync, thread, time};
use std::{
collections::{HashMap, HashSet},
env, fs, io, path, process,
str::FromStr,
sync, thread, time,
};
use miniscript::{
bitcoin::{
@ -334,6 +339,14 @@ impl DatabaseConnection for DummyDatabase {
todo!()
}
fn update_labels(&mut self, _items: &HashMap<LabelItem, String>) {
todo!()
}
fn labels(&mut self, _items: &HashSet<LabelItem>) -> HashMap<String, String> {
todo!()
}
fn list_txids(&mut self, start: u32, end: u32, limit: u64) -> Vec<bitcoin::Txid> {
let mut txids_and_time = Vec::new();
let coins = &self.db.read().unwrap().coins;

View File

@ -596,3 +596,116 @@ def test_create_recovery(lianad, bitcoind):
assert len(reco_psbt.tx.vout) == 1
assert int(0.39999 * COIN) < int(reco_psbt.tx.vout[0].nValue) < int(0.4 * COIN)
sign_and_broadcast(lianad, bitcoind, reco_psbt, recovery=True)
def test_labels(lianad, bitcoind):
"""Test the creation and updating of labels."""
# We can set a label for an address.
addr = lianad.rpc.getnewaddress()["address"]
lianad.rpc.updatelabels({addr: "first-addr"})
assert lianad.rpc.getlabels([addr])["labels"] == {addr: "first-addr"}
# And also update it.
lianad.rpc.updatelabels({addr: "first-addr-1"})
assert lianad.rpc.getlabels([addr])["labels"] == {addr: "first-addr-1"}
# But we can't set a label larger than 100 characters
with pytest.raises(RpcError, match=".*must be less or equal than 100 characters"):
lianad.rpc.updatelabels({addr: "".join("a" for _ in range(101))})
# We can set a label for a coin.
sec_addr = lianad.rpc.getnewaddress()["address"]
txid = bitcoind.rpc.sendtoaddress(sec_addr, 1)
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 1)
coin = lianad.rpc.listcoins()["coins"][0]
lianad.rpc.updatelabels({coin["outpoint"]: "first-coin"})
assert lianad.rpc.getlabels([coin["outpoint"]])["labels"] == {
coin["outpoint"]: "first-coin"
}
# And also update it.
lianad.rpc.updatelabels({coin["outpoint"]: "first-coin-1"})
assert lianad.rpc.getlabels([coin["outpoint"]])["labels"] == {
coin["outpoint"]: "first-coin-1"
}
# Its address though has no label.
assert lianad.rpc.getlabels([sec_addr])["labels"] == {}
# But we can receive a coin to the address that has a label set, and query both.
sec_txid = bitcoind.rpc.sendtoaddress(addr, 1)
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 2)
sec_coin = next(
c for c in lianad.rpc.listcoins()["coins"] if sec_txid in c["outpoint"]
)
lianad.rpc.updatelabels({sec_coin["outpoint"]: "sec-coin"})
res = lianad.rpc.getlabels([sec_coin["outpoint"], addr])["labels"]
assert len(res) == 2
assert res[sec_coin["outpoint"]] == "sec-coin"
assert res[addr] == "first-addr-1"
# We can also query the labels for both coins, of course.
res = lianad.rpc.getlabels([coin["outpoint"], sec_coin["outpoint"]])["labels"]
assert len(res) == 2
assert res[coin["outpoint"]] == "first-coin-1"
assert res[sec_coin["outpoint"]] == "sec-coin"
# We can set, update and query labels for deposit transactions.
lianad.rpc.updatelabels({txid: "first-deposit"})
assert lianad.rpc.getlabels([txid, sec_txid])["labels"] == {txid: "first-deposit"}
lianad.rpc.updatelabels({txid: "first-deposit-1", sec_txid: "second-deposit"})
res = lianad.rpc.getlabels([txid, sec_txid])["labels"]
assert len(res) == 2
assert res[txid] == "first-deposit-1"
assert res[sec_txid] == "second-deposit"
# We can set and update a label for a spend transaction.
spend_txid = get_txid(spend_coins(lianad, bitcoind, [coin, sec_coin]))
lianad.rpc.updatelabels({spend_txid: "spend-tx"})
assert lianad.rpc.getlabels([spend_txid])["labels"] == {spend_txid: "spend-tx"}
lianad.rpc.updatelabels({spend_txid: "spend-tx-1"})
assert lianad.rpc.getlabels([spend_txid])["labels"] == {spend_txid: "spend-tx-1"}
# We can set labels for inexistent stuff, as long as the format of the item being
# labelled is valid.
inexistent_txid = "".join("0" for _ in range(64))
inexistent_outpoint = "".join("1" for _ in range(64)) + ":42"
random_address = bitcoind.rpc.getnewaddress()
lianad.rpc.updatelabels(
{
inexistent_txid: "inex_txid",
inexistent_outpoint: "inex_outpoint",
random_address: "bitcoind-addr",
}
)
res = lianad.rpc.getlabels([inexistent_txid, inexistent_outpoint, random_address])[
"labels"
]
assert len(res) == 3
assert res[inexistent_txid] == "inex_txid"
assert res[inexistent_outpoint] == "inex_outpoint"
assert res[random_address] == "bitcoind-addr"
# We'll confirm everything, shouldn't affect any of the labels.
bitcoind.generate_block(1, wait_for_mempool=spend_txid)
wait_for(
lambda: bitcoind.rpc.getblockcount() == lianad.rpc.getinfo()["block_height"]
)
res = lianad.rpc.getlabels(
[
addr,
sec_addr, # No label for this one.
txid,
sec_txid,
coin["outpoint"],
sec_coin["outpoint"],
spend_txid,
inexistent_txid,
inexistent_outpoint,
random_address,
]
)["labels"]
assert len(res) == 9
assert res[sec_coin["outpoint"]] == "sec-coin"
assert res[addr] == "first-addr-1"
assert res[coin["outpoint"]] == "first-coin-1"
assert res[txid] == "first-deposit-1"
assert res[sec_txid] == "second-deposit"
assert res[spend_txid] == "spend-tx-1"
assert res[inexistent_txid] == "inex_txid"
assert res[inexistent_outpoint] == "inex_outpoint"
assert res[random_address] == "bitcoind-addr"