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:
commit
cf17cc2cd6
31
doc/API.md
31
doc/API.md
@ -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 |
|
||||
|
||||
@ -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>,
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)),
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user