Merge #99: command: add listtransactions

c39cb07360e5d3a6f528d4b17f1e34fae0da550b qa: functional test for transaction listing commands (Antoine Poinsot)
1f06c4d4dc4a6c611231c2e95a1e464e038b2570 db: fix the list_txids query (Antoine Poinsot)
774f9695941757253ba34c9a61c09e70d4a3149f Add listtransaction command (edouard)
3cd709697b4c0f098b9938dc122ddf471659dbe9 Add listconfirmed command (edouard)

Pull request description:

ACKs for top commit:
  darosior:
    ACK c39cb07360e5d3a6f528d4b17f1e34fae0da550b

Tree-SHA512: b0e158bb10d0c6fa3fc6beb6e12ea081a8078116733f2407902a9433f1df1fc67b2ee2d73c15d2d5c3bd49ff797ae0aa0b5c68f0684ed22c87f94c318fa39673
This commit is contained in:
Antoine Poinsot 2022-11-22 20:28:54 +01:00
commit dc23f3667a
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
12 changed files with 1084 additions and 93 deletions

View File

@ -5,16 +5,18 @@ interface over a Unix Domain socket.
Commands must be sent as valid JSONRPC 2.0 requests, ending with a `\n`.
| Command | Description |
| ----------------------------------------------------------- | ---------------------------------------------------- |
| [`stop`](#stop) | Stops the minisafe daemon |
| [`getinfo`](#getinfo) | Get general information about the daemon |
| [`getnewaddress`](#getnewaddress) | Get a new receiving address |
| [`listcoins`](#listcoins) | List all wallet transaction outputs. |
| [`listspendtxs`](#listspendtxs) | List all stored Spend transactions |
| [`delspendtx`](#delspendtx) | Delete a stored Spend transaction |
| [`broadcastspend`](#broadcastspend) | Finalize a stored Spend PSBT, and broadcast it |
| [`startrescan`](#startrescan) | Start rescanning the block chain from a given date |
| Command | Description |
| ----------------------------------------------------------- | ---------------------------------------------------- |
| [`stop`](#stop) | Stops the minisafe daemon |
| [`getinfo`](#getinfo) | Get general information about the daemon |
| [`getnewaddress`](#getnewaddress) | Get a new receiving address |
| [`listcoins`](#listcoins) | List all wallet transaction outputs. |
| [`listspendtxs`](#listspendtxs) | List all stored Spend transactions |
| [`delspendtx`](#delspendtx) | Delete a stored Spend transaction |
| [`broadcastspend`](#broadcastspend) | Finalize a stored Spend PSBT, and broadcast it |
| [`startrescan`](#startrescan) | Start rescanning the block chain from a given date |
| [`listconfirmed`](#listconfirmed) | List of confirmed transactions of incoming and outgoing funds |
| [`listtransactions`](#listtransactions) | List of transactions with the given txids |
# Reference
@ -187,7 +189,6 @@ This command does not return anything for now.
| Field | Type | Description |
| -------------- | --------- | ---------------------------------------------------- |
### `broadcastspend`
#### Request
@ -203,7 +204,6 @@ This command does not return anything for now.
| Field | Type | Description |
| -------------- | --------- | ---------------------------------------------------- |
### `startrescan`
#### Request
@ -218,3 +218,46 @@ This command does not return anything for now.
| Field | Type | Description |
| -------------- | --------- | ---------------------------------------------------- |
### `listconfirmed`
`listconfirmed` retrieves a paginated and ordered list of transactions that were confirmed within a given time window.
Confirmation time is based on the timestamp of blocks.
#### Request
| Field | Type | Description |
| ------------- | ------------ | ------------------------------------------ |
| `start` | int | Inclusive lower bound of the time window |
| `end` | int | Inclusive upper bound of the time window |
| `limit` | int | Maximum number of transactions to retrieve |
#### Response
| Field | Type | Description |
| -------------- | ------ | ------------------------------------------------------ |
| `transactions` | array | Array of [Transaction resource](#transaction-resource) |
##### Transaction Resource
| Field | Type | Description |
| -------- | ------------- | ------------------------------------------------------------------------- |
| `height` | int or `null` | Block height of the transaction, `null` if the transaction is unconfirmed |
| `time` | int or `null` | Block time of the transaction, `null` if the transaction is unconfirmed |
| `tx` | string | hex encoded bitcoin transaction |
### `listtransactions`
`listtransactions` retrieves the transactions with the given txids.
#### Request
| Field | Type | Description |
| ------------- | --------------- | ------------------------------------- |
| `txids` | array of string | Ids of the transactions to retrieve |
#### Response
| Field | Type | Description |
| -------------- | ------ | ------------------------------------------------------ |
| `transactions` | array | Array of [Transaction resource](#transaction-resource) |

View File

@ -2,7 +2,11 @@
///!
///! We use the RPC interface and a watchonly descriptor wallet.
mod utils;
use crate::{bitcoin::BlockChainTip, config, descriptors::MultipathDescriptor};
use crate::{
bitcoin::{Block, BlockChainTip},
config,
descriptors::MultipathDescriptor,
};
use utils::block_before_date;
use std::{cmp, collections::HashSet, convert::TryInto, fs, io, str::FromStr, time::Duration};
@ -12,7 +16,11 @@ use jsonrpc::{
client::Client,
simple_http::{self, SimpleHttpTransport},
};
use miniscript::{bitcoin, descriptor};
use miniscript::{
bitcoin::{self, hashes::hex::FromHex},
descriptor,
};
use serde_json::Value as Json;
@ -1035,12 +1043,15 @@ impl From<Json> for LSBlockRes {
#[derive(Debug, Clone)]
pub struct GetTxRes {
pub conflicting_txs: Vec<bitcoin::Txid>,
pub block_height: Option<i32>,
pub block_time: Option<u32>,
pub block: Option<Block>,
pub tx: bitcoin::Transaction,
}
impl From<Json> for GetTxRes {
fn from(json: Json) -> GetTxRes {
let block_hash = json.get("blockhash").and_then(Json::as_str).map(|s| {
bitcoin::BlockHash::from_str(s).expect("Invalid blockhash in `gettransaction` response")
});
let block_height = json
.get("blockheight")
.and_then(Json::as_i64)
@ -1060,11 +1071,21 @@ impl From<Json> for GetTxRes {
})
.collect()
});
let block = match (block_hash, block_height, block_time) {
(Some(hash), Some(height), Some(time)) => Some(Block { hash, time, height }),
_ => None,
};
let hex = json
.get("hex")
.and_then(Json::as_str)
.expect("Must be present in bitcoind response");
let bytes = Vec::from_hex(hex).expect("bitcoind returned a wrong transaction format");
let tx: bitcoin::Transaction = bitcoin::consensus::encode::deserialize(&bytes)
.expect("bitcoind returned a wrong transaction format");
GetTxRes {
conflicting_txs: conflicting_txs.unwrap_or_default(),
block_height,
block_time,
block,
tx,
}
}
}

View File

@ -13,6 +13,14 @@ use std::{collections::HashMap, fmt, sync};
use miniscript::bitcoin;
/// Information about a block
#[derive(Debug, Clone, Eq, PartialEq, Copy)]
pub struct Block {
pub hash: bitcoin::BlockHash,
pub height: i32,
pub time: u32,
}
/// Information about the best block in the chain
#[derive(Debug, Clone, Eq, PartialEq, Copy)]
pub struct BlockChainTip {
@ -66,7 +74,7 @@ pub trait BitcoinInterface: Send {
fn spent_coins(
&self,
outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)],
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid, i32, u32)>;
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid, Block)>;
/// Get the common ancestor between the Bitcoin backend's tip and the given tip.
fn common_ancestor(&self, tip: &BlockChainTip) -> Option<BlockChainTip>;
@ -88,6 +96,12 @@ pub trait BitcoinInterface: Send {
/// Get the last block chain tip with a timestamp below this. Timestamp must be a valid block
/// timestamp.
fn block_before_date(&self, timestamp: u32) -> Option<BlockChainTip>;
/// Get a transaction related to the wallet along with potential confirmation info.
fn wallet_transaction(
&self,
txid: &bitcoin::Txid,
) -> Option<(bitcoin::Transaction, Option<Block>)>;
}
impl BitcoinInterface for d::BitcoinD {
@ -158,10 +172,8 @@ impl BitcoinInterface for d::BitcoinD {
for op in outpoints {
// TODO: batch those calls to gettransaction
if let Some(res) = self.get_transaction(&op.txid) {
if let Some(h) = res.block_height {
if let Some(t) = res.block_time {
confirmed.push((*op, h, t));
}
if let Some(block) = res.block {
confirmed.push((*op, block.height, block.time));
}
} else {
log::error!("Transaction not in wallet for coin '{}'.", op);
@ -200,7 +212,7 @@ impl BitcoinInterface for d::BitcoinD {
fn spent_coins(
&self,
outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)],
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid, i32, u32)> {
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid, Block)> {
let mut spent = Vec::with_capacity(outpoints.len());
let mut cache: HashMap<bitcoin::Txid, Option<d::GetTxRes>> = HashMap::new();
@ -219,15 +231,8 @@ impl BitcoinInterface for d::BitcoinD {
let mut txs_to_cache: Vec<(bitcoin::Txid, Option<d::GetTxRes>)> = Vec::new();
if let Some(tx) = tx {
if let Some(block_height) = tx.block_height {
// TODO: make both block time and height under the same Option.
assert!(tx.block_height.is_some());
spent.push((
*op,
*txid,
block_height,
tx.block_time.expect("Confirmed tx."),
));
if let Some(block) = tx.block {
spent.push((*op, *txid, block));
} else if !tx.conflicting_txs.is_empty() {
for txid in &tx.conflicting_txs {
let tx: Option<&d::GetTxRes> = match cache.get(txid) {
@ -239,13 +244,8 @@ impl BitcoinInterface for d::BitcoinD {
}
};
if let Some(tx) = tx {
if let Some(block_height) = tx.block_height {
spent.push((
*op,
*txid,
block_height,
tx.block_time.expect("Spend is confirmed"),
))
if let Some(block) = tx.block {
spent.push((*op, *txid, block))
}
}
}
@ -309,6 +309,13 @@ impl BitcoinInterface for d::BitcoinD {
let tip = self.chain_tip();
self.get_block_stats(tip.hash).time
}
fn wallet_transaction(
&self,
txid: &bitcoin::Txid,
) -> Option<(bitcoin::Transaction, Option<Block>)> {
self.get_transaction(txid).map(|res| (res.tx, res.block))
}
}
// FIXME: do we need to repeat the entire trait implemenation? Isn't there a nicer way?
@ -354,7 +361,7 @@ impl BitcoinInterface for sync::Arc<sync::Mutex<dyn BitcoinInterface + 'static>>
fn spent_coins(
&self,
outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)],
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid, i32, u32)> {
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid, Block)> {
self.lock().unwrap().spent_coins(outpoints)
}
@ -385,6 +392,13 @@ impl BitcoinInterface for sync::Arc<sync::Mutex<dyn BitcoinInterface + 'static>>
fn tip_time(&self) -> u32 {
self.lock().unwrap().tip_time()
}
fn wallet_transaction(
&self,
txid: &bitcoin::Txid,
) -> Option<(bitcoin::Transaction, Option<Block>)> {
self.lock().unwrap().wallet_transaction(txid)
}
}
// FIXME: We could avoid this type (and all the conversions entailing allocations) if bitcoind

View File

@ -121,7 +121,11 @@ fn update_coins(
.map(|coin| (coin.outpoint, coin.spend_txid.expect("Coin is spending")))
.chain(spending.iter().cloned())
.collect();
let spent = bit.spent_coins(spending_coins.as_slice());
let spent = bit
.spent_coins(spending_coins.as_slice())
.into_iter()
.map(|(oupoint, txid, block)| (oupoint, txid, block.height, block.time))
.collect();
log::debug!("Newly spent coins: {:?}", spent);
UpdatedCoins {

View File

@ -9,7 +9,10 @@ use crate::{
database::{Coin, DatabaseInterface},
descriptors, DaemonControl, VERSION,
};
use utils::{change_index, deser_amount_from_sats, deser_psbt_base64, ser_amount, ser_base64};
use utils::{
change_index, deser_amount_from_sats, deser_base64, deser_hex, ser_amount, ser_base64, ser_hex,
};
use std::{
collections::{BTreeMap, HashMap},
@ -555,6 +558,51 @@ impl DaemonControl {
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 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 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 }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -606,13 +654,13 @@ pub struct ListCoinsResult {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CreateSpendResult {
#[serde(serialize_with = "ser_base64", deserialize_with = "deser_psbt_base64")]
#[serde(serialize_with = "ser_base64", deserialize_with = "deser_base64")]
pub psbt: Psbt,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListSpendEntry {
#[serde(serialize_with = "ser_base64", deserialize_with = "deser_psbt_base64")]
#[serde(serialize_with = "ser_base64", deserialize_with = "deser_base64")]
pub psbt: Psbt,
pub change_index: Option<u32>,
}
@ -622,17 +670,36 @@ 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>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testutils::*;
use crate::{bitcoin::Block, database::SpendBlock, testutils::*};
use bitcoin::{
blockdata::transaction::{TxIn, TxOut},
util::bip32::ChildNumber,
OutPoint, PackedLockTime, Script, Sequence, Transaction, Txid, Witness,
};
use std::str::FromStr;
use bitcoin::util::bip32;
#[test]
fn getinfo() {
let ms = DummyLiana::new();
let ms = DummyLiana::new(DummyBitcoind::new(), DummyDatabase::new());
// We can query getinfo
ms.handle.control.get_info();
ms.shutdown();
@ -640,7 +707,7 @@ mod tests {
#[test]
fn getnewaddress() {
let ms = DummyLiana::new();
let ms = DummyLiana::new(DummyBitcoind::new(), DummyDatabase::new());
let control = &ms.handle.control;
// We can get an address
@ -661,7 +728,7 @@ mod tests {
#[test]
fn create_spend() {
let ms = DummyLiana::new();
let ms = DummyLiana::new(DummyBitcoind::new(), DummyDatabase::new());
let control = &ms.handle.control;
// Arguments sanity checking
@ -777,7 +844,7 @@ mod tests {
#[test]
fn update_spend() {
let ms = DummyLiana::new();
let ms = DummyLiana::new(DummyBitcoind::new(), DummyDatabase::new());
let control = &ms.handle.control;
let mut db_conn = control.db().lock().unwrap().connection();
@ -888,4 +955,353 @@ mod tests {
ms.shutdown();
}
#[test]
fn list_confirmed_transactions() {
let outpoint = OutPoint::new(
Txid::from_str("617eab1fc0b03ee7f82ba70166725291783461f1a0e7975eaf8b5f8f674234f3")
.unwrap(),
0,
);
let deposit1: Transaction = Transaction {
version: 1,
lock_time: PackedLockTime(1),
input: vec![TxIn {
witness: Witness::new(),
previous_output: outpoint,
script_sig: Script::new(),
sequence: Sequence(0),
}],
output: vec![TxOut {
script_pubkey: Script::new(),
value: 100_000_000,
}],
};
let deposit2: Transaction = Transaction {
version: 1,
lock_time: PackedLockTime(1),
input: vec![TxIn {
witness: Witness::new(),
previous_output: outpoint,
script_sig: Script::new(),
sequence: Sequence(0),
}],
output: vec![TxOut {
script_pubkey: Script::new(),
value: 2000,
}],
};
let deposit3: Transaction = Transaction {
version: 1,
lock_time: PackedLockTime(1),
input: vec![TxIn {
witness: Witness::new(),
previous_output: outpoint,
script_sig: Script::new(),
sequence: Sequence(0),
}],
output: vec![TxOut {
script_pubkey: Script::new(),
value: 3000,
}],
};
let spend_tx: Transaction = Transaction {
version: 1,
lock_time: PackedLockTime(1),
input: vec![TxIn {
witness: Witness::new(),
previous_output: OutPoint {
txid: deposit1.txid(),
vout: 0,
},
script_sig: Script::new(),
sequence: Sequence(0),
}],
output: vec![
TxOut {
script_pubkey: Script::new(),
value: 4000,
},
TxOut {
script_pubkey: Script::new(),
value: 100_000_000 - 4000 - 1000,
},
],
};
let mut db = DummyDatabase::new();
db.insert_coins(vec![
// Deposit 1
Coin {
is_change: false,
outpoint: OutPoint {
txid: deposit1.txid(),
vout: 0,
},
block_time: Some(1),
block_height: Some(1),
spend_block: Some(SpendBlock { time: 3, height: 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,
outpoint: OutPoint {
txid: deposit2.txid(),
vout: 0,
},
block_time: Some(2),
block_height: Some(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,
outpoint: OutPoint::new(spend_tx.txid(), 1),
block_time: Some(3),
block_height: Some(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,
outpoint: OutPoint {
txid: deposit3.txid(),
vout: 0,
},
block_time: Some(4),
block_height: Some(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.handle.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: 1,
lock_time: PackedLockTime(1),
input: vec![TxIn {
witness: Witness::new(),
previous_output: outpoint,
script_sig: Script::new(),
sequence: Sequence(0),
}],
output: vec![TxOut {
script_pubkey: Script::new(),
value: 100_000_000,
}],
};
let tx2: Transaction = Transaction {
version: 1,
lock_time: PackedLockTime(1),
input: vec![TxIn {
witness: Witness::new(),
previous_output: outpoint,
script_sig: Script::new(),
sequence: Sequence(0),
}],
output: vec![TxOut {
script_pubkey: Script::new(),
value: 2000,
}],
};
let tx3: Transaction = Transaction {
version: 1,
lock_time: PackedLockTime(1),
input: vec![TxIn {
witness: Witness::new(),
previous_output: outpoint,
script_sig: Script::new(),
sequence: Sequence(0),
}],
output: vec![TxOut {
script_pubkey: Script::new(),
value: 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.handle.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();
}
}

View File

@ -1,6 +1,8 @@
use crate::database::DatabaseConnection;
use miniscript::bitcoin::{self, consensus, util::psbt::PartiallySignedTransaction as Psbt};
use miniscript::bitcoin::{
self, consensus, hashes::hex::FromHex, util::psbt::PartiallySignedTransaction as Psbt,
};
use serde::{de, Deserialize, Deserializer, Serializer};
/// Serialize an amount as sats
@ -25,14 +27,32 @@ where
s.serialize_str(&base64::encode(consensus::serialize(&t)))
}
pub fn deser_psbt_base64<'de, D>(d: D) -> Result<Psbt, D::Error>
pub fn deser_base64<'de, D, T>(d: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: consensus::Decodable,
{
let s = String::deserialize(d)?;
let s = base64::decode(&s).map_err(de::Error::custom)?;
let psbt = consensus::deserialize(&s).map_err(de::Error::custom)?;
Ok(psbt)
consensus::deserialize(&s).map_err(de::Error::custom)
}
pub fn ser_hex<S, T>(t: T, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
T: consensus::Encodable,
{
s.serialize_str(&consensus::encode::serialize_hex(&t))
}
pub fn deser_hex<'de, D, T>(d: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: consensus::Decodable,
{
let s = String::deserialize(d)?;
let s = Vec::from_hex(&s).map_err(de::Error::custom)?;
consensus::deserialize(&s).map_err(de::Error::custom)
}
// Utility to gather the index of a change output in a Psbt, if there is one.

View File

@ -117,6 +117,9 @@ pub trait DatabaseConnection {
/// 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>;
}
impl DatabaseConnection for SqliteConn {
@ -245,6 +248,10 @@ impl DatabaseConnection for SqliteConn {
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)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]

View File

@ -490,6 +490,38 @@ impl SqliteConn {
.expect("Db must not fail")
}
/// Retrieves a limited and ordered list of transactions ids that happened during the given
/// range.
pub fn db_list_txids(&mut self, start: u32, end: u32, limit: u64) -> Vec<bitcoin::Txid> {
db_query(
&mut self.conn,
"SELECT DISTINCT(txid) FROM ( \
SELECT * from ( \
SELECT txid, blocktime AS date FROM coins \
WHERE blocktime >= (?1) \
AND blocktime <= (?2) \
ORDER BY blocktime \
) \
UNION \
SELECT * FROM (
SELECT spend_txid AS txid, spend_block_time AS date FROM coins \
WHERE spend_block_time >= (?1) \
AND spend_block_time <= (?2) \
ORDER BY spend_block_time \
) \
ORDER BY date DESC LIMIT (?3) \
)",
rusqlite::params![start, end, limit],
|row| {
let txid: Vec<u8> = row.get(0)?;
let txid: bitcoin::Txid =
encode::deserialize(&txid).expect("We only store valid txids");
Ok(txid)
},
)
.expect("Db must not fail")
}
pub fn delete_spend(&mut self, txid: &bitcoin::Txid) {
db_exec(&mut self.conn, |db_tx| {
db_tx.execute(
@ -1089,4 +1121,157 @@ mod tests {
fs::remove_dir_all(&tmp_dir).unwrap();
}
#[test]
fn sqlite_list_txids() {
let (tmp_dir, _, _, db) = dummy_db();
{
let mut conn = db.connection().unwrap();
let coins = [
Coin {
outpoint: bitcoin::OutPoint::from_str(
"6f0dc85a369b44458eba3a1f0ea5b5935d563afb6994f70f5b0094e05be1676c:1",
)
.unwrap(),
block_height: None,
block_time: None,
amount: bitcoin::Amount::from_sat(98765),
derivation_index: bip32::ChildNumber::from_normal_idx(10).unwrap(),
is_change: false,
spend_txid: None,
spend_block: None,
},
Coin {
outpoint: bitcoin::OutPoint::from_str(
"c449539458c60bee6c0d8905ba1dadb20b9187b82045d306a408b894cea492b0:2",
)
.unwrap(),
block_height: Some(101_095),
block_time: Some(1_121_000),
amount: bitcoin::Amount::from_sat(98765),
derivation_index: bip32::ChildNumber::from_normal_idx(100).unwrap(),
is_change: false,
spend_txid: None,
spend_block: None,
},
Coin {
outpoint: bitcoin::OutPoint::from_str(
"f0801fd9ca8bca0624c230ab422b2e2c4c8dc995e4e1dbc6412510959cce1e4f:3",
)
.unwrap(),
block_height: Some(101_099),
block_time: Some(1_122_000),
amount: bitcoin::Amount::from_sat(98765),
derivation_index: bip32::ChildNumber::from_normal_idx(1000).unwrap(),
is_change: false,
spend_txid: Some(
bitcoin::Txid::from_str(
"0c62a990d20d54429e70859292e82374ba6b1b951a3ab60f26bb65fee5724ff7",
)
.unwrap(),
),
spend_block: Some(SpendBlock {
height: 101_199,
time: 1_123_000,
}),
},
Coin {
outpoint: bitcoin::OutPoint::from_str(
"19f56e65069f0a7a3bfb00c6a7085cc0669e03e91befeca1ee9891c9e737b2fb:4",
)
.unwrap(),
block_height: Some(101_100),
block_time: Some(1_124_000),
amount: bitcoin::Amount::from_sat(98765),
derivation_index: bip32::ChildNumber::from_normal_idx(10000).unwrap(),
is_change: false,
spend_txid: None,
spend_block: None,
},
Coin {
outpoint: bitcoin::OutPoint::from_str(
"ed6c8f1af9325f84de521e785e7ddfd33dc28c9ada4d687dcd3850100bde54e9:5",
)
.unwrap(),
block_height: Some(101_102),
block_time: Some(1_125_000),
amount: bitcoin::Amount::from_sat(98765),
derivation_index: bip32::ChildNumber::from_normal_idx(100000).unwrap(),
is_change: false,
spend_txid: Some(
bitcoin::Txid::from_str(
"7477017f992cdc7ba08acafb77cb3b5bc0f42ac340d3e1e1da0785bdda20d5f6",
)
.unwrap(),
),
spend_block: Some(SpendBlock {
height: 101_105,
time: 1_126_000,
}),
},
];
conn.new_unspent_coins(&coins);
conn.confirm_coins(
&coins
.iter()
.filter_map(|c| {
c.block_height
.map(|b| (c.outpoint, b, c.block_time.unwrap()))
})
.collect::<Vec<_>>(),
);
conn.confirm_spend(
&coins
.iter()
.filter_map(|c| {
c.spend_block
.as_ref()
.map(|b| (c.outpoint, c.spend_txid.unwrap(), b.height, b.time))
})
.collect::<Vec<_>>(),
);
let db_txids = conn.db_list_txids(1_123_000, 1_127_000, 10);
assert_eq!(
&db_txids[..],
&[
bitcoin::Txid::from_str(
"7477017f992cdc7ba08acafb77cb3b5bc0f42ac340d3e1e1da0785bdda20d5f6"
)
.unwrap(),
bitcoin::Txid::from_str(
"ed6c8f1af9325f84de521e785e7ddfd33dc28c9ada4d687dcd3850100bde54e9"
)
.unwrap(),
bitcoin::Txid::from_str(
"19f56e65069f0a7a3bfb00c6a7085cc0669e03e91befeca1ee9891c9e737b2fb"
)
.unwrap(),
bitcoin::Txid::from_str(
"0c62a990d20d54429e70859292e82374ba6b1b951a3ab60f26bb65fee5724ff7"
)
.unwrap()
]
);
let db_txids = conn.db_list_txids(1_123_000, 1_127_000, 2);
assert_eq!(
&db_txids[..],
&[
bitcoin::Txid::from_str(
"7477017f992cdc7ba08acafb77cb3b5bc0f42ac340d3e1e1da0785bdda20d5f6"
)
.unwrap(),
bitcoin::Txid::from_str(
"ed6c8f1af9325f84de521e785e7ddfd33dc28c9ada4d687dcd3850100bde54e9"
)
.unwrap(),
]
);
}
fs::remove_dir_all(&tmp_dir).unwrap();
}
}

View File

@ -84,6 +84,49 @@ fn broadcast_spend(control: &DaemonControl, params: Params) -> Result<serde_json
Ok(serde_json::json!({}))
}
fn list_confirmed(control: &DaemonControl, params: Params) -> Result<serde_json::Value, Error> {
let start: u32 = params
.get(0, "start")
.ok_or_else(|| Error::invalid_params("Missing 'start' parameter."))?
.as_i64()
.and_then(|i| i.try_into().ok())
.ok_or_else(|| Error::invalid_params("Invalid 'start' parameter."))?;
let end: u32 = params
.get(1, "end")
.ok_or_else(|| Error::invalid_params("Missing 'end' parameter."))?
.as_i64()
.and_then(|i| i.try_into().ok())
.ok_or_else(|| Error::invalid_params("Invalid 'end' parameter."))?;
let limit: u64 = params
.get(2, "limit")
.ok_or_else(|| Error::invalid_params("Missing 'limit' parameter."))?
.as_i64()
.and_then(|i| i.try_into().ok())
.ok_or_else(|| Error::invalid_params("Invalid 'limit' parameter."))?;
Ok(serde_json::json!(&control.list_confirmed_transactions(
start as u32,
end as u32,
limit
)))
}
fn list_transactions(control: &DaemonControl, params: Params) -> Result<serde_json::Value, Error> {
let txids: Vec<bitcoin::Txid> = params
.get(0, "txids")
.ok_or_else(|| Error::invalid_params("Missing 'txids' parameter."))?
.as_array()
.and_then(|arr| {
arr.iter()
.map(|entry| entry.as_str().and_then(|e| bitcoin::Txid::from_str(e).ok()))
.collect()
})
.ok_or_else(|| Error::invalid_params("Invalid 'txids' parameter."))?;
Ok(serde_json::json!(&control.list_transactions(&txids)))
}
fn start_rescan(control: &DaemonControl, params: Params) -> Result<serde_json::Value, Error> {
let timestamp: u32 = params
.get(0, "timestamp")
@ -136,6 +179,22 @@ pub fn handle_request(control: &DaemonControl, req: Request) -> Result<Response,
.ok_or_else(|| Error::invalid_params("Missing 'psbt' parameter."))?;
update_spend(control, params)?
}
"listconfirmed" => {
let params = req.params.ok_or_else(|| {
Error::invalid_params(
"The 'listconfirmed' command requires 3 parameters: 'start', 'end' and 'limit'",
)
})?;
list_confirmed(control, params)?
}
"listtransactions" => {
let params = req.params.ok_or_else(|| {
Error::invalid_params(
"The 'listtransactions' command requires 1 parameter: 'txids'",
)
})?;
list_transactions(control, params)?
}
_ => {
return Err(Error::method_not_found());
}

View File

@ -400,7 +400,7 @@ mod tests {
#[cfg(not(target_os = "macos"))]
#[test]
fn server_sanity_check() {
let ms = DummyLiana::new();
let ms = DummyLiana::new(DummyBitcoind::new(), DummyDatabase::new());
let socket_path: path::PathBuf = [
ms.tmp_dir.as_path(),
path::Path::new("d"),

View File

@ -1,5 +1,5 @@
use crate::{
bitcoin::{BitcoinInterface, BlockChainTip, UTxO},
bitcoin::{BitcoinInterface, Block, BlockChainTip, UTxO},
config::{BitcoinConfig, Config},
database::{Coin, DatabaseConnection, DatabaseInterface, SpendBlock},
descriptors, DaemonHandle,
@ -11,11 +11,24 @@ use miniscript::{
bitcoin::{
self, secp256k1,
util::{bip32, psbt::PartiallySignedTransaction as Psbt},
Transaction, Txid,
},
descriptor,
};
pub struct DummyBitcoind {}
pub struct DummyBitcoind {
pub txs: HashMap<Txid, (Transaction, Option<Block>)>,
}
impl DummyBitcoind {}
impl DummyBitcoind {
pub fn new() -> Self {
Self {
txs: HashMap::new(),
}
}
}
impl BitcoinInterface for DummyBitcoind {
fn genesis_block(&self) -> BlockChainTip {
@ -63,7 +76,7 @@ impl BitcoinInterface for DummyBitcoind {
fn spent_coins(
&self,
_: &[(bitcoin::OutPoint, bitcoin::Txid)],
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid, i32, u32)> {
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid, Block)> {
Vec::new()
}
@ -90,9 +103,16 @@ impl BitcoinInterface for DummyBitcoind {
fn tip_time(&self) -> u32 {
todo!()
}
fn wallet_transaction(
&self,
txid: &bitcoin::Txid,
) -> Option<(bitcoin::Transaction, Option<Block>)> {
self.txs.get(txid).cloned()
}
}
pub struct DummyDb {
struct DummyDbState {
deposit_index: bip32::ChildNumber,
change_index: bip32::ChildNumber,
curr_tip: Option<BlockChainTip>,
@ -100,35 +120,39 @@ pub struct DummyDb {
spend_txs: HashMap<bitcoin::Txid, Psbt>,
}
impl DummyDb {
pub fn new() -> DummyDb {
DummyDb {
deposit_index: 0.into(),
change_index: 0.into(),
curr_tip: None,
coins: HashMap::new(),
spend_txs: HashMap::new(),
pub struct DummyDatabase {
db: sync::Arc<sync::RwLock<DummyDbState>>,
}
impl DatabaseInterface for DummyDatabase {
fn connection(&self) -> Box<dyn DatabaseConnection> {
Box::new(DummyDatabase {
db: self.db.clone(),
})
}
}
impl DummyDatabase {
pub fn new() -> DummyDatabase {
DummyDatabase {
db: sync::Arc::new(sync::RwLock::new(DummyDbState {
deposit_index: 0.into(),
change_index: 0.into(),
curr_tip: None,
coins: HashMap::new(),
spend_txs: HashMap::new(),
})),
}
}
pub fn insert_coins(&mut self, coins: Vec<Coin>) {
for coin in coins {
self.db.write().unwrap().coins.insert(coin.outpoint, coin);
}
}
}
impl Default for DummyDb {
fn default() -> DummyDb {
DummyDb::new()
}
}
impl DatabaseInterface for sync::Arc<sync::RwLock<DummyDb>> {
fn connection(&self) -> Box<dyn DatabaseConnection> {
Box::new(DummyDbConn { db: self.clone() })
}
}
pub struct DummyDbConn {
db: sync::Arc<sync::RwLock<DummyDb>>,
}
impl DatabaseConnection for DummyDbConn {
impl DatabaseConnection for DummyDatabase {
fn network(&mut self) -> bitcoin::Network {
bitcoin::Network::Bitcoin
}
@ -284,6 +308,35 @@ impl DatabaseConnection for DummyDbConn {
fn complete_rescan(&mut self) {
todo!()
}
fn list_txids(&mut self, start: u32, end: u32, limit: u64) -> Vec<bitcoin::Txid> {
let mut txids_and_time = Vec::new();
let coins = &self.db.read().unwrap().coins;
// Get txid and block time of every transactions that happened between start and end
// timestamps.
for coin in coins.values() {
if let Some(time) = coin.block_time {
if time >= start && time <= end {
let row = (coin.outpoint.txid, time);
if !txids_and_time.contains(&row) {
txids_and_time.push(row);
}
}
}
if let Some(time) = coin.spend_block.map(|b| b.time) {
if time >= start && time <= end {
let row = (coin.spend_txid.expect("spent_at is not none"), time);
if !txids_and_time.contains(&row) {
txids_and_time.push(row);
}
}
}
}
// Apply order and limit
txids_and_time.sort_by(|(_, t1), (_, t2)| t2.cmp(t1));
txids_and_time.truncate(limit as usize);
txids_and_time.into_iter().map(|(txid, _)| txid).collect()
}
}
pub struct DummyLiana {
@ -310,7 +363,11 @@ pub fn tmp_dir() -> path::PathBuf {
}
impl DummyLiana {
pub fn new() -> DummyLiana {
/// Creates a new DummyLiana interface
pub fn new(
bitcoin_interface: impl BitcoinInterface + 'static,
database: impl DatabaseInterface + 'static,
) -> DummyLiana {
let tmp_dir = tmp_dir();
fs::create_dir_all(&tmp_dir).unwrap();
// Use a shorthand for 'datadir', to avoid overflowing SUN_LEN on MacOS.
@ -336,8 +393,7 @@ impl DummyLiana {
main_descriptor: desc,
};
let db = sync::Arc::from(sync::RwLock::from(DummyDb::new()));
let handle = DaemonHandle::start(config, Some(DummyBitcoind {}), Some(db)).unwrap();
let handle = DaemonHandle::start(config, Some(bitcoin_interface), Some(database)).unwrap();
DummyLiana { tmp_dir, handle }
}

View File

@ -1,7 +1,5 @@
import os
import pytest
import random
import shutil
import time
from fixtures import *
@ -47,9 +45,7 @@ def test_listcoins(lianad, bitcoind):
# If the coin gets confirmed, it'll be marked as such.
bitcoind.generate_block(1, wait_for_mempool=txid)
block_height = bitcoind.rpc.getblockcount()
wait_for(
lambda: lianad.rpc.listcoins()["coins"][0]["block_height"] == block_height
)
wait_for(lambda: lianad.rpc.listcoins()["coins"][0]["block_height"] == block_height)
# Same if the coin gets spent.
spend_tx = spend_coins(lianad, bitcoind, (res[0],))
@ -356,3 +352,173 @@ def test_start_rescan(lianad, bitcoind):
# Now that it caught up it noticed which one were used onchain, so it won't reuse
# this derivation indexes anymore.
assert lianad.rpc.getnewaddress() not in (first_address, second_address)
def test_listtransactions(lianad, bitcoind):
"""Test listing of transactions by txid and timespan"""
def sign_and_broadcast(psbt):
txid = psbt.tx.txid().hex()
psbt = lianad.sign_psbt(psbt)
lianad.rpc.updatespend(psbt.to_base64())
lianad.rpc.broadcastspend(txid)
return txid
def wait_synced():
wait_for(
lambda: lianad.rpc.getinfo()["blockheight"] == bitcoind.rpc.getblockcount()
)
best_block = bitcoind.rpc.getbestblockhash()
initial_timestamp = bitcoind.rpc.getblockheader(best_block)["time"]
wait_synced()
# Deposit multiple coins in a single transaction
destinations = {
lianad.rpc.getnewaddress()["address"]: 0.0123456,
lianad.rpc.getnewaddress()["address"]: 0.0123457,
lianad.rpc.getnewaddress()["address"]: 0.0123458,
}
txid = bitcoind.rpc.sendmany("", destinations)
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 3)
bitcoind.generate_block(1, wait_for_mempool=txid)
# Mine 12 blocks to force the blocktime to increase
bitcoind.generate_block(12)
wait_synced()
best_block = bitcoind.rpc.getbestblockhash()
second_timestamp = bitcoind.rpc.getblockheader(best_block)["time"]
assert second_timestamp > initial_timestamp
# Deposit a coin that will be unspent
addr = lianad.rpc.getnewaddress()["address"]
txid = bitcoind.rpc.sendtoaddress(addr, 0.123456)
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 4)
bitcoind.generate_block(1, wait_for_mempool=txid)
# Deposit a coin that will be spent with a change output
addr = lianad.rpc.getnewaddress()["address"]
txid = bitcoind.rpc.sendtoaddress(addr, 0.23456)
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 5)
bitcoind.generate_block(1, wait_for_mempool=txid)
outpoint = next(
c["outpoint"] for c in lianad.rpc.listcoins()["coins"] if txid in c["outpoint"]
)
destinations = {
bitcoind.rpc.getnewaddress(): 100_000,
}
res = lianad.rpc.createspend(destinations, [outpoint], 6)
psbt = PSBT.from_base64(res["psbt"])
txid = sign_and_broadcast(psbt)
bitcoind.generate_block(1, wait_for_mempool=txid)
# Mine 12 blocks to force the blocktime to increase
bitcoind.generate_block(12)
wait_synced()
best_block = bitcoind.rpc.getbestblockhash()
third_timestamp = bitcoind.rpc.getblockheader(best_block)["time"]
assert third_timestamp > second_timestamp
bitcoind.generate_block(12)
wait_synced()
# Deposit a coin that will be spent with a change output and also two new deposits
addr = lianad.rpc.getnewaddress()["address"]
txid = bitcoind.rpc.sendtoaddress(addr, 0.3456)
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 7)
bitcoind.generate_block(1, wait_for_mempool=txid)
outpoint = next(
c["outpoint"] for c in lianad.rpc.listcoins()["coins"] if txid in c["outpoint"]
)
destinations = {
bitcoind.rpc.getnewaddress(): 11_000,
addr: 12_000, # Even with address reuse! Booooh
lianad.rpc.getnewaddress()["address"]: 13_000,
}
res = lianad.rpc.createspend(destinations, [outpoint], 6)
psbt = PSBT.from_base64(res["psbt"])
txid = sign_and_broadcast(psbt)
bitcoind.generate_block(1, wait_for_mempool=txid)
# Deposit a coin that will be spending (unconfirmed spend transaction)
addr = lianad.rpc.getnewaddress()["address"]
txid = bitcoind.rpc.sendtoaddress(addr, 0.456)
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 11)
bitcoind.generate_block(1, wait_for_mempool=txid)
outpoint = next(
c["outpoint"] for c in lianad.rpc.listcoins()["coins"] if txid in c["outpoint"]
)
destinations = {
bitcoind.rpc.getnewaddress(): 11_000,
}
res = lianad.rpc.createspend(destinations, [outpoint], 6)
psbt = PSBT.from_base64(res["psbt"])
txid = sign_and_broadcast(psbt)
# At this point we have 12 spent and unspent coins, one of them is unconfirmed.
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 12)
# However some of them share the same txid! This is the case of the 3 first coins
# for instance, or the Spend transactions with multiple outputs at one of our addresses.
# In total, that's 8 transactions.
txids = set(c["outpoint"][:-2] for c in lianad.rpc.listcoins()["coins"])
assert len(txids) == 8
# We can query all of them at once using listtransactions. The result contains all
# the correct transactions as hex, with no duplicate.
all_txs = lianad.rpc.listtransactions(list(txids))["transactions"]
assert len(all_txs) == 8
for tx in all_txs:
txid = bitcoind.rpc.decoderawtransaction(tx["tx"])["txid"]
txids.remove(txid) # This will raise an error if it isn't there
# We can also query them one by one.
txids = set(c["outpoint"][:-2] for c in lianad.rpc.listcoins()["coins"])
for txid in txids:
txs = lianad.rpc.listtransactions([txid])["transactions"]
bit_txid = bitcoind.rpc.decoderawtransaction(txs[0]["tx"])["txid"]
assert bit_txid == txid
# We can query all confirmed transactions
best_block = bitcoind.rpc.getbestblockhash()
final_timestamp = bitcoind.rpc.getblockheader(best_block)["time"]
txs = lianad.rpc.listconfirmed(initial_timestamp, final_timestamp, 10)[
"transactions"
]
assert len(txs) == 7, "The last spend tx is unconfirmed"
for tx in txs:
txid = bitcoind.rpc.decoderawtransaction(tx["tx"])["txid"]
txids.remove(txid) # This will raise an error if it isn't there
# We can limit the size of the result
txs = lianad.rpc.listconfirmed(initial_timestamp, final_timestamp, 5)[
"transactions"
]
assert len(txs) == 5
# We can restrict the query to a certain time window.
# First get the txid of all the transactions that happened during this timespan.
txids = set()
for coin in lianad.rpc.listcoins()["coins"]:
if coin["block_height"] is None:
continue
block_hash = bitcoind.rpc.getblockhash(coin["block_height"])
block_time = bitcoind.rpc.getblockheader(block_hash)["time"]
spend_time = None
if coin["spend_info"] is not None and coin["spend_info"]["height"] is not None:
spend_bhash = bitcoind.rpc.getblockhash(coin["spend_info"]["height"])
spend_time = bitcoind.rpc.getblockheader(spend_bhash)["time"]
if (block_time >= second_timestamp and block_time <= third_timestamp) or (
spend_time is not None
and spend_time >= second_timestamp
and spend_time <= third_timestamp
):
txids.add(coin["outpoint"][:-2])
# It's all 7 minus the first deposit and the last confirmed spend. So that's 5 of them.
assert len(txids) == 3
# Now let's compare with what lianad is giving us.
txs = lianad.rpc.listconfirmed(second_timestamp, third_timestamp, 10)[
"transactions"
]
assert len(txs) == 3
bit_txids = set(bitcoind.rpc.decoderawtransaction(tx["tx"])["txid"] for tx in txs)
assert bit_txids == txids