Skip to content

Commit

Permalink
wip: add labels to lianad
Browse files Browse the repository at this point in the history
  • Loading branch information
edouardparis committed Aug 11, 2023
1 parent 39d576f commit d2fa3ed
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 4 deletions.
14 changes: 14 additions & 0 deletions doc/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ 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 |

# Reference

Expand Down Expand Up @@ -181,6 +182,7 @@ This command does not take any parameter for now.
| -------------- | ----------------- | ----------------------------------------------------------------------- |
| `psbt` | string | Base64-encoded PSBT of the Spend transaction. |
| `updated_at` | int or null | UNIX timestamp of the last time this PSBT was updated. |
| `labels` | object | Map of addresses, txid, outpoints as key and label string as value |


### `delspendtx`
Expand Down Expand Up @@ -254,6 +256,7 @@ Confirmation time is based on the timestamp of blocks.
| `height` | int or `null` | Block height of the transaction, `null` if the transaction is unconfirmed |
| `time` | int or `null` | Block time of the transaction, `null` if the transaction is unconfirmed |
| `tx` | string | hex encoded bitcoin transaction |
| `labels` | object | Map of addresses, txid, outpoints as key and label string as value |

### `listtransactions`

Expand Down Expand Up @@ -300,3 +303,14 @@ 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 target, the new label override the previous one.

#### Request

| Field | Type | Description |
| -------- | ------ | ------------------------------------------------------------------------------- |
| `labels` | object | Map with bitcoin addresses, txids or oupoints as keys, and 100 max string value |
10 changes: 10 additions & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,16 @@ impl DaemonControl {
Ok(())
}

pub fn update_labels(
&self,
addresses: &HashMap<bitcoin::Address, String>,
txids: &HashMap<bitcoin::Txid, String>,
outpoints: &HashMap<bitcoin::OutPoint, String>,
) {
let mut db_conn = self.db.connection();
db_conn.update_labels(addresses, txids, outpoints);
}

pub fn list_spend(&self) -> ListSpendResult {
let mut db_conn = self.db.connection();
let spend_txs = db_conn
Expand Down
43 changes: 42 additions & 1 deletion src/database/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ use crate::{
},
};

use std::{collections::HashMap, sync};
use std::{
collections::{HashMap, HashSet},
iter::FromIterator,
sync,
};

use miniscript::bitcoin::{self, bip32, psbt::PartiallySignedTransaction as Psbt, secp256k1};

Expand Down Expand Up @@ -119,6 +123,20 @@ pub trait DatabaseConnection {
/// Delete a Spend transaction from database.
fn delete_spend(&mut self, txid: &bitcoin::Txid);

fn update_labels(
&mut self,
addresses: &HashMap<bitcoin::Address, String>,
txids: &HashMap<bitcoin::Txid, String>,
outpoints: &HashMap<bitcoin::OutPoint, String>,
);

fn labels(
&mut self,
addresses: &HashSet<&bitcoin::Address>,
txids: &HashSet<&bitcoin::Txid>,
outpoints: &HashSet<&bitcoin::OutPoint>,
) -> 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);

Expand Down Expand Up @@ -257,6 +275,29 @@ impl DatabaseConnection for SqliteConn {
self.delete_spend(txid)
}

fn update_labels(
&mut self,
addresses: &HashMap<bitcoin::Address, String>,
txids: &HashMap<bitcoin::Txid, String>,
outpoints: &HashMap<bitcoin::OutPoint, String>,
) {
self.update_labels(addresses, txids, outpoints)
}

fn labels(
&mut self,
addresses: &HashSet<&bitcoin::Address>,
txids: &HashSet<&bitcoin::Txid>,
outpoints: &HashSet<&bitcoin::OutPoint>,
) -> HashMap<String, String> {
let labels = self.db_labels(addresses, txids, outpoints);
HashMap::from_iter(
labels
.into_iter()
.map(|label| (label.labelled, label.value)),
)
}

fn rollback_tip(&mut self, new_tip: &BlockChainTip) {
self.rollback_tip(new_tip)
}
Expand Down
77 changes: 75 additions & 2 deletions src/database/sqlite/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ 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,
Expand All @@ -25,7 +28,12 @@ use crate::{
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,
Expand Down Expand Up @@ -554,6 +562,71 @@ impl SqliteConn {
.expect("Db must not fail")
}

pub fn update_labels(
&mut self,
addresses: &HashMap<bitcoin::Address, String>,
txids: &HashMap<bitcoin::Txid, String>,
outpoints: &HashMap<bitcoin::OutPoint, String>,
) {
let wallet_id: i64 = db_query(
&mut self.conn,
"SELECT id FROM wallets",
rusqlite::params![],
|row| row.get(0),
)
.expect("Db must not fail")
.pop()
.expect("There is always a wallet");
db_exec(&mut self.conn, |db_tx| {
for (labelled, kind, value) in addresses
.iter()
.map(|(a, v)| (a.to_string(), DbLabelledKind::Address, v))
.chain(
txids
.into_iter()
.map(|(t, v)| (t.to_string(), DbLabelledKind::Txid, v)),
)
.chain(
outpoints
.into_iter()
.map(|(o, v)| (o.to_string(), DbLabelledKind::OutPoint, v)),
)
{
db_tx.execute(
"INSERT INTO labels (wallet_id, labelled, labelled_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,
addresses: &HashSet<&bitcoin::Address>,
txids: &HashSet<&bitcoin::Txid>,
outpoints: &HashSet<&bitcoin::OutPoint>,
) -> Vec<DbLabel> {
db_query(
&mut self.conn,
&format!(
"SELECT * FROM labels where labelled in ({})",
addresses
.iter()
.map(|a| a.to_string())
.chain(txids.iter().map(|a| a.to_string()))
.chain(outpoints.iter().map(|a| a.to_string()))
.collect::<Vec<String>>()
.join(",")
),
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> {
Expand Down
59 changes: 59 additions & 0 deletions src/database/sqlite/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
labelled_kind INTEGER NOT NULL CHECK (labelled_kind IN (0,1,2)),
labelled TEXT UNIQUE NOT NULL,
value TEXT NOT NULL
);
";

/// A row in the "tip" table.
Expand Down Expand Up @@ -293,3 +302,53 @@ 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 labelled_kind: DbLabelledKind,
pub labelled: 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 {
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 labelled_kind: i64 = row.get(2)?;
let labelled: String = row.get(3)?;
let value: String = row.get(4)?;

Ok(DbLabel {
id,
wallet_id,
labelled_kind: labelled_kind.into(),
labelled,
value,
})
}
}
46 changes: 46 additions & 0 deletions src/jsonrpc/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,46 @@ 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 addresses = HashMap::new();
let mut txids = HashMap::new();
let mut outpoints = HashMap::new();
for (labelled, 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.", labelled))
})?;
if value.len() >= 100 {
return Err(Error::invalid_params(format!(
"Invalid 'labels.{}' value length: must be less or equal than 100 characters",
labelled
)));
}
if let Ok(addr) = bitcoin::Address::from_str(&labelled) {
let addr = addr.assume_checked();
addresses.insert(addr, value);
} else if let Ok(txid) = bitcoin::Txid::from_str(&labelled) {
txids.insert(txid, value);
} else {
let outpoint = bitcoin::OutPoint::from_str(&labelled).map_err(|_| {
Error::invalid_params(format!(
"Invalid 'labels.{}' parameter: must be an address, a txid or an outpoint",
labelled
))
})?;
outpoints.insert(outpoint, value);
}
}

control.update_labels(&addresses, &txids, &outpoints);
Ok(serde_json::json!({}))
}

/// Handle an incoming JSONRPC2 request.
pub fn handle_request(control: &DaemonControl, req: Request) -> Result<Response, Error> {
let result = match req.method.as_str() {
Expand Down Expand Up @@ -222,6 +262,12 @@ 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)?
}
_ => {
return Err(Error::method_not_found());
}
Expand Down
19 changes: 18 additions & 1 deletion src/testutils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
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::{
Expand Down Expand Up @@ -334,6 +334,23 @@ impl DatabaseConnection for DummyDatabase {
todo!()
}

fn update_labels(
&mut self,
_addresses: &HashMap<bitcoin::Address, String>,
_txids: &HashMap<bitcoin::Txid, String>,
_outpoints: &HashMap<bitcoin::OutPoint, String>,
) {
todo!()
}

fn labels(&mut self,
_addresses: &HashSet<&bitcoin::Address>,
_txids: &HashSet<&bitcoin::Txid>,
_outpoints: &HashSet<&bitcoin::OutPoint>,
) {
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;
Expand Down

0 comments on commit d2fa3ed

Please sign in to comment.