This allows us to get several values relating to the wallet in one call. The field names of the `Wallet` struct have been chosen to match the corresponding DB interface methods. Some sqlite methods will call `wallet()` instead of `db_wallet()` to ensure consistency in output.
575 lines
17 KiB
Rust
575 lines
17 KiB
Rust
//! Database interface for Liana.
|
|
//!
|
|
//! Record wallet metadata, spent and unspent coins, ongoing transactions.
|
|
|
|
pub mod sqlite;
|
|
|
|
use crate::{
|
|
bitcoin::BlockChainTip,
|
|
database::sqlite::{
|
|
schema::{DbBlockInfo, DbCoin, DbTip},
|
|
SqliteConn, SqliteDb,
|
|
},
|
|
};
|
|
|
|
use std::{
|
|
collections::{HashMap, HashSet},
|
|
fmt::Display,
|
|
iter::FromIterator,
|
|
str::FromStr,
|
|
sync,
|
|
};
|
|
|
|
use miniscript::bitcoin::{self, bip32, psbt::Psbt, secp256k1};
|
|
|
|
/// Information about the wallet.
|
|
///
|
|
/// All timestamps are the number of seconds since the UNIX epoch.
|
|
#[derive(Clone, Debug)]
|
|
pub struct Wallet {
|
|
/// Timestamp at wallet creation time.
|
|
pub timestamp: u32,
|
|
/// Derivation index for the next receiving address.
|
|
pub receive_index: bip32::ChildNumber,
|
|
/// Derivation index for the next change address.
|
|
pub change_index: bip32::ChildNumber,
|
|
/// Timestamp to start rescanning from, if any.
|
|
pub rescan_timestamp: Option<u32>,
|
|
/// Timestamp at which the last poll of the blockchain completed, if any,
|
|
pub last_poll_timestamp: Option<u32>,
|
|
}
|
|
|
|
pub trait DatabaseInterface: Send {
|
|
fn connection(&self) -> Box<dyn DatabaseConnection>;
|
|
}
|
|
|
|
impl DatabaseInterface for SqliteDb {
|
|
fn connection(&self) -> Box<dyn DatabaseConnection> {
|
|
Box::new(self.connection().expect("Database must be available"))
|
|
}
|
|
}
|
|
|
|
// FIXME: do we need to repeat the entire trait implemenation? Isn't there a nicer way?
|
|
impl DatabaseInterface for sync::Arc<sync::Mutex<dyn DatabaseInterface>> {
|
|
fn connection(&self) -> Box<dyn DatabaseConnection> {
|
|
self.lock().unwrap().connection()
|
|
}
|
|
}
|
|
|
|
pub trait DatabaseConnection {
|
|
/// Get the tip of the best chain we've seen.
|
|
fn chain_tip(&mut self) -> Option<BlockChainTip>;
|
|
|
|
/// The network we are operating on.
|
|
fn network(&mut self) -> bitcoin::Network;
|
|
|
|
/// Get the `Wallet`.
|
|
fn wallet(&mut self) -> Wallet;
|
|
|
|
/// The timestamp at wallet creation time
|
|
fn timestamp(&mut self) -> u32;
|
|
|
|
/// Update our best chain seen.
|
|
fn update_tip(&mut self, tip: &BlockChainTip);
|
|
|
|
/// Get the derivation index for the next receiving address
|
|
fn receive_index(&mut self) -> bip32::ChildNumber;
|
|
|
|
/// Set the derivation index for the next receiving address
|
|
fn set_receive_index(
|
|
&mut self,
|
|
index: bip32::ChildNumber,
|
|
secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
|
|
);
|
|
|
|
/// Get the derivation index for the next change address
|
|
fn change_index(&mut self) -> bip32::ChildNumber;
|
|
|
|
/// Set the derivation index for the next change address
|
|
fn set_change_index(
|
|
&mut self,
|
|
index: bip32::ChildNumber,
|
|
secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
|
|
);
|
|
|
|
/// Get the timestamp at which to start rescaning from, if any.
|
|
fn rescan_timestamp(&mut self) -> Option<u32>;
|
|
|
|
/// Set a timestamp at which to start rescaning the block chain from.
|
|
fn set_rescan(&mut self, timestamp: u32);
|
|
|
|
/// Mark the rescan as complete.
|
|
fn complete_rescan(&mut self);
|
|
|
|
/// Get the timestamp at which the last poll of the blockchain completed, if any,
|
|
/// as the number of seconds since the UNIX epoch.
|
|
fn last_poll_timestamp(&mut self) -> Option<u32>;
|
|
|
|
/// Set the timestamp at which the last poll of the blockchain completed,
|
|
/// where `timestamp` should be given as the number of seconds since the UNIX epoch.
|
|
fn set_last_poll(&mut self, timestamp: u32);
|
|
|
|
/// Get the derivation index for this address, as well as whether this address is change.
|
|
fn derivation_index_by_address(
|
|
&mut self,
|
|
address: &bitcoin::Address,
|
|
) -> Option<(bip32::ChildNumber, bool)>;
|
|
|
|
/// Get all our coins, past or present, spent or not.
|
|
fn coins(
|
|
&mut self,
|
|
statuses: &[CoinStatus],
|
|
outpoints: &[bitcoin::OutPoint],
|
|
) -> HashMap<bitcoin::OutPoint, Coin>;
|
|
|
|
/// List coins that are being spent and whose spending transaction is still unconfirmed.
|
|
fn list_spending_coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin>;
|
|
|
|
/// Store new UTxOs. Coins must not already be in database.
|
|
fn new_unspent_coins(&mut self, coins: &[Coin]);
|
|
|
|
/// Remove some UTxOs from the database.
|
|
fn remove_coins(&mut self, coins: &[bitcoin::OutPoint]);
|
|
|
|
/// Mark a set of coins as being confirmed at a specified height and block time.
|
|
/// NOTE: if the coin comes from an immature coinbase transaction, this will mark it as mature.
|
|
/// Immature coinbase deposits must not be confirmed before they are 100 blocks deep in the
|
|
/// chain.
|
|
fn confirm_coins(&mut self, outpoints: &[(bitcoin::OutPoint, i32, u32)]);
|
|
|
|
/// Mark a set of coins as being spent by a specified txid of a pending transaction.
|
|
fn spend_coins(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)]);
|
|
|
|
/// Mark a set of coins as not being spent anymore.
|
|
fn unspend_coins(&mut self, outpoints: &[bitcoin::OutPoint]);
|
|
|
|
/// Mark a set of coins as spent by a specified txid at a specified block time.
|
|
fn confirm_spend(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid, i32, u32)]);
|
|
|
|
/// Get specific coins from the database.
|
|
fn coins_by_outpoints(
|
|
&mut self,
|
|
outpoints: &[bitcoin::OutPoint],
|
|
) -> HashMap<bitcoin::OutPoint, Coin>;
|
|
|
|
fn spend_tx(&mut self, txid: &bitcoin::Txid) -> Option<Psbt>;
|
|
|
|
/// Insert a new Spend transaction or replace an existing one.
|
|
fn store_spend(&mut self, psbt: &Psbt);
|
|
|
|
/// List all existing Spend transactions, along with an optional last update timestamp.
|
|
fn list_spend(&mut self) -> Vec<(Psbt, Option<u32>)>;
|
|
|
|
/// Delete a Spend transaction from database.
|
|
fn delete_spend(&mut self, txid: &bitcoin::Txid);
|
|
|
|
/// Update, for a set of items (as key), their label (as value). A `None` value deletes the
|
|
/// label.
|
|
fn update_labels(&mut self, items: &HashMap<LabelItem, Option<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);
|
|
|
|
/// Retrieve a limited list of txids that where deposited or spent between the start and end timestamps (inclusive bounds)
|
|
fn list_txids(&mut self, start: u32, end: u32, limit: u64) -> Vec<bitcoin::Txid>;
|
|
|
|
/// Retrieves all txids from the transactions table whether or not they are referenced by a coin.
|
|
fn list_saved_txids(&mut self) -> Vec<bitcoin::Txid>;
|
|
|
|
/// Store transactions in database, ignoring any that already exist.
|
|
fn new_txs(&mut self, txs: &[bitcoin::Transaction]);
|
|
|
|
/// Retrieve a list of transactions and their corresponding block heights and times.
|
|
fn list_wallet_transactions(
|
|
&mut self,
|
|
txids: &[bitcoin::Txid],
|
|
) -> Vec<(bitcoin::Transaction, Option<i32>, Option<u32>)>;
|
|
}
|
|
|
|
impl DatabaseConnection for SqliteConn {
|
|
fn chain_tip(&mut self) -> Option<BlockChainTip> {
|
|
match self.db_tip() {
|
|
DbTip {
|
|
block_height: Some(height),
|
|
block_hash: Some(hash),
|
|
..
|
|
} => Some(BlockChainTip { height, hash }),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn network(&mut self) -> bitcoin::Network {
|
|
self.db_tip().network
|
|
}
|
|
|
|
fn wallet(&mut self) -> Wallet {
|
|
let db_wallet = self.db_wallet();
|
|
Wallet {
|
|
timestamp: db_wallet.timestamp,
|
|
receive_index: db_wallet.deposit_derivation_index,
|
|
change_index: db_wallet.change_derivation_index,
|
|
rescan_timestamp: db_wallet.rescan_timestamp,
|
|
last_poll_timestamp: db_wallet.last_poll_timestamp,
|
|
}
|
|
}
|
|
|
|
fn timestamp(&mut self) -> u32 {
|
|
self.wallet().timestamp
|
|
}
|
|
|
|
fn update_tip(&mut self, tip: &BlockChainTip) {
|
|
self.update_tip(tip)
|
|
}
|
|
|
|
fn receive_index(&mut self) -> bip32::ChildNumber {
|
|
self.wallet().receive_index
|
|
}
|
|
|
|
fn set_receive_index(
|
|
&mut self,
|
|
index: bip32::ChildNumber,
|
|
secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
|
|
) {
|
|
self.set_derivation_index(index, false, secp)
|
|
}
|
|
|
|
fn change_index(&mut self) -> bip32::ChildNumber {
|
|
self.wallet().change_index
|
|
}
|
|
|
|
fn set_change_index(
|
|
&mut self,
|
|
index: bip32::ChildNumber,
|
|
secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
|
|
) {
|
|
self.set_derivation_index(index, true, secp)
|
|
}
|
|
|
|
fn rescan_timestamp(&mut self) -> Option<u32> {
|
|
self.wallet().rescan_timestamp
|
|
}
|
|
|
|
fn set_rescan(&mut self, timestamp: u32) {
|
|
self.set_wallet_rescan_timestamp(timestamp)
|
|
}
|
|
|
|
fn complete_rescan(&mut self) {
|
|
self.complete_wallet_rescan()
|
|
}
|
|
|
|
fn last_poll_timestamp(&mut self) -> Option<u32> {
|
|
self.wallet().last_poll_timestamp
|
|
}
|
|
|
|
fn set_last_poll(&mut self, timestamp: u32) {
|
|
self.set_wallet_last_poll_timestamp(timestamp)
|
|
.expect("database must be available")
|
|
}
|
|
|
|
fn coins(
|
|
&mut self,
|
|
statuses: &[CoinStatus],
|
|
outpoints: &[bitcoin::OutPoint],
|
|
) -> HashMap<bitcoin::OutPoint, Coin> {
|
|
self.coins(statuses, outpoints)
|
|
.into_iter()
|
|
.map(|db_coin| (db_coin.outpoint, db_coin.into()))
|
|
.collect()
|
|
}
|
|
|
|
fn list_spending_coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin> {
|
|
self.list_spending_coins()
|
|
.into_iter()
|
|
.map(|db_coin| (db_coin.outpoint, db_coin.into()))
|
|
.collect()
|
|
}
|
|
|
|
fn new_unspent_coins<'a>(&mut self, coins: &[Coin]) {
|
|
self.new_unspent_coins(coins)
|
|
}
|
|
|
|
fn remove_coins(&mut self, outpoints: &[bitcoin::OutPoint]) {
|
|
self.remove_coins(outpoints)
|
|
}
|
|
|
|
fn confirm_coins<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, i32, u32)]) {
|
|
self.confirm_coins(outpoints)
|
|
}
|
|
|
|
fn spend_coins<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)]) {
|
|
self.spend_coins(outpoints)
|
|
}
|
|
|
|
fn unspend_coins(&mut self, outpoints: &[bitcoin::OutPoint]) {
|
|
self.unspend_coins(outpoints)
|
|
}
|
|
|
|
fn confirm_spend<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid, i32, u32)]) {
|
|
self.confirm_spend(outpoints)
|
|
}
|
|
|
|
fn derivation_index_by_address(
|
|
&mut self,
|
|
address: &bitcoin::Address,
|
|
) -> Option<(bip32::ChildNumber, bool)> {
|
|
self.db_address(address).map(|db_addr| {
|
|
(
|
|
db_addr.derivation_index,
|
|
// We only compare address strings in case `assume_checked()` uses a different network.
|
|
// E.g. An unchecked signet address would have its network set to testnet and so comparing
|
|
// to a signet `Address` would never match.
|
|
address.to_string() == db_addr.change_address.assume_checked().to_string(),
|
|
)
|
|
})
|
|
}
|
|
|
|
fn coins_by_outpoints(
|
|
&mut self,
|
|
outpoints: &[bitcoin::OutPoint],
|
|
) -> HashMap<bitcoin::OutPoint, Coin> {
|
|
self.db_coins(outpoints)
|
|
.into_iter()
|
|
.map(|db_coin| (db_coin.outpoint, db_coin.into()))
|
|
.collect()
|
|
}
|
|
|
|
fn spend_tx(&mut self, txid: &bitcoin::Txid) -> Option<Psbt> {
|
|
self.db_spend(txid).map(|db_spend| db_spend.psbt)
|
|
}
|
|
|
|
fn store_spend(&mut self, psbt: &Psbt) {
|
|
self.store_spend(psbt)
|
|
}
|
|
|
|
fn list_spend(&mut self) -> Vec<(Psbt, Option<u32>)> {
|
|
self.list_spend()
|
|
.into_iter()
|
|
.map(|db_spend| (db_spend.psbt, db_spend.updated_at))
|
|
.collect()
|
|
}
|
|
|
|
fn delete_spend(&mut self, txid: &bitcoin::Txid) {
|
|
self.delete_spend(txid)
|
|
}
|
|
|
|
fn update_labels(&mut self, items: &HashMap<LabelItem, Option<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)
|
|
}
|
|
|
|
fn list_txids(&mut self, start: u32, end: u32, limit: u64) -> Vec<bitcoin::Txid> {
|
|
self.db_list_txids(start, end, limit)
|
|
}
|
|
|
|
fn list_saved_txids(&mut self) -> Vec<bitcoin::Txid> {
|
|
self.db_list_saved_txids()
|
|
}
|
|
|
|
fn new_txs<'a>(&mut self, txs: &[bitcoin::Transaction]) {
|
|
self.new_txs(txs)
|
|
}
|
|
|
|
fn list_wallet_transactions(
|
|
&mut self,
|
|
txids: &[bitcoin::Txid],
|
|
) -> Vec<(bitcoin::Transaction, Option<i32>, Option<u32>)> {
|
|
self.list_wallet_transactions(txids)
|
|
.into_iter()
|
|
.map(|wtx| {
|
|
(
|
|
wtx.transaction,
|
|
wtx.block_info.map(|b| b.height),
|
|
wtx.block_info.map(|b| b.time),
|
|
)
|
|
})
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub struct BlockInfo {
|
|
pub height: i32,
|
|
pub time: u32,
|
|
}
|
|
|
|
impl From<DbBlockInfo> for BlockInfo {
|
|
fn from(b: DbBlockInfo) -> BlockInfo {
|
|
BlockInfo {
|
|
height: b.height,
|
|
time: b.time,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub struct Coin {
|
|
pub outpoint: bitcoin::OutPoint,
|
|
pub is_immature: bool,
|
|
pub block_info: Option<BlockInfo>,
|
|
pub amount: bitcoin::Amount,
|
|
pub derivation_index: bip32::ChildNumber,
|
|
pub is_change: bool,
|
|
pub spend_txid: Option<bitcoin::Txid>,
|
|
pub spend_block: Option<BlockInfo>,
|
|
}
|
|
|
|
impl std::convert::From<DbCoin> for Coin {
|
|
fn from(db_coin: DbCoin) -> Coin {
|
|
let DbCoin {
|
|
outpoint,
|
|
is_immature,
|
|
block_info,
|
|
amount,
|
|
derivation_index,
|
|
is_change,
|
|
spend_txid,
|
|
spend_block,
|
|
..
|
|
} = db_coin;
|
|
Coin {
|
|
outpoint,
|
|
is_immature,
|
|
block_info: block_info.map(BlockInfo::from),
|
|
amount,
|
|
derivation_index,
|
|
is_change,
|
|
spend_txid,
|
|
spend_block: spend_block.map(BlockInfo::from),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Coin {
|
|
pub fn is_confirmed(&self) -> bool {
|
|
self.block_info.is_some()
|
|
}
|
|
|
|
pub fn is_spent(&self) -> bool {
|
|
self.spend_txid.is_some()
|
|
}
|
|
}
|
|
|
|
/// Possible (mutually exclusive) status of a coin.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub enum CoinStatus {
|
|
/// Has not yet been included in a block and has no spend transaction.
|
|
Unconfirmed,
|
|
/// Has been included in a block and has no spend transaction.
|
|
Confirmed,
|
|
/// Has an unconfirmed spend transaction, but coin itself may not yet have been included in a block.
|
|
Spending,
|
|
/// Has a confirmed spend transaction.
|
|
Spent,
|
|
}
|
|
|
|
impl CoinStatus {
|
|
pub fn from_arg(s: &str) -> Option<CoinStatus> {
|
|
match s {
|
|
"unconfirmed" => Some(CoinStatus::Unconfirmed),
|
|
"confirmed" => Some(CoinStatus::Confirmed),
|
|
"spending" => Some(CoinStatus::Spending),
|
|
"spent" => Some(CoinStatus::Spent),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Converts a `CoinStatus` to its equivalent argument name
|
|
/// as used in the `listcoins` RPC command.
|
|
pub fn to_arg(&self) -> &'static str {
|
|
match self {
|
|
CoinStatus::Unconfirmed => "unconfirmed",
|
|
CoinStatus::Confirmed => "confirmed",
|
|
CoinStatus::Spending => "spending",
|
|
CoinStatus::Spent => "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
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn coin_status_as_arg() {
|
|
assert_eq!(
|
|
CoinStatus::from_arg(CoinStatus::Unconfirmed.to_arg()),
|
|
Some(CoinStatus::Unconfirmed)
|
|
);
|
|
assert_eq!(
|
|
CoinStatus::from_arg(CoinStatus::Confirmed.to_arg()),
|
|
Some(CoinStatus::Confirmed)
|
|
);
|
|
assert_eq!(
|
|
CoinStatus::from_arg(CoinStatus::Spending.to_arg()),
|
|
Some(CoinStatus::Spending)
|
|
);
|
|
assert_eq!(
|
|
CoinStatus::from_arg(CoinStatus::Spent.to_arg()),
|
|
Some(CoinStatus::Spent)
|
|
);
|
|
}
|
|
}
|