2455 lines
93 KiB
Rust
2455 lines
93 KiB
Rust
//! # Liana commands
|
|
//!
|
|
//! External interface to the Liana daemon.
|
|
|
|
mod utils;
|
|
|
|
use crate::{
|
|
bitcoin::BitcoinInterface,
|
|
database::{Coin, DatabaseConnection, DatabaseInterface},
|
|
descriptors,
|
|
poller::PollerMessage,
|
|
spend::{
|
|
create_spend, AddrInfo, AncestorInfo, CandidateCoin, CreateSpendRes, SpendCreationError,
|
|
SpendOutputAddress, SpendTxFees, TxGetter,
|
|
},
|
|
DaemonControl, VERSION,
|
|
};
|
|
|
|
pub use crate::database::{CoinStatus, 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, HashMap, HashSet},
|
|
fmt,
|
|
sync::{self, mpsc},
|
|
};
|
|
|
|
use miniscript::{
|
|
bitcoin::{self, address, bip32, psbt::Psbt},
|
|
psbt::PsbtExt,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum CommandError {
|
|
NoOutpointForSelfSend,
|
|
InvalidFeerate(/* sats/vb */ u64),
|
|
UnknownOutpoint(bitcoin::OutPoint),
|
|
AlreadySpent(bitcoin::OutPoint),
|
|
ImmatureCoinbase(bitcoin::OutPoint),
|
|
Address(bitcoin::address::Error),
|
|
SpendCreation(SpendCreationError),
|
|
InsufficientFunds(
|
|
/* in value */ bitcoin::Amount,
|
|
/* out value */ Option<bitcoin::Amount>,
|
|
/* target feerate */ u64,
|
|
),
|
|
UnknownSpend(bitcoin::Txid),
|
|
// FIXME: when upgrading Miniscript put the actual error there
|
|
SpendFinalization(String),
|
|
TxBroadcast(String),
|
|
AlreadyRescanning,
|
|
InsaneRescanTimestamp(u32),
|
|
/// An error that might occur in the racy rescan triggering logic.
|
|
RescanTrigger(String),
|
|
RecoveryNotAvailable,
|
|
/// Overflowing or unhardened derivation index.
|
|
InvalidDerivationIndex,
|
|
RbfError(RbfErrorInfo),
|
|
EmptyFilterList,
|
|
}
|
|
|
|
impl fmt::Display for CommandError {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
match self {
|
|
Self::NoOutpointForSelfSend => {
|
|
write!(f, "No provided outpoint for self-send. Need at least one.")
|
|
}
|
|
Self::InvalidFeerate(sats_vb) => write!(f, "Invalid feerate: {} sats/vb.", sats_vb),
|
|
Self::AlreadySpent(op) => write!(f, "Coin at '{}' is already spent.", op),
|
|
Self::ImmatureCoinbase(op) => write!(
|
|
f,
|
|
"Coin at '{}' is from an immature coinbase transaction.",
|
|
op
|
|
),
|
|
Self::UnknownOutpoint(op) => write!(f, "Unknown outpoint '{}'.", op),
|
|
Self::Address(e) => write!(f, "Address error: {}", e),
|
|
Self::SpendCreation(e) => write!(f, "Creating spend: {}", e),
|
|
Self::InsufficientFunds(in_val, out_val, feerate) => {
|
|
if let Some(out_val) = out_val {
|
|
write!(
|
|
f,
|
|
"Cannot create a {} sat/vb transaction with input value {} and output value {}",
|
|
feerate, in_val, out_val
|
|
)
|
|
} else {
|
|
write!(
|
|
f,
|
|
"Not enough fund to create a {} sat/vb transaction with input value {}",
|
|
feerate, in_val
|
|
)
|
|
}
|
|
}
|
|
Self::UnknownSpend(txid) => write!(f, "Unknown spend transaction '{}'.", txid),
|
|
Self::SpendFinalization(e) => {
|
|
write!(f, "Failed to finalize the spend transaction PSBT: '{}'.", e)
|
|
}
|
|
Self::TxBroadcast(e) => write!(f, "Failed to broadcast transaction: '{}'.", e),
|
|
Self::AlreadyRescanning => write!(
|
|
f,
|
|
"There is already a rescan ongoing. Please wait for it to complete first."
|
|
),
|
|
Self::InsaneRescanTimestamp(t) => write!(f, "Insane timestamp '{}'.", t),
|
|
Self::RescanTrigger(s) => write!(f, "Error while starting rescan: '{}'", s),
|
|
Self::RecoveryNotAvailable => write!(
|
|
f,
|
|
"No coin currently spendable through this timelocked recovery path."
|
|
),
|
|
Self::InvalidDerivationIndex => {
|
|
write!(f, "Unhardened or overflowing BIP32 derivation index.")
|
|
}
|
|
Self::RbfError(e) => write!(f, "RBF error: '{}'.", e),
|
|
Self::EmptyFilterList => write!(f, "Filter list is empty, should supply None instead."),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for CommandError {}
|
|
|
|
impl From<SpendCreationError> for CommandError {
|
|
fn from(e: SpendCreationError) -> Self {
|
|
CommandError::SpendCreation(e)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum RbfErrorInfo {
|
|
MissingFeerate,
|
|
SuperfluousFeerate,
|
|
TooLowFeerate(u64),
|
|
NotSignaling,
|
|
}
|
|
|
|
impl fmt::Display for RbfErrorInfo {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
match *self {
|
|
Self::MissingFeerate => {
|
|
write!(f, "A feerate must be provided if not creating a cancel.")
|
|
}
|
|
Self::SuperfluousFeerate => {
|
|
write!(f, "A feerate must not be provided if creating a cancel. We'll always use the smallest one which satisfies the RBF rules.")
|
|
}
|
|
Self::TooLowFeerate(r) => write!(f, "Feerate too low: {}.", r),
|
|
Self::NotSignaling => write!(f, "Replacement candidate does not signal for RBF."),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A wallet transaction getter which fetches the transaction from our Bitcoin backend with a cache
|
|
/// to avoid needless redundant calls. Note the cache holds an Option<> so we also avoid redundant
|
|
/// calls when the txid isn't known by our Bitcoin backend.
|
|
struct BitcoindTxGetter<'a> {
|
|
bitcoind: &'a sync::Arc<sync::Mutex<dyn BitcoinInterface>>,
|
|
cache: HashMap<bitcoin::Txid, Option<bitcoin::Transaction>>,
|
|
}
|
|
|
|
impl<'a> BitcoindTxGetter<'a> {
|
|
pub fn new(bitcoind: &'a sync::Arc<sync::Mutex<dyn BitcoinInterface>>) -> Self {
|
|
Self {
|
|
bitcoind,
|
|
cache: HashMap::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> TxGetter for BitcoindTxGetter<'a> {
|
|
fn get_tx(&mut self, txid: &bitcoin::Txid) -> Option<bitcoin::Transaction> {
|
|
if let hash_map::Entry::Vacant(entry) = self.cache.entry(*txid) {
|
|
entry.insert(self.bitcoind.wallet_transaction(txid).map(|wtx| wtx.0));
|
|
}
|
|
self.cache.get(txid).cloned().flatten()
|
|
}
|
|
}
|
|
|
|
fn coin_to_candidate(
|
|
coin: &Coin,
|
|
must_select: bool,
|
|
sequence: Option<bitcoin::Sequence>,
|
|
ancestor_info: Option<AncestorInfo>,
|
|
) -> CandidateCoin {
|
|
CandidateCoin {
|
|
outpoint: coin.outpoint,
|
|
amount: coin.amount,
|
|
deriv_index: coin.derivation_index,
|
|
is_change: coin.is_change,
|
|
must_select,
|
|
sequence,
|
|
ancestor_info,
|
|
}
|
|
}
|
|
|
|
impl DaemonControl {
|
|
// Get the derived descriptor for this coin
|
|
fn derived_desc(&self, coin: &Coin) -> descriptors::DerivedSinglePathLianaDesc {
|
|
let desc = if coin.is_change {
|
|
self.config.main_descriptor.change_descriptor()
|
|
} else {
|
|
self.config.main_descriptor.receive_descriptor()
|
|
};
|
|
desc.derive(coin.derivation_index, &self.secp)
|
|
}
|
|
|
|
// Check whether this address is valid for the network we are operating on.
|
|
fn validate_address(
|
|
&self,
|
|
addr: bitcoin::Address<address::NetworkUnchecked>,
|
|
) -> Result<bitcoin::Address, CommandError> {
|
|
// NOTE: signet uses testnet addresses, and legacy addresses on regtest use testnet
|
|
// encoding.
|
|
addr.require_network(self.config.bitcoin_config.network)
|
|
.map_err(CommandError::Address)
|
|
}
|
|
|
|
// Get details about this address, if we know about it.
|
|
fn addr_info(
|
|
&self,
|
|
db_conn: &mut Box<dyn DatabaseConnection>,
|
|
addr: &bitcoin::Address,
|
|
) -> Option<AddrInfo> {
|
|
db_conn
|
|
.derivation_index_by_address(addr)
|
|
.map(|(index, is_change)| AddrInfo { index, is_change })
|
|
}
|
|
|
|
// Create an address to be used in an output of a spend transaction.
|
|
fn spend_addr(
|
|
&self,
|
|
db_conn: &mut Box<dyn DatabaseConnection>,
|
|
addr: bitcoin::Address,
|
|
) -> SpendOutputAddress {
|
|
SpendOutputAddress {
|
|
info: self.addr_info(db_conn, &addr),
|
|
addr,
|
|
}
|
|
}
|
|
|
|
// Get the change address for the next derivation index.
|
|
fn next_change_addr(&self, db_conn: &mut Box<dyn DatabaseConnection>) -> SpendOutputAddress {
|
|
let index = db_conn.change_index();
|
|
let desc = self
|
|
.config
|
|
.main_descriptor
|
|
.change_descriptor()
|
|
.derive(index, &self.secp);
|
|
let addr = desc.address(self.config.bitcoin_config.network);
|
|
SpendOutputAddress {
|
|
addr,
|
|
info: Some(AddrInfo {
|
|
index,
|
|
is_change: true,
|
|
}),
|
|
}
|
|
}
|
|
|
|
// If we detect the given address as ours, and it has a higher derivation index than our next
|
|
// derivation index, update our next derivation index to the one after the address'.
|
|
fn maybe_increase_next_deriv_index(
|
|
&self,
|
|
db_conn: &mut Box<dyn DatabaseConnection>,
|
|
addr_info: &Option<AddrInfo>,
|
|
) {
|
|
if let Some(AddrInfo { index, is_change }) = addr_info {
|
|
if *is_change && db_conn.change_index() <= *index {
|
|
let next_index = index
|
|
.increment()
|
|
.expect("Must not get into hardened territory");
|
|
db_conn.set_change_index(next_index, &self.secp);
|
|
} else if !is_change && db_conn.receive_index() <= *index {
|
|
let next_index = index
|
|
.increment()
|
|
.expect("Must not get into hardened territory");
|
|
db_conn.set_receive_index(next_index, &self.secp);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl DaemonControl {
|
|
/// Get information about the current state of the daemon
|
|
pub fn get_info(&self) -> GetInfoResult {
|
|
let mut db_conn = self.db.connection();
|
|
|
|
let block_height = db_conn.chain_tip().map(|tip| tip.height).unwrap_or(0);
|
|
let rescan_progress = db_conn
|
|
.rescan_timestamp()
|
|
.map(|_| self.bitcoin.rescan_progress().unwrap_or(1.0));
|
|
GetInfoResult {
|
|
version: VERSION.to_string(),
|
|
network: self.config.bitcoin_config.network,
|
|
block_height,
|
|
sync: self.bitcoin.sync_progress().rounded_up_progress(),
|
|
descriptors: GetInfoDescriptors {
|
|
main: self.config.main_descriptor.clone(),
|
|
},
|
|
rescan_progress,
|
|
timestamp: db_conn.timestamp(),
|
|
}
|
|
}
|
|
|
|
/// Get a new deposit address. This will always generate a new deposit address, regardless of
|
|
/// whether it was actually used.
|
|
pub fn get_new_address(&self) -> GetAddressResult {
|
|
let mut db_conn = self.db.connection();
|
|
let index = db_conn.receive_index();
|
|
let new_index = index
|
|
.increment()
|
|
.expect("Can't get into hardened territory");
|
|
db_conn.set_receive_index(new_index, &self.secp);
|
|
let address = self
|
|
.config
|
|
.main_descriptor
|
|
.receive_descriptor()
|
|
.derive(index, &self.secp)
|
|
.address(self.config.bitcoin_config.network);
|
|
GetAddressResult::new(address, index)
|
|
}
|
|
|
|
/// list addresses
|
|
pub fn list_addresses(
|
|
&self,
|
|
start_index: Option<u32>,
|
|
count: Option<u32>,
|
|
) -> Result<ListAddressesResult, CommandError> {
|
|
let mut db_conn = self.db.connection();
|
|
let receive_index: u32 = db_conn.receive_index().into();
|
|
let change_index: u32 = db_conn.change_index().into();
|
|
|
|
// If a start index isn't provided, we derive from index 0. Make sure the provided index is
|
|
// unhardened.
|
|
let start_index = bip32::ChildNumber::from_normal_idx(start_index.unwrap_or(0))
|
|
.map_err(|_| CommandError::InvalidDerivationIndex)?;
|
|
let start_index_u32: u32 = start_index.into();
|
|
|
|
// Derive the end index (ie, the first index to not be returned) from the count of
|
|
// addresses to provide. If no count was passed, use the next derivation index between
|
|
// change and receive as end index.
|
|
let end_index = if let Some(c) = count {
|
|
start_index_u32
|
|
.checked_add(c)
|
|
.ok_or(CommandError::InvalidDerivationIndex)?
|
|
} else {
|
|
receive_index.max(change_index)
|
|
};
|
|
|
|
// Derive all receive and change addresses for the queried range.
|
|
let addresses: Result<Vec<AddressInfo>, CommandError> = (start_index_u32..end_index)
|
|
.map(|index| {
|
|
let child = bip32::ChildNumber::from_normal_idx(index)
|
|
.map_err(|_| CommandError::InvalidDerivationIndex)?;
|
|
|
|
let receive = self
|
|
.config
|
|
.main_descriptor
|
|
.receive_descriptor()
|
|
.derive(child, &self.secp)
|
|
.address(self.config.bitcoin_config.network);
|
|
|
|
let change = self
|
|
.config
|
|
.main_descriptor
|
|
.change_descriptor()
|
|
.derive(child, &self.secp)
|
|
.address(self.config.bitcoin_config.network);
|
|
|
|
Ok(AddressInfo {
|
|
index,
|
|
receive,
|
|
change,
|
|
})
|
|
})
|
|
.collect();
|
|
Ok(ListAddressesResult::new(addresses?))
|
|
}
|
|
|
|
/// Get a list of all known coins, optionally by status and/or outpoint.
|
|
pub fn list_coins(
|
|
&self,
|
|
statuses: &[CoinStatus],
|
|
outpoints: &[bitcoin::OutPoint],
|
|
) -> ListCoinsResult {
|
|
let mut db_conn = self.db.connection();
|
|
let coins: Vec<ListCoinsEntry> = db_conn
|
|
.coins(statuses, outpoints)
|
|
.into_values()
|
|
.map(|coin| {
|
|
let Coin {
|
|
amount,
|
|
outpoint,
|
|
block_info,
|
|
spend_txid,
|
|
spend_block,
|
|
is_immature,
|
|
is_change,
|
|
derivation_index,
|
|
..
|
|
} = coin;
|
|
let spend_info = spend_txid.map(|txid| LCSpendInfo {
|
|
txid,
|
|
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,
|
|
derivation_index,
|
|
outpoint,
|
|
block_height,
|
|
spend_info,
|
|
is_immature,
|
|
is_change,
|
|
}
|
|
})
|
|
.collect();
|
|
ListCoinsResult { coins }
|
|
}
|
|
|
|
pub fn create_spend(
|
|
&self,
|
|
destinations: &HashMap<bitcoin::Address<bitcoin::address::NetworkUnchecked>, u64>,
|
|
coins_outpoints: &[bitcoin::OutPoint],
|
|
feerate_vb: u64,
|
|
change_address: Option<bitcoin::Address<bitcoin::address::NetworkUnchecked>>,
|
|
) -> Result<CreateSpendResult, CommandError> {
|
|
let is_self_send = destinations.is_empty();
|
|
// For self-send, the coins must be specified.
|
|
if is_self_send && coins_outpoints.is_empty() {
|
|
return Err(CommandError::NoOutpointForSelfSend);
|
|
}
|
|
if feerate_vb < 1 {
|
|
return Err(CommandError::InvalidFeerate(feerate_vb));
|
|
}
|
|
let mut db_conn = self.db.connection();
|
|
let mut tx_getter = BitcoindTxGetter::new(&self.bitcoin);
|
|
|
|
// Prepare the destination addresses.
|
|
let mut destinations_checked = Vec::with_capacity(destinations.len());
|
|
for (address, value_sat) in destinations {
|
|
let address = self.validate_address(address.clone())?;
|
|
let amount = bitcoin::Amount::from_sat(*value_sat);
|
|
let address = self.spend_addr(&mut db_conn, address);
|
|
destinations_checked.push((address, amount));
|
|
}
|
|
|
|
// The change address to be used if a change output needs to be created. It may be
|
|
// specified by the caller (for instance for the purpose of a sweep, or to avoid us
|
|
// creating a new change address on every call).
|
|
let change_address = change_address
|
|
.map(|addr| {
|
|
Ok::<_, CommandError>(self.spend_addr(&mut db_conn, self.validate_address(addr)?))
|
|
})
|
|
.transpose()?
|
|
.unwrap_or_else(|| self.next_change_addr(&mut db_conn));
|
|
|
|
// The candidate coins will be either all optional or all mandatory.
|
|
// If no coins have been specified, then coins will be selected automatically for
|
|
// the spend from a set of optional candidates.
|
|
// Otherwise, only the specified coins will be used, all as mandatory candidates.
|
|
let candidate_coins: Vec<CandidateCoin> = if coins_outpoints.is_empty() {
|
|
// From our unconfirmed coins, we only include those that are change outputs
|
|
// since unconfirmed external deposits are more at risk of being dropped
|
|
// unexpectedly from the mempool as they are beyond the user's control.
|
|
db_conn
|
|
.coins(&[CoinStatus::Unconfirmed, CoinStatus::Confirmed], &[])
|
|
.into_iter()
|
|
.filter_map(|(op, c)| {
|
|
if c.block_info.is_some() {
|
|
Some((c, None)) // confirmed coins have no ancestor info
|
|
} else if c.is_change {
|
|
// In case the mempool_entry is None, the coin will be included without
|
|
// any ancestor info.
|
|
Some((
|
|
c,
|
|
self.bitcoin.mempool_entry(&op.txid).map(AncestorInfo::from),
|
|
))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.map(|(c, ancestor_info)| {
|
|
coin_to_candidate(
|
|
&c,
|
|
/*must_select=*/ false,
|
|
/*sequence=*/ None,
|
|
ancestor_info,
|
|
)
|
|
})
|
|
.collect()
|
|
} else {
|
|
// Query from DB and sanity check the provided coins to spend.
|
|
let coins = db_conn.coins(&[], coins_outpoints);
|
|
for op in coins_outpoints {
|
|
let coin = coins.get(op).ok_or(CommandError::UnknownOutpoint(*op))?;
|
|
if coin.is_spent() {
|
|
return Err(CommandError::AlreadySpent(*op));
|
|
}
|
|
if coin.is_immature {
|
|
return Err(CommandError::ImmatureCoinbase(*op));
|
|
}
|
|
}
|
|
coins
|
|
.into_iter()
|
|
.map(|(op, c)| {
|
|
let ancestor_info = if c.block_info.is_none() {
|
|
// We include any non-change coins here as they have been selected by the caller.
|
|
// If the unconfirmed coin's transaction is no longer in the mempool, keep the
|
|
// coin as a candidate but without any ancestor info (same as confirmed candidate).
|
|
self.bitcoin.mempool_entry(&op.txid).map(AncestorInfo::from)
|
|
} else {
|
|
None
|
|
};
|
|
coin_to_candidate(
|
|
&c,
|
|
/*must_select=*/ true,
|
|
/*sequence=*/ None,
|
|
ancestor_info,
|
|
)
|
|
})
|
|
.collect()
|
|
};
|
|
|
|
// Create the PSBT. If there was no error in doing so make sure to update our next
|
|
// derivation index in case any address in the transaction outputs was ours and from the
|
|
// future.
|
|
let change_info = change_address.info;
|
|
let CreateSpendRes {
|
|
psbt,
|
|
has_change,
|
|
warnings,
|
|
} = match create_spend(
|
|
&self.config.main_descriptor,
|
|
&self.secp,
|
|
&mut tx_getter,
|
|
&destinations_checked,
|
|
&candidate_coins,
|
|
SpendTxFees::Regular(feerate_vb),
|
|
change_address,
|
|
) {
|
|
Ok(res) => res,
|
|
Err(SpendCreationError::CoinSelection(e)) => {
|
|
return Ok(CreateSpendResult::InsufficientFunds { missing: e.missing });
|
|
}
|
|
Err(e) => {
|
|
return Err(e.into());
|
|
}
|
|
};
|
|
for (addr, _) in destinations_checked {
|
|
self.maybe_increase_next_deriv_index(&mut db_conn, &addr.info);
|
|
}
|
|
if has_change {
|
|
self.maybe_increase_next_deriv_index(&mut db_conn, &change_info);
|
|
}
|
|
|
|
Ok(CreateSpendResult::Success {
|
|
psbt,
|
|
warnings: warnings.iter().map(|w| w.to_string()).collect(),
|
|
})
|
|
}
|
|
|
|
pub fn update_spend(&self, mut psbt: Psbt) -> Result<(), CommandError> {
|
|
let mut db_conn = self.db.connection();
|
|
let tx = &psbt.unsigned_tx;
|
|
|
|
// If the transaction already exists in DB, merge the signatures for each input on a best
|
|
// effort basis.
|
|
// We work on the newly provided PSBT, in case its content was updated.
|
|
let txid = tx.txid();
|
|
if let Some(db_psbt) = db_conn.spend_tx(&txid) {
|
|
let db_tx = db_psbt.unsigned_tx;
|
|
for i in 0..db_tx.input.len() {
|
|
if tx
|
|
.input
|
|
.get(i)
|
|
.map(|tx_in| tx_in.previous_output == db_tx.input[i].previous_output)
|
|
!= Some(true)
|
|
{
|
|
continue;
|
|
}
|
|
let psbtin = match psbt.inputs.get_mut(i) {
|
|
Some(psbtin) => psbtin,
|
|
None => continue,
|
|
};
|
|
let db_psbtin = match db_psbt.inputs.get(i) {
|
|
Some(db_psbtin) => db_psbtin,
|
|
None => continue,
|
|
};
|
|
psbtin
|
|
.partial_sigs
|
|
.extend(db_psbtin.partial_sigs.clone().into_iter());
|
|
psbtin
|
|
.tap_script_sigs
|
|
.extend(db_psbtin.tap_script_sigs.clone().into_iter());
|
|
if psbtin.tap_key_sig.is_none() {
|
|
psbtin.tap_key_sig = db_psbtin.tap_key_sig;
|
|
}
|
|
}
|
|
} else {
|
|
// If the transaction doesn't exist in DB already, sanity check its inputs.
|
|
// FIXME: should we allow for external inputs?
|
|
let outpoints: Vec<bitcoin::OutPoint> =
|
|
tx.input.iter().map(|txin| txin.previous_output).collect();
|
|
let coins = db_conn.coins_by_outpoints(&outpoints);
|
|
if coins.len() != outpoints.len() {
|
|
for op in outpoints {
|
|
if coins.get(&op).is_none() {
|
|
return Err(CommandError::UnknownOutpoint(op));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Finally, insert (or update) the PSBT in database.
|
|
db_conn.store_spend(&psbt);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn update_labels(&self, items: &HashMap<LabelItem, Option<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,
|
|
txids: Option<Vec<bitcoin::Txid>>,
|
|
) -> Result<ListSpendResult, CommandError> {
|
|
if let Some(ids) = &txids {
|
|
if ids.is_empty() {
|
|
return Err(CommandError::EmptyFilterList);
|
|
}
|
|
}
|
|
|
|
let mut db_conn = self.db.connection();
|
|
let spend_psbts = db_conn.list_spend();
|
|
|
|
let txids_set: Option<HashSet<_>> = txids.as_ref().map(|list| list.iter().collect());
|
|
let spend_txs = spend_psbts
|
|
.into_iter()
|
|
.filter_map(|(psbt, updated_at)| {
|
|
if let Some(set) = &txids_set {
|
|
if !set.contains(&psbt.unsigned_tx.txid()) {
|
|
return None;
|
|
}
|
|
}
|
|
Some(ListSpendEntry { psbt, updated_at })
|
|
})
|
|
.collect();
|
|
Ok(ListSpendResult { spend_txs })
|
|
}
|
|
|
|
pub fn delete_spend(&self, txid: &bitcoin::Txid) {
|
|
let mut db_conn = self.db.connection();
|
|
db_conn.delete_spend(txid);
|
|
}
|
|
|
|
/// Finalize and broadcast this stored Spend transaction.
|
|
pub fn broadcast_spend(&self, txid: &bitcoin::Txid) -> Result<(), CommandError> {
|
|
let mut db_conn = self.db.connection();
|
|
|
|
// First, try to finalize the spending transaction with the elements contained
|
|
// in the PSBT.
|
|
let mut spend_psbt = db_conn
|
|
.spend_tx(txid)
|
|
.ok_or(CommandError::UnknownSpend(*txid))?;
|
|
spend_psbt.finalize_mut(&self.secp).map_err(|e| {
|
|
CommandError::SpendFinalization(
|
|
e.into_iter()
|
|
.next()
|
|
.map(|e| e.to_string())
|
|
.unwrap_or_default(),
|
|
)
|
|
})?;
|
|
|
|
// Then, broadcast it (or try to, we never know if we are not going to hit an
|
|
// error at broadcast time).
|
|
// These checks are already performed at Spend creation time. TODO: a belt-and-suspenders is still worth it though.
|
|
let final_tx = spend_psbt.extract_tx_unchecked_fee_rate();
|
|
self.bitcoin
|
|
.broadcast_tx(&final_tx)
|
|
.map_err(CommandError::TxBroadcast)?;
|
|
|
|
// Finally, update our state with the changes from this transaction.
|
|
let (tx, rx) = mpsc::sync_channel(0);
|
|
if let Err(e) = self.poller_sender.send(PollerMessage::PollNow(tx)) {
|
|
log::error!("Error requesting update from poller: {}", e);
|
|
}
|
|
if let Err(e) = rx.recv() {
|
|
log::error!("Error receiving completion signal from poller: {}", e);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Create PSBT to replace the given transaction using RBF.
|
|
///
|
|
/// `txid` must point to a PSBT in our database.
|
|
///
|
|
/// `is_cancel` indicates whether to "cancel" the transaction by including only a single (change)
|
|
/// output in the replacement or otherwise to keep the same (non-change) outputs and simply
|
|
/// bump the fee.
|
|
/// If `true`, the only output of the RBF transaction will be change and the inputs will include
|
|
/// at least one of the inputs from the previous transaction. If `false`, all inputs from the previous
|
|
/// transaction will be used in the replacement.
|
|
/// In both cases:
|
|
/// - if the previous transaction includes a change output to one of our own change addresses,
|
|
/// this same address will be used for change in the RBF transaction, if required. If the previous
|
|
/// transaction pays to more than one of our change addresses, then the one receiving the highest
|
|
/// value will be used as a change address and the others will be treated as non-change outputs.
|
|
/// - the RBF transaction may include additional confirmed coins as inputs if required
|
|
/// in order to pay the higher fee (this applies also when replacing a self-send).
|
|
///
|
|
/// `feerate_vb` is the target feerate for the RBF transaction (in sat/vb). If `None`, it will be set
|
|
/// to 1 sat/vb larger than the feerate of the previous transaction, which is the minimum value allowed
|
|
/// when using RBF.
|
|
pub fn rbf_psbt(
|
|
&self,
|
|
txid: &bitcoin::Txid,
|
|
is_cancel: bool,
|
|
feerate_vb: Option<u64>,
|
|
) -> Result<CreateSpendResult, CommandError> {
|
|
let mut db_conn = self.db.connection();
|
|
let mut tx_getter = BitcoindTxGetter::new(&self.bitcoin);
|
|
|
|
if is_cancel && feerate_vb.is_some() {
|
|
return Err(CommandError::RbfError(RbfErrorInfo::SuperfluousFeerate));
|
|
}
|
|
|
|
let prev_psbt = db_conn
|
|
.spend_tx(txid)
|
|
.ok_or(CommandError::UnknownSpend(*txid))?;
|
|
if !prev_psbt.unsigned_tx.is_explicitly_rbf() {
|
|
return Err(CommandError::RbfError(RbfErrorInfo::NotSignaling));
|
|
}
|
|
let prev_outpoints: Vec<bitcoin::OutPoint> = prev_psbt
|
|
.unsigned_tx
|
|
.input
|
|
.iter()
|
|
.map(|txin| txin.previous_output)
|
|
.collect();
|
|
let prev_coins = db_conn.coins_by_outpoints(&prev_outpoints);
|
|
// Make sure all prev outpoints are coins in our DB.
|
|
if let Some(op) = prev_outpoints
|
|
.iter()
|
|
.find(|op| !prev_coins.contains_key(op))
|
|
{
|
|
return Err(CommandError::UnknownOutpoint(*op));
|
|
}
|
|
if let Some(op) = prev_coins.iter().find_map(|(_, coin)| {
|
|
if coin.spend_block.is_some() {
|
|
Some(coin.outpoint)
|
|
} else {
|
|
None
|
|
}
|
|
}) {
|
|
return Err(CommandError::AlreadySpent(op));
|
|
}
|
|
// Compute the minimal feerate and fee the replacement transaction must have to satisfy RBF
|
|
// rules #3, #4 and #6 (see
|
|
// https://github.com/bitcoin/bitcoin/blob/master/doc/policy/mempool-replacements.md). By
|
|
// default (ie if the transaction we are replacing was dropped from the mempool) there is
|
|
// no minimum absolute fee and the minimum feerate is 1, the minimum relay feerate.
|
|
let (min_feerate_vb, descendant_fees) = self
|
|
.bitcoin
|
|
.mempool_spenders(&prev_outpoints)
|
|
.into_iter()
|
|
.fold(
|
|
(1, bitcoin::Amount::from_sat(0)),
|
|
|(min_feerate, descendant_fee), entry| {
|
|
let entry_feerate = entry
|
|
.fees
|
|
.base
|
|
.checked_div(entry.vsize)
|
|
.expect("Can't have a null vsize or tx would be invalid")
|
|
.to_sat()
|
|
.checked_add(1)
|
|
.expect("Can't overflow or tx would be invalid");
|
|
(
|
|
std::cmp::max(min_feerate, entry_feerate),
|
|
descendant_fee + entry.fees.descendant,
|
|
)
|
|
},
|
|
);
|
|
// Check replacement transaction's target feerate, if set, is high enough,
|
|
// and otherwise set it to the min feerate found above.
|
|
let feerate_vb = if is_cancel {
|
|
min_feerate_vb
|
|
} else {
|
|
feerate_vb.ok_or(CommandError::RbfError(RbfErrorInfo::MissingFeerate))?
|
|
};
|
|
if feerate_vb < min_feerate_vb {
|
|
return Err(CommandError::RbfError(RbfErrorInfo::TooLowFeerate(
|
|
feerate_vb,
|
|
)));
|
|
}
|
|
// Get info about prev outputs to determine replacement outputs.
|
|
let prev_derivs: Vec<_> = prev_psbt
|
|
.unsigned_tx
|
|
.output
|
|
.iter()
|
|
.map(|txo| {
|
|
let address = bitcoin::Address::from_script(
|
|
&txo.script_pubkey,
|
|
self.config.bitcoin_config.network,
|
|
)
|
|
.expect("address already used in finalized transaction");
|
|
(
|
|
address.clone(),
|
|
txo.value,
|
|
db_conn.derivation_index_by_address(&address),
|
|
)
|
|
})
|
|
.collect();
|
|
// Set the previous change address to that of the change output with the largest value
|
|
// and then largest index.
|
|
let prev_change_address = prev_derivs
|
|
.iter()
|
|
.filter_map(|(addr, amt, deriv)| {
|
|
if let Some((ind, true)) = &deriv {
|
|
Some((addr, amt, ind))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.max_by(|(_, amt_1, ind_1), (_, amt_2, ind_2)| amt_1.cmp(amt_2).then(ind_1.cmp(ind_2)))
|
|
.map(|(addr, _, _)| addr)
|
|
.cloned();
|
|
// If not cancel, use all previous outputs as destinations, except for
|
|
// the output corresponding to the change address we found above.
|
|
// If cancel, the replacement will not have any destinations, only a change output.
|
|
let destinations = if !is_cancel {
|
|
prev_derivs
|
|
.into_iter()
|
|
.filter_map(|(addr, amt, _)| {
|
|
if prev_change_address.as_ref() != Some(&addr) {
|
|
Some((self.spend_addr(&mut db_conn, addr), amt))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect()
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
|
|
// If there was no previous change address, we set the change address for the replacement
|
|
// to our next change address. This way, we won't increment the change index with each attempt
|
|
// at creating the replacement PSBT below.
|
|
let change_address = prev_change_address
|
|
.map(|addr| self.spend_addr(&mut db_conn, addr))
|
|
.unwrap_or_else(|| self.next_change_addr(&mut db_conn));
|
|
// If `!is_cancel`, we take the previous coins as mandatory candidates and add confirmed coins as optional.
|
|
// Otherwise, we take the previous coins as optional candidates and let coin selection find the
|
|
// best solution that includes at least one of these. If there are insufficient funds to create the replacement
|
|
// transaction in this way, then we set candidates in the same way as for the `!is_cancel` case.
|
|
let mut candidate_coins: Vec<CandidateCoin> = prev_coins
|
|
.values()
|
|
.map(|c| {
|
|
// In case any previous coins are unconfirmed, we don't include their ancestor info
|
|
// in the candidate as the replacement fee and feerate will be higher and any
|
|
// additional fee to pay for ancestors should already have been taken into account
|
|
// when including these coins in the previous transaction.
|
|
coin_to_candidate(
|
|
c, /*must_select=*/ !is_cancel, /*sequence=*/ None,
|
|
/*ancestor_info=*/ None,
|
|
)
|
|
})
|
|
.collect();
|
|
let confirmed_cands: Vec<CandidateCoin> = db_conn
|
|
.coins(&[CoinStatus::Confirmed], &[])
|
|
.into_values()
|
|
.filter_map(|c| {
|
|
// Make sure we don't have duplicate candidates in case any of the coins are not
|
|
// currently set as spending in the DB (and are therefore still confirmed).
|
|
if !prev_coins.contains_key(&c.outpoint) {
|
|
Some(coin_to_candidate(
|
|
&c, /*must_select=*/ false, /*sequence=*/ None,
|
|
/*ancestor_info=*/ None,
|
|
))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect();
|
|
if !is_cancel {
|
|
candidate_coins.extend(&confirmed_cands);
|
|
}
|
|
// Try with increasing fee until fee paid by replacement transaction is high enough.
|
|
// Replacement fee must be at least:
|
|
// sum of fees paid by original transactions + incremental feerate * replacement size.
|
|
// Loop will continue until either we find a suitable replacement or we have insufficient funds.
|
|
let mut replacement_vsize = 0;
|
|
for incremental_feerate in 0.. {
|
|
let min_fee = descendant_fees.to_sat() + replacement_vsize * incremental_feerate;
|
|
let CreateSpendRes {
|
|
psbt: rbf_psbt,
|
|
has_change,
|
|
warnings,
|
|
} = match create_spend(
|
|
&self.config.main_descriptor,
|
|
&self.secp,
|
|
&mut tx_getter,
|
|
&destinations,
|
|
&candidate_coins,
|
|
SpendTxFees::Rbf(feerate_vb, min_fee),
|
|
change_address.clone(),
|
|
) {
|
|
Ok(psbt) => psbt,
|
|
Err(SpendCreationError::CoinSelection(e)) => {
|
|
// If we get a coin selection error due to insufficient funds and we want to cancel the
|
|
// transaction, then set all previous coins as mandatory and add confirmed coins as
|
|
// optional, unless we have already done this.
|
|
if is_cancel && candidate_coins.iter().all(|c| !c.must_select) {
|
|
for cand in candidate_coins.iter_mut() {
|
|
cand.must_select = true;
|
|
}
|
|
candidate_coins.extend(&confirmed_cands);
|
|
continue;
|
|
} else {
|
|
return Ok(CreateSpendResult::InsufficientFunds { missing: e.missing });
|
|
}
|
|
}
|
|
Err(e) => {
|
|
return Err(e.into());
|
|
}
|
|
};
|
|
replacement_vsize = self
|
|
.config
|
|
.main_descriptor
|
|
.unsigned_tx_max_vbytes(&rbf_psbt.unsigned_tx);
|
|
|
|
// Make sure it satisfies RBF rule 4.
|
|
if rbf_psbt.fee().expect("has already been sanity checked")
|
|
>= descendant_fees + bitcoin::Amount::from_sat(replacement_vsize)
|
|
{
|
|
// In case of success, make sure to update our next derivation index if any address
|
|
// used in the transaction outputs was from the future.
|
|
for (addr, _) in destinations {
|
|
self.maybe_increase_next_deriv_index(&mut db_conn, &addr.info);
|
|
}
|
|
if has_change {
|
|
self.maybe_increase_next_deriv_index(&mut db_conn, &change_address.info);
|
|
}
|
|
|
|
return Ok(CreateSpendResult::Success {
|
|
psbt: rbf_psbt,
|
|
warnings: warnings.iter().map(|w| w.to_string()).collect(),
|
|
});
|
|
}
|
|
}
|
|
|
|
unreachable!("We keep increasing the min fee until we run out of funds or satisfy rule 4.")
|
|
}
|
|
|
|
/// Trigger a rescan of the block chain for transactions involving our main descriptor between
|
|
/// the given date and the current tip.
|
|
/// The date must be after the genesis block time and before the current tip blocktime.
|
|
pub fn start_rescan(&self, timestamp: u32) -> Result<(), CommandError> {
|
|
let mut db_conn = self.db.connection();
|
|
let genesis_timestamp = self.bitcoin.genesis_block_timestamp();
|
|
|
|
let future_timestamp = self
|
|
.bitcoin
|
|
.tip_time()
|
|
.map(|t| timestamp >= t)
|
|
.unwrap_or(false);
|
|
if timestamp < genesis_timestamp || future_timestamp {
|
|
return Err(CommandError::InsaneRescanTimestamp(timestamp));
|
|
}
|
|
if db_conn.rescan_timestamp().is_some() || self.bitcoin.rescan_progress().is_some() {
|
|
return Err(CommandError::AlreadyRescanning);
|
|
}
|
|
|
|
// TODO: there is a race with the above check for whether the backend is already
|
|
// rescanning. This could make us crash with the bitcoind backend if someone triggered a
|
|
// rescan of the wallet just after we checked above and did now.
|
|
self.bitcoin
|
|
.start_rescan(&self.config.main_descriptor, timestamp)
|
|
.map_err(CommandError::RescanTrigger)?;
|
|
db_conn.set_rescan(timestamp);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// list_confirmed_transactions retrieves a limited list of transactions which occured between two given dates.
|
|
pub fn list_confirmed_transactions(
|
|
&self,
|
|
start: u32,
|
|
end: u32,
|
|
limit: u64,
|
|
) -> ListTransactionsResult {
|
|
let mut db_conn = self.db.connection();
|
|
let txids = db_conn.list_txids(start, end, limit);
|
|
let transactions = txids
|
|
.iter()
|
|
.filter_map(|txid| {
|
|
// TODO: batch those calls to the Bitcoin backend
|
|
// so it can in turn optimize its queries.
|
|
self.bitcoin
|
|
.wallet_transaction(txid)
|
|
.map(|(tx, block)| TransactionInfo {
|
|
tx,
|
|
height: block.map(|b| b.height),
|
|
time: block.map(|b| b.time),
|
|
})
|
|
})
|
|
.collect();
|
|
ListTransactionsResult { transactions }
|
|
}
|
|
|
|
/// list_transactions retrieves the transactions with the given txids.
|
|
pub fn list_transactions(&self, txids: &[bitcoin::Txid]) -> ListTransactionsResult {
|
|
let transactions = txids
|
|
.iter()
|
|
.filter_map(|txid| {
|
|
// TODO: batch those calls to the Bitcoin backend
|
|
// so it can in turn optimize its queries.
|
|
self.bitcoin
|
|
.wallet_transaction(txid)
|
|
.map(|(tx, block)| TransactionInfo {
|
|
tx,
|
|
height: block.map(|b| b.height),
|
|
time: block.map(|b| b.time),
|
|
})
|
|
})
|
|
.collect();
|
|
ListTransactionsResult { transactions }
|
|
}
|
|
|
|
/// Create a transaction that sweeps all coins for which a timelocked recovery path is
|
|
/// currently available to a provided address with the provided feerate.
|
|
///
|
|
/// The `timelock` parameter can be used to specify which recovery path to use. By default,
|
|
/// we'll use the first recovery path available.
|
|
///
|
|
/// Note that not all coins may be spendable through a single recovery path at the same time.
|
|
pub fn create_recovery(
|
|
&self,
|
|
address: bitcoin::Address<address::NetworkUnchecked>,
|
|
feerate_vb: u64,
|
|
timelock: Option<u16>,
|
|
) -> Result<CreateRecoveryResult, CommandError> {
|
|
if feerate_vb < 1 {
|
|
return Err(CommandError::InvalidFeerate(feerate_vb));
|
|
}
|
|
let mut tx_getter = BitcoindTxGetter::new(&self.bitcoin);
|
|
let mut db_conn = self.db.connection();
|
|
let sweep_addr = self.spend_addr(&mut db_conn, self.validate_address(address)?);
|
|
|
|
// Query the coins that we can spend through the specified recovery path (if no recovery
|
|
// path specified, use the first available one) from the database.
|
|
let current_height = self.bitcoin.chain_tip().height;
|
|
let timelock =
|
|
timelock.unwrap_or_else(|| self.config.main_descriptor.first_timelock_value());
|
|
let height_delta: i32 = timelock.into();
|
|
let sweepable_coins: Vec<_> = db_conn
|
|
.coins(&[CoinStatus::Confirmed], &[])
|
|
.into_values()
|
|
.filter_map(|c| {
|
|
// We are interested in coins available at the *next* block
|
|
if c.block_info
|
|
.map(|b| current_height + 1 >= b.height + height_delta)
|
|
.unwrap_or(false)
|
|
{
|
|
Some(coin_to_candidate(
|
|
&c,
|
|
/*must_select=*/ true,
|
|
/*sequence=*/ Some(bitcoin::Sequence::from_height(timelock)),
|
|
/*ancestor_info=*/ None,
|
|
))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect();
|
|
if sweepable_coins.is_empty() {
|
|
return Err(CommandError::RecoveryNotAvailable);
|
|
}
|
|
|
|
let sweep_addr_info = sweep_addr.info;
|
|
let CreateSpendRes {
|
|
psbt, has_change, ..
|
|
} = create_spend(
|
|
&self.config.main_descriptor,
|
|
&self.secp,
|
|
&mut tx_getter,
|
|
&[], // No destination, only the change address.
|
|
&sweepable_coins,
|
|
SpendTxFees::Regular(feerate_vb),
|
|
sweep_addr,
|
|
)?;
|
|
if has_change {
|
|
self.maybe_increase_next_deriv_index(&mut db_conn, &sweep_addr_info);
|
|
}
|
|
|
|
Ok(CreateRecoveryResult { psbt })
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct GetInfoDescriptors {
|
|
pub main: descriptors::LianaDescriptor,
|
|
}
|
|
|
|
/// Information about the daemon
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct GetInfoResult {
|
|
pub version: String,
|
|
pub network: bitcoin::Network,
|
|
pub block_height: i32,
|
|
pub sync: f64,
|
|
pub descriptors: GetInfoDescriptors,
|
|
/// The progress as a percentage (between 0 and 1) of an ongoing rescan if there is any
|
|
pub rescan_progress: Option<f64>,
|
|
/// Timestamp at wallet creation date
|
|
pub timestamp: u32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct GetAddressResult {
|
|
#[serde(deserialize_with = "deser_addr_assume_checked")]
|
|
pub address: bitcoin::Address,
|
|
pub derivation_index: bip32::ChildNumber,
|
|
}
|
|
|
|
impl GetAddressResult {
|
|
pub fn new(address: bitcoin::Address, derivation_index: bip32::ChildNumber) -> Self {
|
|
Self {
|
|
address,
|
|
derivation_index,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct GetLabelsResult {
|
|
pub labels: HashMap<String, String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
|
pub struct AddressInfo {
|
|
index: u32,
|
|
receive: bitcoin::Address,
|
|
change: bitcoin::Address,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
|
pub struct ListAddressesResult {
|
|
addresses: Vec<AddressInfo>,
|
|
}
|
|
|
|
impl ListAddressesResult {
|
|
pub fn new(addresses: Vec<AddressInfo>) -> Self {
|
|
ListAddressesResult { addresses }
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
pub struct LCSpendInfo {
|
|
pub txid: bitcoin::Txid,
|
|
/// The block height this spending transaction was confirmed at.
|
|
pub height: Option<i32>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ListCoinsEntry {
|
|
#[serde(
|
|
serialize_with = "ser_amount",
|
|
deserialize_with = "deser_amount_from_sats"
|
|
)]
|
|
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>,
|
|
/// Derivation index used to create the coin deposit address.
|
|
pub derivation_index: bip32::ChildNumber,
|
|
/// Information about the transaction spending this coin.
|
|
pub spend_info: Option<LCSpendInfo>,
|
|
/// Whether this coin was created by a coinbase transaction that is still immature.
|
|
pub is_immature: bool,
|
|
/// Whether the coin deposit address was derived from the change descriptor.
|
|
pub is_change: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ListCoinsResult {
|
|
pub coins: Vec<ListCoinsEntry>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(untagged)]
|
|
pub enum CreateSpendResult {
|
|
Success {
|
|
#[serde(serialize_with = "ser_to_string", deserialize_with = "deser_fromstr")]
|
|
psbt: Psbt,
|
|
warnings: Vec<String>,
|
|
},
|
|
InsufficientFunds {
|
|
missing: u64,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ListSpendEntry {
|
|
#[serde(serialize_with = "ser_to_string", deserialize_with = "deser_fromstr")]
|
|
pub psbt: Psbt,
|
|
pub updated_at: Option<u32>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ListSpendResult {
|
|
pub spend_txs: Vec<ListSpendEntry>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ListTransactionsResult {
|
|
pub transactions: Vec<TransactionInfo>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct TransactionInfo {
|
|
#[serde(serialize_with = "ser_hex", deserialize_with = "deser_hex")]
|
|
pub tx: bitcoin::Transaction,
|
|
pub height: Option<i32>,
|
|
pub time: Option<u32>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct CreateRecoveryResult {
|
|
#[serde(serialize_with = "ser_to_string", deserialize_with = "deser_fromstr")]
|
|
pub psbt: Psbt,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::{bitcoin::Block, database::BlockInfo, spend::InsaneFeeInfo, testutils::*};
|
|
|
|
use bitcoin::{
|
|
bip32::{self, ChildNumber},
|
|
blockdata::transaction::{TxIn, TxOut, Version as TxVersion},
|
|
locktime::absolute,
|
|
Amount, OutPoint, ScriptBuf, Sequence, Transaction, Txid, Witness,
|
|
};
|
|
use std::{collections::BTreeMap, str::FromStr};
|
|
|
|
#[test]
|
|
fn getinfo() {
|
|
let ms = DummyLiana::new(DummyBitcoind::new(), DummyDatabase::new());
|
|
// We can query getinfo
|
|
ms.control().get_info();
|
|
ms.shutdown();
|
|
}
|
|
|
|
#[test]
|
|
fn getnewaddress() {
|
|
let ms = DummyLiana::new(DummyBitcoind::new(), DummyDatabase::new());
|
|
|
|
let control = &ms.control();
|
|
// We can get an address
|
|
let addr = control.get_new_address().address;
|
|
assert_eq!(
|
|
addr,
|
|
bitcoin::Address::from_str(
|
|
"bc1q9ksrc647hx8zp2cewl8p5f487dgux3777yees8rjcx46t4daqzzqt7yga8"
|
|
)
|
|
.unwrap()
|
|
.assume_checked()
|
|
);
|
|
// We won't get the same twice.
|
|
let addr2 = control.get_new_address().address;
|
|
assert_ne!(addr, addr2);
|
|
|
|
ms.shutdown();
|
|
}
|
|
|
|
#[test]
|
|
fn listaddresses() {
|
|
let ms = DummyLiana::new(DummyBitcoind::new(), DummyDatabase::new());
|
|
|
|
let control = &ms.control();
|
|
|
|
let list = control.list_addresses(Some(2), Some(5)).unwrap();
|
|
|
|
assert_eq!(list.addresses[0].index, 2);
|
|
assert_eq!(list.addresses.last().unwrap().index, 6);
|
|
|
|
let addr0 = control.get_new_address().address;
|
|
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 list = control.list_addresses(Some(0), None).unwrap();
|
|
|
|
assert_eq!(list.addresses[0].index, 0);
|
|
assert_eq!(list.addresses[0].receive, addr0);
|
|
assert_eq!(list.addresses.last().unwrap().index, 4);
|
|
assert_eq!(list.addresses.last().unwrap().receive, addr4);
|
|
|
|
let list = control.list_addresses(None, None).unwrap();
|
|
|
|
assert_eq!(list.addresses[0].index, 0);
|
|
assert_eq!(list.addresses[0].receive, addr0);
|
|
assert_eq!(list.addresses.last().unwrap().index, 4);
|
|
assert_eq!(list.addresses.last().unwrap().receive, addr4);
|
|
|
|
let list = control.list_addresses(Some(1), Some(3)).unwrap();
|
|
|
|
assert_eq!(list.addresses[0].index, 1);
|
|
assert_eq!(list.addresses[0].receive, addr1);
|
|
assert_eq!(list.addresses.last().unwrap().index, 3);
|
|
assert_eq!(list.addresses.last().unwrap().receive, addr3);
|
|
|
|
let addr5 = control.get_new_address().address;
|
|
let list = control.list_addresses(Some(5), None).unwrap();
|
|
|
|
assert_eq!(list.addresses[0].index, 5);
|
|
assert_eq!(list.addresses[0].receive, addr5);
|
|
assert_eq!(list.addresses.last().unwrap().index, 5);
|
|
assert_eq!(list.addresses.last().unwrap().receive, addr5);
|
|
|
|
// We can get no address for the last unhardened index.
|
|
let max_unhardened_index = 2u32.pow(31) - 1;
|
|
let res = control
|
|
.list_addresses(Some(max_unhardened_index), Some(0))
|
|
.unwrap();
|
|
// This is equivalent to not passing a count.
|
|
assert_eq!(
|
|
res,
|
|
control
|
|
.list_addresses(Some(max_unhardened_index), None)
|
|
.unwrap()
|
|
);
|
|
// We can also get the one last unhardened index.
|
|
control
|
|
.list_addresses(Some(max_unhardened_index), Some(1))
|
|
.unwrap();
|
|
// However we can't get into hardened territory.
|
|
assert_eq!(
|
|
control
|
|
.list_addresses(Some(max_unhardened_index), Some(2))
|
|
.unwrap_err(),
|
|
CommandError::InvalidDerivationIndex
|
|
);
|
|
|
|
// We also can't pass a hardened start index.
|
|
let first_hardened_index = max_unhardened_index + 1;
|
|
assert_eq!(
|
|
control
|
|
.list_addresses(Some(first_hardened_index), None)
|
|
.unwrap_err(),
|
|
CommandError::InvalidDerivationIndex
|
|
);
|
|
assert_eq!(
|
|
control
|
|
.list_addresses(Some(first_hardened_index), Some(0))
|
|
.unwrap_err(),
|
|
CommandError::InvalidDerivationIndex
|
|
);
|
|
assert_eq!(
|
|
control
|
|
.list_addresses(Some(first_hardened_index), Some(1))
|
|
.unwrap_err(),
|
|
CommandError::InvalidDerivationIndex
|
|
);
|
|
|
|
// Much less so overflow.
|
|
assert_eq!(
|
|
control.list_addresses(Some(u32::MAX), None).unwrap_err(),
|
|
CommandError::InvalidDerivationIndex
|
|
);
|
|
assert_eq!(
|
|
control.list_addresses(Some(u32::MAX), Some(0)).unwrap_err(),
|
|
CommandError::InvalidDerivationIndex
|
|
);
|
|
assert_eq!(
|
|
control.list_addresses(Some(u32::MAX), Some(1)).unwrap_err(),
|
|
CommandError::InvalidDerivationIndex
|
|
);
|
|
|
|
// We won't crash if we pass a start index larger than the next derivation index without
|
|
// passing a count. (ie no underflow.)
|
|
let next_deriv_index = list.addresses.last().unwrap().index + 1;
|
|
control
|
|
.list_addresses(Some(next_deriv_index + 1), None)
|
|
.unwrap();
|
|
|
|
ms.shutdown();
|
|
}
|
|
|
|
#[test]
|
|
fn create_spend() {
|
|
let dummy_op = bitcoin::OutPoint::from_str(
|
|
"3753a1d74c0af8dd0a0f3b763c14faf3bd9ed03cbdf33337a074fb0e9f6c7810:0",
|
|
)
|
|
.unwrap();
|
|
let mut dummy_bitcoind = DummyBitcoind::new();
|
|
dummy_bitcoind.txs.insert(
|
|
dummy_op.txid,
|
|
(
|
|
bitcoin::Transaction {
|
|
version: TxVersion::TWO,
|
|
lock_time: absolute::LockTime::Blocks(absolute::Height::ZERO),
|
|
input: vec![],
|
|
output: vec![],
|
|
},
|
|
None,
|
|
),
|
|
);
|
|
let ms = DummyLiana::new(dummy_bitcoind, DummyDatabase::new());
|
|
let control = &ms.control();
|
|
|
|
// Arguments sanity checking
|
|
let dummy_addr =
|
|
bitcoin::Address::from_str("bc1qnsexk3gnuyayu92fc3tczvc7k62u22a22ua2kv").unwrap();
|
|
let dummy_value = 10_000;
|
|
let mut destinations = <HashMap<bitcoin::Address<address::NetworkUnchecked>, u64>>::new();
|
|
assert_eq!(
|
|
control.create_spend(&destinations, &[], 1, None),
|
|
Err(CommandError::NoOutpointForSelfSend)
|
|
);
|
|
destinations = [(dummy_addr.clone(), dummy_value)]
|
|
.iter()
|
|
.cloned()
|
|
.collect();
|
|
// Insufficient funds for coin selection.
|
|
assert!(matches!(
|
|
control.create_spend(&destinations, &[], 1, None),
|
|
Ok(CreateSpendResult::InsufficientFunds { .. }),
|
|
));
|
|
assert_eq!(
|
|
control.create_spend(&destinations, &[dummy_op], 0, None),
|
|
Err(CommandError::InvalidFeerate(0))
|
|
);
|
|
|
|
// The coin doesn't exist. If we create a new unspent one at this outpoint with a much
|
|
// higher value, we'll get a Spend transaction with a change output.
|
|
assert_eq!(
|
|
control.create_spend(&destinations, &[dummy_op], 1, None),
|
|
Err(CommandError::UnknownOutpoint(dummy_op))
|
|
);
|
|
let mut db_conn = control.db().lock().unwrap().connection();
|
|
db_conn.new_unspent_coins(&[Coin {
|
|
outpoint: dummy_op,
|
|
is_immature: false,
|
|
block_info: None,
|
|
amount: bitcoin::Amount::from_sat(100_000),
|
|
derivation_index: bip32::ChildNumber::from(13),
|
|
is_change: false,
|
|
spend_txid: None,
|
|
spend_block: None,
|
|
}]);
|
|
// If we try to use coin selection, the unconfirmed non-change coin will not be used
|
|
// as a candidate and so we get a coin selection error due to insufficient funds.
|
|
assert!(matches!(
|
|
control.create_spend(&destinations, &[], 1, None),
|
|
Ok(CreateSpendResult::InsufficientFunds { .. }),
|
|
));
|
|
let (psbt, warnings) = if let CreateSpendResult::Success { psbt, warnings } = control
|
|
.create_spend(&destinations, &[dummy_op], 1, None)
|
|
.unwrap()
|
|
{
|
|
(psbt, warnings)
|
|
} else {
|
|
panic!("expect successful spend creation")
|
|
};
|
|
assert!(psbt.inputs[0].non_witness_utxo.is_some());
|
|
let tx = psbt.unsigned_tx;
|
|
assert_eq!(tx.input.len(), 1);
|
|
assert_eq!(tx.input[0].previous_output, dummy_op);
|
|
assert_eq!(tx.output.len(), 2);
|
|
// It has change so no warnings expected.
|
|
assert!(warnings.is_empty());
|
|
assert_eq!(
|
|
tx.output[0].script_pubkey,
|
|
dummy_addr.payload().script_pubkey()
|
|
);
|
|
assert_eq!(tx.output[0].value.to_sat(), dummy_value);
|
|
|
|
// NOTE: if you are wondering about the usefulness of these tests asserting arbitrary fixed
|
|
// values, that's a belt-and-suspenders check to make sure size and fee calculations do not
|
|
// change unexpectedly. For instance this specific test caught how a change in
|
|
// rust-bitcoin's serialization of transactions with no input silently affected our fee
|
|
// calculation.
|
|
|
|
// Transaction is 1 in (P2WSH satisfaction), 2 outs. At 1sat/vb, it's 170 sats fees.
|
|
// At 2sats/vb, it's twice that.
|
|
assert_eq!(tx.output[1].value.to_sat(), 89_830);
|
|
let psbt = if let CreateSpendResult::Success { psbt, .. } = control
|
|
.create_spend(&destinations, &[dummy_op], 2, None)
|
|
.unwrap()
|
|
{
|
|
psbt
|
|
} else {
|
|
panic!("expect successful spend creation")
|
|
};
|
|
let tx = psbt.unsigned_tx;
|
|
assert_eq!(tx.output[1].value.to_sat(), 89_660);
|
|
|
|
// A feerate of 555 won't trigger the sanity checks (they were previously not taking the
|
|
// satisfaction size into account and overestimating the feerate).
|
|
control
|
|
.create_spend(&destinations, &[dummy_op], 555, None)
|
|
.unwrap();
|
|
|
|
// If we ask for a too high feerate, or a too large/too small output, it'll fail.
|
|
assert!(matches!(
|
|
control.create_spend(&destinations, &[dummy_op], 10_000, None),
|
|
Ok(CreateSpendResult::InsufficientFunds { .. }),
|
|
));
|
|
*destinations.get_mut(&dummy_addr).unwrap() = 100_001;
|
|
assert!(matches!(
|
|
control.create_spend(&destinations, &[dummy_op], 1, None),
|
|
Ok(CreateSpendResult::InsufficientFunds { .. }),
|
|
));
|
|
*destinations.get_mut(&dummy_addr).unwrap() = 4_500;
|
|
assert_eq!(
|
|
control.create_spend(&destinations, &[dummy_op], 1, None),
|
|
Err(CommandError::SpendCreation(
|
|
SpendCreationError::InvalidOutputValue(bitcoin::Amount::from_sat(4_500))
|
|
))
|
|
);
|
|
|
|
// If we ask to create an output for an address from another network, it will fail.
|
|
let invalid_addr =
|
|
bitcoin::Address::new(bitcoin::Network::Testnet, dummy_addr.payload().clone());
|
|
let invalid_destinations: HashMap<bitcoin::Address<address::NetworkUnchecked>, u64> =
|
|
[(invalid_addr, dummy_value)].iter().cloned().collect();
|
|
assert!(matches!(
|
|
control.create_spend(&invalid_destinations, &[dummy_op], 1, None),
|
|
Err(CommandError::Address(
|
|
address::Error::NetworkValidation { .. }
|
|
))
|
|
));
|
|
|
|
// If we ask for a large, but valid, output we won't get a change output. 95_000 because we
|
|
// won't create an output lower than 5k sats.
|
|
*destinations.get_mut(&dummy_addr).unwrap() = 95_000;
|
|
let (psbt, warnings) = if let CreateSpendResult::Success { psbt, warnings } = control
|
|
.create_spend(&destinations, &[dummy_op], 1, None)
|
|
.unwrap()
|
|
{
|
|
(psbt, warnings)
|
|
} else {
|
|
panic!("expect successful spend creation")
|
|
};
|
|
let tx = psbt.unsigned_tx;
|
|
assert_eq!(tx.input.len(), 1);
|
|
assert_eq!(tx.input[0].previous_output, dummy_op);
|
|
assert_eq!(tx.output.len(), 1);
|
|
assert_eq!(
|
|
tx.output[0].script_pubkey,
|
|
dummy_addr.payload().script_pubkey()
|
|
);
|
|
assert_eq!(tx.output[0].value.to_sat(), 95_000);
|
|
// change = 100_000 - 95_000 - /* fee without change */ 127 - /* extra fee for change output */ 43 = 4830
|
|
assert_eq!(warnings, vec!["Change amount of 4830 sats added to fee as it was too small to create a transaction output."]);
|
|
|
|
// Increase the target value by the change amount and the warning will disappear.
|
|
*destinations.get_mut(&dummy_addr).unwrap() = 95_000 + 4_830;
|
|
let (psbt, warnings) = if let CreateSpendResult::Success { psbt, warnings } = control
|
|
.create_spend(&destinations, &[dummy_op], 1, None)
|
|
.unwrap()
|
|
{
|
|
(psbt, warnings)
|
|
} else {
|
|
panic!("expect successful spend creation")
|
|
};
|
|
let tx = psbt.unsigned_tx;
|
|
assert_eq!(tx.output.len(), 1);
|
|
assert!(warnings.is_empty());
|
|
|
|
// Now increase target also by the extra fee that was paying for change and we can still create the spend.
|
|
*destinations.get_mut(&dummy_addr).unwrap() =
|
|
95_000 + 4_830 + /* fee for change output */ 43;
|
|
let (psbt, warnings) = if let CreateSpendResult::Success { psbt, warnings } = control
|
|
.create_spend(&destinations, &[dummy_op], 1, None)
|
|
.unwrap()
|
|
{
|
|
(psbt, warnings)
|
|
} else {
|
|
panic!("expect successful spend creation")
|
|
};
|
|
let tx = psbt.unsigned_tx;
|
|
assert_eq!(tx.output.len(), 1);
|
|
assert!(warnings.is_empty());
|
|
|
|
// Now increase the target by 1 more sat and we will have insufficient funds.
|
|
*destinations.get_mut(&dummy_addr).unwrap() =
|
|
95_000 + 4_830 + /* fee for change output */ 43 + 1;
|
|
assert_eq!(
|
|
control.create_spend(&destinations, &[dummy_op], 1, None),
|
|
Ok(CreateSpendResult::InsufficientFunds { missing: 1 }),
|
|
);
|
|
|
|
// Now decrease the target so that the lost change is just 1 sat.
|
|
*destinations.get_mut(&dummy_addr).unwrap() =
|
|
100_000 - /* fee without change */ 127 - /* extra fee for change output */ 43 - 1;
|
|
let warnings = if let CreateSpendResult::Success { warnings, .. } = control
|
|
.create_spend(&destinations, &[dummy_op], 1, None)
|
|
.unwrap()
|
|
{
|
|
warnings
|
|
} else {
|
|
panic!("expect successful spend creation")
|
|
};
|
|
// Message uses "sat" instead of "sats" when value is 1.
|
|
assert_eq!(warnings, vec!["Change amount of 1 sat added to fee as it was too small to create a transaction output."]);
|
|
|
|
// Now decrease the target value so that we have enough for a change output.
|
|
*destinations.get_mut(&dummy_addr).unwrap() =
|
|
95_000 - /* fee without change */ 127 - /* extra fee for change output */ 43;
|
|
let (psbt, warnings) = if let CreateSpendResult::Success { psbt, warnings } = control
|
|
.create_spend(&destinations, &[dummy_op], 1, None)
|
|
.unwrap()
|
|
{
|
|
(psbt, warnings)
|
|
} else {
|
|
panic!("expect successful spend creation")
|
|
};
|
|
let tx = psbt.unsigned_tx;
|
|
assert_eq!(tx.output.len(), 2);
|
|
assert_eq!(tx.output[1].value.to_sat(), 5_000);
|
|
assert!(warnings.is_empty());
|
|
|
|
// Now increase the target by 1 and we'll get a warning again, this time for 1 less than the dust threshold.
|
|
*destinations.get_mut(&dummy_addr).unwrap() =
|
|
95_000 - /* fee without change */ 127 - /* extra fee for change output */ 43 + 1;
|
|
let warnings = if let CreateSpendResult::Success { warnings, .. } = control
|
|
.create_spend(&destinations, &[dummy_op], 1, None)
|
|
.unwrap()
|
|
{
|
|
warnings
|
|
} else {
|
|
panic!("expect successful spend creation")
|
|
};
|
|
assert_eq!(warnings, vec!["Change amount of 4999 sats added to fee as it was too small to create a transaction output."]);
|
|
|
|
// Now if we mark the coin as spent, we won't create another Spend transaction containing
|
|
// it.
|
|
db_conn.spend_coins(&[(
|
|
dummy_op,
|
|
bitcoin::Txid::from_str(
|
|
"ef78f79ba747813887747cf8582897a48f1a09f1ca04d2cd3d6fcfdcbb5e0797",
|
|
)
|
|
.unwrap(),
|
|
)]);
|
|
assert_eq!(
|
|
control.create_spend(&destinations, &[dummy_op], 1, None),
|
|
Err(CommandError::AlreadySpent(dummy_op))
|
|
);
|
|
// If we try to use coin selection, the spent coin will not be used as a candidate
|
|
// and so we get a coin selection error due to insufficient funds.
|
|
assert!(matches!(
|
|
control.create_spend(&destinations, &[], 1, None),
|
|
Ok(CreateSpendResult::InsufficientFunds { .. }),
|
|
));
|
|
|
|
// We'd bail out if they tried to create a transaction with a too high feerate.
|
|
let dummy_op_dup = bitcoin::OutPoint {
|
|
txid: dummy_op.txid,
|
|
vout: dummy_op.vout + 10,
|
|
};
|
|
db_conn.new_unspent_coins(&[Coin {
|
|
outpoint: dummy_op_dup,
|
|
is_immature: false,
|
|
block_info: None,
|
|
amount: bitcoin::Amount::from_sat(400_000),
|
|
derivation_index: bip32::ChildNumber::from(42),
|
|
is_change: false,
|
|
spend_txid: None,
|
|
spend_block: None,
|
|
}]);
|
|
// Even though 1_000 is the max feerate allowed by our sanity check, we need to
|
|
// use 1_003 in order to exceed it and fail this test since coin selection is
|
|
// based on a minimum feerate of `feerate_vb / 4.0` sats/wu, which can result in
|
|
// the sats/vb feerate being lower than `feerate_vb`.
|
|
assert_eq!(
|
|
control.create_spend(&destinations, &[dummy_op_dup], 1_003, None),
|
|
Err(CommandError::SpendCreation(SpendCreationError::InsaneFees(
|
|
InsaneFeeInfo::TooHighFeerate(1_001)
|
|
)))
|
|
);
|
|
|
|
// Add an unconfirmed change coin to be used for coin selection.
|
|
let confirmed_op_1 = bitcoin::OutPoint {
|
|
txid: dummy_op.txid,
|
|
vout: dummy_op.vout + 100,
|
|
};
|
|
db_conn.new_unspent_coins(&[Coin {
|
|
outpoint: confirmed_op_1,
|
|
is_immature: false,
|
|
block_info: None,
|
|
amount: bitcoin::Amount::from_sat(80_000),
|
|
derivation_index: bip32::ChildNumber::from(42),
|
|
is_change: true,
|
|
spend_txid: None,
|
|
spend_block: None,
|
|
}]);
|
|
// Coin selection error due to insufficient funds.
|
|
assert!(matches!(
|
|
control.create_spend(&destinations, &[], 1, None),
|
|
Ok(CreateSpendResult::InsufficientFunds { .. }),
|
|
));
|
|
// Set destination amount equal to value of confirmed coins.
|
|
*destinations.get_mut(&dummy_addr).unwrap() = 80_000;
|
|
// Coin selection error occurs due to insufficient funds to pay fee.
|
|
assert!(matches!(
|
|
control.create_spend(&destinations, &[], 1, None),
|
|
Ok(CreateSpendResult::InsufficientFunds { .. }),
|
|
));
|
|
let confirmed_op_2 = bitcoin::OutPoint {
|
|
txid: confirmed_op_1.txid,
|
|
vout: confirmed_op_1.vout + 10,
|
|
};
|
|
// Add new confirmed coin to cover the fee.
|
|
db_conn.new_unspent_coins(&[Coin {
|
|
outpoint: confirmed_op_2,
|
|
is_immature: false,
|
|
block_info: Some(BlockInfo {
|
|
height: 174500,
|
|
time: 174500,
|
|
}),
|
|
amount: bitcoin::Amount::from_sat(20_000),
|
|
derivation_index: bip32::ChildNumber::from(43),
|
|
is_change: false,
|
|
spend_txid: None,
|
|
spend_block: None,
|
|
}]);
|
|
// First, create a transaction using auto coin selection.
|
|
let psbt = if let CreateSpendResult::Success { psbt, .. } =
|
|
control.create_spend(&destinations, &[], 1, None).unwrap()
|
|
{
|
|
psbt
|
|
} else {
|
|
panic!("expect successful spend creation")
|
|
};
|
|
let tx_auto = psbt.unsigned_tx;
|
|
let mut tx_prev_outpoints = tx_auto
|
|
.input
|
|
.iter()
|
|
.map(|txin| txin.previous_output)
|
|
.collect::<Vec<OutPoint>>();
|
|
tx_prev_outpoints.sort();
|
|
assert_eq!(tx_auto.input.len(), 2);
|
|
assert_eq!(tx_prev_outpoints, vec![confirmed_op_1, confirmed_op_2]);
|
|
// Output includes change.
|
|
assert_eq!(tx_auto.output.len(), 2);
|
|
assert_eq!(
|
|
tx_auto.output[0].script_pubkey,
|
|
dummy_addr.payload().script_pubkey()
|
|
);
|
|
assert_eq!(tx_auto.output[0].value, Amount::from_sat(80_000));
|
|
|
|
// Create a second transaction using manual coin selection.
|
|
let psbt = if let CreateSpendResult::Success { psbt, .. } = control
|
|
.create_spend(&destinations, &[confirmed_op_1, confirmed_op_2], 1, None)
|
|
.unwrap()
|
|
{
|
|
psbt
|
|
} else {
|
|
panic!("expect successful spend creation")
|
|
};
|
|
let tx_manual = psbt.unsigned_tx;
|
|
// Check that manual and auto selection give same outputs (except change address).
|
|
assert_ne!(tx_auto.output, tx_manual.output);
|
|
assert_eq!(tx_auto.output.len(), tx_manual.output.len());
|
|
assert_eq!(tx_auto.output[0], tx_manual.output[0]);
|
|
assert_eq!(tx_auto.output[1].value, tx_manual.output[1].value);
|
|
assert_ne!(
|
|
tx_auto.output[1].script_pubkey,
|
|
tx_manual.output[1].script_pubkey
|
|
);
|
|
// Check inputs are also the same. Need to sort as order is not guaranteed by `create_spend`.
|
|
let mut auto_input = tx_auto.clone().input;
|
|
let mut manual_input = tx_manual.input;
|
|
auto_input.sort();
|
|
manual_input.sort();
|
|
assert_eq!(auto_input, manual_input);
|
|
|
|
// Now do the same again, but this time specifying the change address to be the same
|
|
// as for the auto spend.
|
|
let change_address = bitcoin::Address::from_script(
|
|
tx_auto.output[1].script_pubkey.as_script(),
|
|
bitcoin::Network::Bitcoin,
|
|
)
|
|
.unwrap();
|
|
let psbt = if let CreateSpendResult::Success { psbt, .. } = control
|
|
.create_spend(
|
|
&destinations,
|
|
&[confirmed_op_1, confirmed_op_2],
|
|
1,
|
|
Some(change_address.as_unchecked().clone()),
|
|
)
|
|
.unwrap()
|
|
{
|
|
psbt
|
|
} else {
|
|
panic!("expect successful spend creation")
|
|
};
|
|
let tx_manual = psbt.unsigned_tx;
|
|
// Now the outputs of each transaction are the same.
|
|
assert_eq!(tx_auto.output, tx_manual.output);
|
|
// Check again that inputs are still the same.
|
|
let mut auto_input = tx_auto.input;
|
|
let mut manual_input = tx_manual.input;
|
|
auto_input.sort();
|
|
manual_input.sort();
|
|
assert_eq!(auto_input, manual_input);
|
|
|
|
// Add a confirmed coin with a value near the dust limit and check that
|
|
// `InsufficientFunds` error is returned if feerate is too high.
|
|
let confirmed_op_3 = bitcoin::OutPoint {
|
|
txid: confirmed_op_2.txid,
|
|
vout: confirmed_op_2.vout + 10,
|
|
};
|
|
db_conn.new_unspent_coins(&[Coin {
|
|
outpoint: confirmed_op_3,
|
|
is_immature: false,
|
|
block_info: Some(BlockInfo {
|
|
height: 174500,
|
|
time: 174500,
|
|
}),
|
|
amount: bitcoin::Amount::from_sat(5_250),
|
|
derivation_index: bip32::ChildNumber::from(56),
|
|
is_change: false,
|
|
spend_txid: None,
|
|
spend_block: None,
|
|
}]);
|
|
let empty_dest = &HashMap::<bitcoin::Address<address::NetworkUnchecked>, u64>::new();
|
|
assert!(matches!(
|
|
control.create_spend(empty_dest, &[confirmed_op_3], 5, None),
|
|
Ok(CreateSpendResult::InsufficientFunds { .. }),
|
|
));
|
|
// If we use a lower fee, the self-send will succeed.
|
|
let psbt = if let CreateSpendResult::Success { psbt, .. } = control
|
|
.create_spend(empty_dest, &[confirmed_op_3], 1, None)
|
|
.unwrap()
|
|
{
|
|
psbt
|
|
} else {
|
|
panic!("expect successful spend creation")
|
|
};
|
|
let tx = psbt.unsigned_tx;
|
|
let tx_prev_outpoints = tx
|
|
.input
|
|
.iter()
|
|
.map(|txin| txin.previous_output)
|
|
.collect::<Vec<OutPoint>>();
|
|
assert_eq!(tx.input.len(), 1);
|
|
assert_eq!(tx_prev_outpoints, vec![confirmed_op_3]);
|
|
assert_eq!(tx.output.len(), 1);
|
|
|
|
// Can't create a transaction that spends an immature coinbase deposit.
|
|
let imma_op = bitcoin::OutPoint::from_str(
|
|
"4753a1d74c0af8dd0a0f3b763c14faf3bd9ed03cbdf33337a074fb0e9f6c7810:0",
|
|
)
|
|
.unwrap();
|
|
db_conn.new_unspent_coins(&[Coin {
|
|
outpoint: imma_op,
|
|
is_immature: true,
|
|
block_info: None,
|
|
amount: bitcoin::Amount::from_sat(100_000),
|
|
derivation_index: bip32::ChildNumber::from(13),
|
|
is_change: false,
|
|
spend_txid: None,
|
|
spend_block: None,
|
|
}]);
|
|
assert_eq!(
|
|
control.create_spend(&destinations, &[imma_op], 1_001, None),
|
|
Err(CommandError::ImmatureCoinbase(imma_op))
|
|
);
|
|
|
|
ms.shutdown();
|
|
}
|
|
|
|
#[test]
|
|
fn update_spend() {
|
|
let dummy_op_a = bitcoin::OutPoint::from_str(
|
|
"3753a1d74c0af8dd0a0f3b763c14faf3bd9ed03cbdf33337a074fb0e9f6c7810:0",
|
|
)
|
|
.unwrap();
|
|
let dummy_op_b = bitcoin::OutPoint::from_str(
|
|
"4753a1d74c0af8dd0a0f3b763c14faf3bd9ed03cbdf33337a074fb0e9f6c7810:1",
|
|
)
|
|
.unwrap();
|
|
let mut dummy_bitcoind = DummyBitcoind::new();
|
|
let dummy_tx = bitcoin::Transaction {
|
|
version: TxVersion::TWO,
|
|
lock_time: absolute::LockTime::Blocks(absolute::Height::ZERO),
|
|
input: vec![],
|
|
output: vec![],
|
|
};
|
|
dummy_bitcoind
|
|
.txs
|
|
.insert(dummy_op_a.txid, (dummy_tx.clone(), None));
|
|
dummy_bitcoind.txs.insert(dummy_op_b.txid, (dummy_tx, None));
|
|
let ms = DummyLiana::new(dummy_bitcoind, DummyDatabase::new());
|
|
let control = &ms.control();
|
|
let mut db_conn = control.db().lock().unwrap().connection();
|
|
|
|
// Add two (unconfirmed) coins in DB
|
|
db_conn.new_unspent_coins(&[
|
|
Coin {
|
|
outpoint: dummy_op_a,
|
|
is_immature: false,
|
|
block_info: None,
|
|
amount: bitcoin::Amount::from_sat(100_000),
|
|
derivation_index: bip32::ChildNumber::from(13),
|
|
is_change: false,
|
|
spend_txid: None,
|
|
spend_block: None,
|
|
},
|
|
Coin {
|
|
outpoint: dummy_op_b,
|
|
is_immature: false,
|
|
block_info: None,
|
|
amount: bitcoin::Amount::from_sat(115_680),
|
|
derivation_index: bip32::ChildNumber::from(34),
|
|
is_change: false,
|
|
spend_txid: None,
|
|
spend_block: None,
|
|
},
|
|
]);
|
|
|
|
// Now create three transactions spending those coins differently
|
|
let dummy_addr_a =
|
|
bitcoin::Address::from_str("bc1qnsexk3gnuyayu92fc3tczvc7k62u22a22ua2kv").unwrap();
|
|
let dummy_addr_b =
|
|
bitcoin::Address::from_str("bc1q39srgatmkp6k2ne3l52yhkjprdvunvspqydmkx").unwrap();
|
|
let dummy_value_a = 50_000;
|
|
let dummy_value_b = 60_000;
|
|
let destinations_a: HashMap<bitcoin::Address<address::NetworkUnchecked>, u64> =
|
|
[(dummy_addr_a.clone(), dummy_value_a)]
|
|
.iter()
|
|
.cloned()
|
|
.collect();
|
|
let destinations_b: HashMap<bitcoin::Address<address::NetworkUnchecked>, u64> =
|
|
[(dummy_addr_b.clone(), dummy_value_b)]
|
|
.iter()
|
|
.cloned()
|
|
.collect();
|
|
let destinations_c: HashMap<bitcoin::Address<address::NetworkUnchecked>, u64> =
|
|
[(dummy_addr_a, dummy_value_a), (dummy_addr_b, dummy_value_b)]
|
|
.iter()
|
|
.cloned()
|
|
.collect();
|
|
let mut psbt_a = if let CreateSpendResult::Success { psbt, .. } = control
|
|
.create_spend(&destinations_a, &[dummy_op_a], 1, None)
|
|
.unwrap()
|
|
{
|
|
psbt
|
|
} else {
|
|
panic!("expect successful spend creation")
|
|
};
|
|
let txid_a = psbt_a.unsigned_tx.txid();
|
|
let psbt_b = if let CreateSpendResult::Success { psbt, .. } = control
|
|
.create_spend(&destinations_b, &[dummy_op_b], 10, None)
|
|
.unwrap()
|
|
{
|
|
psbt
|
|
} else {
|
|
panic!("expect successful spend creation")
|
|
};
|
|
let txid_b = psbt_b.unsigned_tx.txid();
|
|
let psbt_c = if let CreateSpendResult::Success { psbt, .. } = control
|
|
.create_spend(&destinations_c, &[dummy_op_a, dummy_op_b], 100, None)
|
|
.unwrap()
|
|
{
|
|
psbt
|
|
} else {
|
|
panic!("expect successful spend creation")
|
|
};
|
|
let txid_c = psbt_c.unsigned_tx.txid();
|
|
|
|
// We can store and query them all
|
|
control.update_spend(psbt_a.clone()).unwrap();
|
|
assert_eq!(db_conn.spend_tx(&txid_a).unwrap(), psbt_a);
|
|
control.update_spend(psbt_b.clone()).unwrap();
|
|
assert_eq!(db_conn.spend_tx(&txid_b).unwrap(), psbt_b);
|
|
control.update_spend(psbt_c.clone()).unwrap();
|
|
assert_eq!(db_conn.spend_tx(&txid_c).unwrap(), psbt_c);
|
|
|
|
// As well as update them, with or without new signatures
|
|
let sig = bitcoin::ecdsa::Signature::from_str("304402204004fcdbb9c0d0cbf585f58cee34dccb012efbd8fc2b0d5e97760045ae35803802201a0bd7ec2383e0b93748abc9946c8e17a8312e314dab85982aeba650e738cbf401").unwrap();
|
|
psbt_a.inputs[0].partial_sigs.insert(
|
|
bitcoin::PublicKey::from_str(
|
|
"023a664c5617412f0b292665b1fd9d766456a7a3b1614c7e7c5f411200ff1958ef",
|
|
)
|
|
.unwrap(),
|
|
sig,
|
|
);
|
|
control.update_spend(psbt_a.clone()).unwrap();
|
|
assert_eq!(db_conn.spend_tx(&txid_a).unwrap(), psbt_a);
|
|
control.update_spend(psbt_b.clone()).unwrap();
|
|
assert_eq!(db_conn.spend_tx(&txid_b).unwrap(), psbt_b);
|
|
control.update_spend(psbt_c.clone()).unwrap();
|
|
assert_eq!(db_conn.spend_tx(&txid_c).unwrap(), psbt_c);
|
|
|
|
// We can't store a PSBT spending an external coin
|
|
let external_op = bitcoin::OutPoint::from_str(
|
|
"8753a1d74c0af8dd0a0f3b763c14faf3bd9ed03cbdf33337a074fb0e9f6c7810:2",
|
|
)
|
|
.unwrap();
|
|
psbt_a.unsigned_tx.input[0].previous_output = external_op;
|
|
assert_eq!(
|
|
control.update_spend(psbt_a),
|
|
Err(CommandError::UnknownOutpoint(external_op))
|
|
);
|
|
|
|
ms.shutdown();
|
|
}
|
|
|
|
#[test]
|
|
fn rbf_psbt() {
|
|
let dummy_op_a = bitcoin::OutPoint::from_str(
|
|
"3753a1d74c0af8dd0a0f3b763c14faf3bd9ed03cbdf33337a074fb0e9f6c7810:0",
|
|
)
|
|
.unwrap();
|
|
let mut dummy_bitcoind = DummyBitcoind::new();
|
|
// Transaction spends outpoint a.
|
|
let dummy_tx_a = bitcoin::Transaction {
|
|
version: TxVersion::TWO,
|
|
lock_time: absolute::LockTime::Blocks(absolute::Height::ZERO),
|
|
input: vec![bitcoin::TxIn {
|
|
previous_output: dummy_op_a,
|
|
sequence: bitcoin::Sequence::ENABLE_RBF_NO_LOCKTIME,
|
|
..bitcoin::TxIn::default()
|
|
}],
|
|
output: vec![],
|
|
};
|
|
// PSBT corresponding to the above transaction.
|
|
let dummy_psbt_a = Psbt {
|
|
unsigned_tx: dummy_tx_a.clone(),
|
|
version: 0,
|
|
xpub: BTreeMap::new(),
|
|
proprietary: BTreeMap::new(),
|
|
unknown: BTreeMap::new(),
|
|
inputs: vec![],
|
|
outputs: vec![],
|
|
};
|
|
let dummy_txid_a = dummy_psbt_a.unsigned_tx.txid();
|
|
dummy_bitcoind.txs.insert(dummy_txid_a, (dummy_tx_a, None));
|
|
let ms = DummyLiana::new(dummy_bitcoind, DummyDatabase::new());
|
|
let control = &ms.control();
|
|
let mut db_conn = control.db().lock().unwrap().connection();
|
|
// The spend needs to be in DB before using RBF.
|
|
assert_eq!(
|
|
control.rbf_psbt(&dummy_txid_a, true, None),
|
|
Err(CommandError::UnknownSpend(dummy_txid_a))
|
|
);
|
|
// Store the spend.
|
|
db_conn.store_spend(&dummy_psbt_a);
|
|
// Now add the coin to DB, but as spent.
|
|
db_conn.new_unspent_coins(&[Coin {
|
|
outpoint: dummy_op_a,
|
|
is_immature: false,
|
|
block_info: Some(BlockInfo {
|
|
height: 174500,
|
|
time: 174500,
|
|
}),
|
|
amount: bitcoin::Amount::from_sat(300_000),
|
|
derivation_index: bip32::ChildNumber::from(11),
|
|
is_change: false,
|
|
spend_txid: Some(dummy_txid_a),
|
|
spend_block: Some(BlockInfo {
|
|
height: 184500,
|
|
time: 184500,
|
|
}),
|
|
}]);
|
|
// The coin is spent so we cannot RBF.
|
|
assert_eq!(
|
|
control.rbf_psbt(&dummy_txid_a, true, None),
|
|
Err(CommandError::AlreadySpent(dummy_op_a))
|
|
);
|
|
db_conn.unspend_coins(&[dummy_op_a]);
|
|
// Now remove the coin.
|
|
db_conn.remove_coins(&[dummy_op_a]);
|
|
assert_eq!(
|
|
control.rbf_psbt(&dummy_txid_a, true, None),
|
|
Err(CommandError::UnknownOutpoint(dummy_op_a))
|
|
);
|
|
// A target feerate not higher than the previous should return an error. This is tested in
|
|
// the functional tests.
|
|
|
|
ms.shutdown();
|
|
}
|
|
|
|
#[test]
|
|
fn list_confirmed_transactions() {
|
|
let outpoint = OutPoint::new(
|
|
Txid::from_str("617eab1fc0b03ee7f82ba70166725291783461f1a0e7975eaf8b5f8f674234f3")
|
|
.unwrap(),
|
|
0,
|
|
);
|
|
|
|
let deposit1: Transaction = Transaction {
|
|
version: TxVersion::ONE,
|
|
lock_time: absolute::LockTime::Blocks(absolute::Height::from_consensus(1).unwrap()),
|
|
input: vec![TxIn {
|
|
witness: Witness::new(),
|
|
previous_output: outpoint,
|
|
script_sig: ScriptBuf::new(),
|
|
sequence: Sequence(0),
|
|
}],
|
|
output: vec![TxOut {
|
|
script_pubkey: ScriptBuf::new(),
|
|
value: Amount::from_sat(100_000_000),
|
|
}],
|
|
};
|
|
|
|
let deposit2: Transaction = Transaction {
|
|
version: TxVersion::ONE,
|
|
lock_time: absolute::LockTime::Blocks(absolute::Height::from_consensus(1).unwrap()),
|
|
input: vec![TxIn {
|
|
witness: Witness::new(),
|
|
previous_output: outpoint,
|
|
script_sig: ScriptBuf::new(),
|
|
sequence: Sequence(0),
|
|
}],
|
|
output: vec![TxOut {
|
|
script_pubkey: ScriptBuf::new(),
|
|
value: Amount::from_sat(2000),
|
|
}],
|
|
};
|
|
|
|
let deposit3: Transaction = Transaction {
|
|
version: TxVersion::ONE,
|
|
lock_time: absolute::LockTime::Blocks(absolute::Height::from_consensus(1).unwrap()),
|
|
input: vec![TxIn {
|
|
witness: Witness::new(),
|
|
previous_output: outpoint,
|
|
script_sig: ScriptBuf::new(),
|
|
sequence: Sequence(0),
|
|
}],
|
|
output: vec![TxOut {
|
|
script_pubkey: ScriptBuf::new(),
|
|
value: Amount::from_sat(3000),
|
|
}],
|
|
};
|
|
|
|
let spend_tx: Transaction = Transaction {
|
|
version: TxVersion::ONE,
|
|
lock_time: absolute::LockTime::Blocks(absolute::Height::from_consensus(1).unwrap()),
|
|
input: vec![TxIn {
|
|
witness: Witness::new(),
|
|
previous_output: OutPoint {
|
|
txid: deposit1.txid(),
|
|
vout: 0,
|
|
},
|
|
script_sig: ScriptBuf::new(),
|
|
sequence: Sequence(0),
|
|
}],
|
|
output: vec![
|
|
TxOut {
|
|
script_pubkey: ScriptBuf::new(),
|
|
value: Amount::from_sat(4000),
|
|
},
|
|
TxOut {
|
|
script_pubkey: ScriptBuf::new(),
|
|
value: Amount::from_sat(100_000_000 - 4000 - 1000),
|
|
},
|
|
],
|
|
};
|
|
|
|
let mut db = DummyDatabase::new();
|
|
db.insert_coins(vec![
|
|
// Deposit 1
|
|
Coin {
|
|
is_change: false,
|
|
is_immature: false,
|
|
outpoint: OutPoint {
|
|
txid: deposit1.txid(),
|
|
vout: 0,
|
|
},
|
|
block_info: Some(BlockInfo { height: 1, time: 1 }),
|
|
spend_block: Some(BlockInfo { height: 3, time: 3 }),
|
|
derivation_index: ChildNumber::from(0),
|
|
amount: bitcoin::Amount::from_sat(100_000_000),
|
|
spend_txid: Some(spend_tx.txid()),
|
|
},
|
|
// Deposit 2
|
|
Coin {
|
|
is_change: false,
|
|
is_immature: false,
|
|
outpoint: OutPoint {
|
|
txid: deposit2.txid(),
|
|
vout: 0,
|
|
},
|
|
block_info: Some(BlockInfo { height: 2, time: 2 }),
|
|
spend_block: None,
|
|
derivation_index: ChildNumber::from(1),
|
|
amount: bitcoin::Amount::from_sat(2000),
|
|
spend_txid: None,
|
|
},
|
|
// This coin is a change output.
|
|
Coin {
|
|
is_change: true,
|
|
is_immature: false,
|
|
outpoint: OutPoint::new(spend_tx.txid(), 1),
|
|
block_info: Some(BlockInfo { height: 3, time: 3 }),
|
|
spend_block: None,
|
|
derivation_index: ChildNumber::from(2),
|
|
amount: bitcoin::Amount::from_sat(100_000_000 - 4000 - 1000),
|
|
spend_txid: None,
|
|
},
|
|
// Deposit 3
|
|
Coin {
|
|
is_change: false,
|
|
is_immature: false,
|
|
outpoint: OutPoint {
|
|
txid: deposit3.txid(),
|
|
vout: 0,
|
|
},
|
|
block_info: Some(BlockInfo { height: 4, time: 4 }),
|
|
spend_block: None,
|
|
derivation_index: ChildNumber::from(3),
|
|
amount: bitcoin::Amount::from_sat(3000),
|
|
spend_txid: None,
|
|
},
|
|
]);
|
|
|
|
let mut btc = DummyBitcoind::new();
|
|
btc.txs.insert(
|
|
deposit1.txid(),
|
|
(
|
|
deposit1.clone(),
|
|
Some(Block {
|
|
hash: bitcoin::BlockHash::from_str(
|
|
"0000000000000000000326b8fca8d3f820647c97ea33ef722096b3c7b2c8ee94",
|
|
)
|
|
.unwrap(),
|
|
time: 1,
|
|
height: 1,
|
|
}),
|
|
),
|
|
);
|
|
btc.txs.insert(
|
|
deposit2.txid(),
|
|
(
|
|
deposit2.clone(),
|
|
Some(Block {
|
|
hash: bitcoin::BlockHash::from_str(
|
|
"0000000000000000000326b8fca8d3f820647c97ea33ef722096b3c7b2c8ee94",
|
|
)
|
|
.unwrap(),
|
|
time: 2,
|
|
height: 2,
|
|
}),
|
|
),
|
|
);
|
|
btc.txs.insert(
|
|
spend_tx.txid(),
|
|
(
|
|
spend_tx.clone(),
|
|
Some(Block {
|
|
hash: bitcoin::BlockHash::from_str(
|
|
"0000000000000000000326b8fca8d3f820647c97ea33ef722096b3c7b2c8ee94",
|
|
)
|
|
.unwrap(),
|
|
time: 3,
|
|
height: 3,
|
|
}),
|
|
),
|
|
);
|
|
btc.txs.insert(
|
|
deposit3.txid(),
|
|
(
|
|
deposit3.clone(),
|
|
Some(Block {
|
|
hash: bitcoin::BlockHash::from_str(
|
|
"0000000000000000000326b8fca8d3f820647c97ea33ef722096b3c7b2c8ee94",
|
|
)
|
|
.unwrap(),
|
|
time: 4,
|
|
height: 4,
|
|
}),
|
|
),
|
|
);
|
|
|
|
let ms = DummyLiana::new(btc, db);
|
|
|
|
let control = &ms.control();
|
|
|
|
let transactions = control.list_confirmed_transactions(0, 4, 10).transactions;
|
|
assert_eq!(transactions.len(), 4);
|
|
|
|
assert_eq!(transactions[0].time, Some(4));
|
|
assert_eq!(transactions[0].tx, deposit3);
|
|
|
|
assert_eq!(transactions[1].time, Some(3));
|
|
assert_eq!(transactions[1].tx, spend_tx);
|
|
|
|
assert_eq!(transactions[2].time, Some(2));
|
|
assert_eq!(transactions[2].tx, deposit2);
|
|
|
|
assert_eq!(transactions[3].time, Some(1));
|
|
assert_eq!(transactions[3].tx, deposit1);
|
|
|
|
let transactions = control.list_confirmed_transactions(2, 3, 10).transactions;
|
|
assert_eq!(transactions.len(), 2);
|
|
|
|
assert_eq!(transactions[0].time, Some(3));
|
|
assert_eq!(transactions[1].time, Some(2));
|
|
assert_eq!(transactions[1].tx, deposit2);
|
|
|
|
let transactions = control.list_confirmed_transactions(2, 3, 1).transactions;
|
|
assert_eq!(transactions.len(), 1);
|
|
|
|
assert_eq!(transactions[0].time, Some(3));
|
|
assert_eq!(transactions[0].tx, spend_tx);
|
|
|
|
ms.shutdown();
|
|
}
|
|
|
|
#[test]
|
|
fn list_transactions() {
|
|
let outpoint = OutPoint::new(
|
|
Txid::from_str("617eab1fc0b03ee7f82ba70166725291783461f1a0e7975eaf8b5f8f674234f3")
|
|
.unwrap(),
|
|
0,
|
|
);
|
|
|
|
let tx1: Transaction = Transaction {
|
|
version: TxVersion::ONE,
|
|
lock_time: absolute::LockTime::Blocks(absolute::Height::from_consensus(1).unwrap()),
|
|
input: vec![TxIn {
|
|
witness: Witness::new(),
|
|
previous_output: outpoint,
|
|
script_sig: ScriptBuf::new(),
|
|
sequence: Sequence(0),
|
|
}],
|
|
output: vec![TxOut {
|
|
script_pubkey: ScriptBuf::new(),
|
|
value: Amount::from_sat(100_000_000),
|
|
}],
|
|
};
|
|
|
|
let tx2: Transaction = Transaction {
|
|
version: TxVersion::ONE,
|
|
lock_time: absolute::LockTime::Blocks(absolute::Height::from_consensus(1).unwrap()),
|
|
input: vec![TxIn {
|
|
witness: Witness::new(),
|
|
previous_output: outpoint,
|
|
script_sig: ScriptBuf::new(),
|
|
sequence: Sequence(0),
|
|
}],
|
|
output: vec![TxOut {
|
|
script_pubkey: ScriptBuf::new(),
|
|
value: Amount::from_sat(2000),
|
|
}],
|
|
};
|
|
|
|
let tx3: Transaction = Transaction {
|
|
version: TxVersion::ONE,
|
|
lock_time: absolute::LockTime::Blocks(absolute::Height::from_consensus(1).unwrap()),
|
|
input: vec![TxIn {
|
|
witness: Witness::new(),
|
|
previous_output: outpoint,
|
|
script_sig: ScriptBuf::new(),
|
|
sequence: Sequence(0),
|
|
}],
|
|
output: vec![TxOut {
|
|
script_pubkey: ScriptBuf::new(),
|
|
value: Amount::from_sat(3000),
|
|
}],
|
|
};
|
|
|
|
let mut btc = DummyBitcoind::new();
|
|
btc.txs.insert(
|
|
tx1.txid(),
|
|
(
|
|
tx1.clone(),
|
|
Some(Block {
|
|
hash: bitcoin::BlockHash::from_str(
|
|
"0000000000000000000326b8fca8d3f820647c97ea33ef722096b3c7b2c8ee94",
|
|
)
|
|
.unwrap(),
|
|
time: 1,
|
|
height: 1,
|
|
}),
|
|
),
|
|
);
|
|
btc.txs.insert(
|
|
tx2.txid(),
|
|
(
|
|
tx2.clone(),
|
|
Some(Block {
|
|
hash: bitcoin::BlockHash::from_str(
|
|
"0000000000000000000326b8fca8d3f820647c97ea33ef722096b3c7b2c8ee94",
|
|
)
|
|
.unwrap(),
|
|
time: 2,
|
|
height: 2,
|
|
}),
|
|
),
|
|
);
|
|
btc.txs.insert(
|
|
tx3.txid(),
|
|
(
|
|
tx3.clone(),
|
|
Some(Block {
|
|
hash: bitcoin::BlockHash::from_str(
|
|
"0000000000000000000326b8fca8d3f820647c97ea33ef722096b3c7b2c8ee94",
|
|
)
|
|
.unwrap(),
|
|
time: 4,
|
|
height: 4,
|
|
}),
|
|
),
|
|
);
|
|
|
|
let ms = DummyLiana::new(btc, DummyDatabase::new());
|
|
|
|
let control = &ms.control();
|
|
|
|
let transactions = control.list_transactions(&[tx1.txid()]).transactions;
|
|
assert_eq!(transactions.len(), 1);
|
|
assert_eq!(transactions[0].tx, tx1);
|
|
|
|
let transactions = control
|
|
.list_transactions(&[tx1.txid(), tx2.txid(), tx3.txid()])
|
|
.transactions;
|
|
assert_eq!(transactions.len(), 3);
|
|
|
|
let txs: Vec<Transaction> = transactions
|
|
.iter()
|
|
.map(|transaction| transaction.tx.clone())
|
|
.collect();
|
|
|
|
assert!(txs.contains(&tx1));
|
|
assert!(txs.contains(&tx2));
|
|
assert!(txs.contains(&tx3));
|
|
|
|
ms.shutdown();
|
|
}
|
|
}
|