Merge #700: commands: optionally filter listcoins by status and outpoint
36e04edc8207df2b3b3c9fac78f11700514ee2bd db: update docstring for spend_coins (jp1ac4) c492c51f26b55cc2fd1558bba2ea8a0ab2638d3f db: use coins() with filter in list_spending_coins() (jp1ac4) eeaf90e5223049ee89832b5aa4a28a85d6c0f38e commands: optionally filter `listcoins` by status and/or outpoint (jp1ac4) Pull request description: This is to resolve #676. I've renamed `CoinType` to `CoinStatus` and updated its values. The `listcoins` command and related functions can be optionally filtered by coin status and/or outpoint. Opening this as draft to check that I'm on the right track. Remaining tasks: - [x] Update tests - [x] Update API doc - [x] Possibly use the updated functions elsewhere, e.g. `list_spending_coins()` ACKs for top commit: darosior: ACK 36e04edc8207df2b3b3c9fac78f11700514ee2bd Tree-SHA512: 3f5692cd92c2b2011f845adb09523e86b5ea7b13a8bcf42452241b2e6229441c1d46efceb6138446cd7ddb083b3a8c9c2043b058f5caf691768f2f4b472e0fd0
This commit is contained in:
commit
3a59790530
14
doc/API.md
14
doc/API.md
@ -81,14 +81,20 @@ This command does not take any parameter for now.
|
|||||||
|
|
||||||
### `listcoins`
|
### `listcoins`
|
||||||
|
|
||||||
List all our transaction outputs, regardless of their state (unspent or not).
|
List all our transaction outputs, optionally filtered by status and/or outpoint.
|
||||||
|
|
||||||
#### Request
|
#### Request
|
||||||
|
|
||||||
This command does not take any parameter for now.
|
| Field | Type | Description |
|
||||||
|
| -------------- | ----------------- | ----------------------------------------------------------------- |
|
||||||
|
| `statuses` | list of string | List of statuses to filter coins by (see below). |
|
||||||
|
| `outpoints` | list of string | List of outpoints to filter coins by, as `txid:vout`. |
|
||||||
|
|
||||||
| Field | Type | Description |
|
A coin may have one of the following four statuses:
|
||||||
| ------------- | ----------------- | ----------------------------------------------------------- |
|
- `unconfirmed`: deposit transaction has not yet been included in a block and coin has not been included in a spend transaction
|
||||||
|
- `confirmed`: deposit transaction has been included in a block and coin has not been included in a spend transaction
|
||||||
|
- `spending`: coin (whose deposit transaction may not yet have been confirmed) has been included in an unconfirmed spend transaction
|
||||||
|
- `spent`: coin has been included in a confirmed spend transaction
|
||||||
|
|
||||||
#### Response
|
#### Response
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
bitcoin::{BitcoinInterface, BlockChainTip, UTxO},
|
bitcoin::{BitcoinInterface, BlockChainTip, UTxO},
|
||||||
database::{Coin, CoinType, DatabaseConnection, DatabaseInterface},
|
database::{Coin, DatabaseConnection, DatabaseInterface},
|
||||||
descriptors,
|
descriptors,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ fn update_coins(
|
|||||||
secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
|
secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
|
||||||
) -> UpdatedCoins {
|
) -> UpdatedCoins {
|
||||||
let network = db_conn.network();
|
let network = db_conn.network();
|
||||||
let curr_coins = db_conn.coins(CoinType::All);
|
let curr_coins = db_conn.coins(&[], &[]);
|
||||||
log::debug!("Current coins: {:?}", curr_coins);
|
log::debug!("Current coins: {:?}", curr_coins);
|
||||||
|
|
||||||
// Start by fetching newly received coins.
|
// Start by fetching newly received coins.
|
||||||
|
|||||||
@ -6,11 +6,11 @@ mod utils;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
bitcoin::BitcoinInterface,
|
bitcoin::BitcoinInterface,
|
||||||
database::{Coin, CoinType, DatabaseInterface},
|
database::{Coin, DatabaseInterface},
|
||||||
descriptors, DaemonControl, VERSION,
|
descriptors, DaemonControl, VERSION,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use crate::database::LabelItem;
|
pub use crate::database::{CoinStatus, LabelItem};
|
||||||
|
|
||||||
use utils::{
|
use utils::{
|
||||||
deser_addr_assume_checked, deser_amount_from_sats, deser_fromstr, deser_hex, ser_amount,
|
deser_addr_assume_checked, deser_amount_from_sats, deser_fromstr, deser_hex, ser_amount,
|
||||||
@ -289,11 +289,15 @@ impl DaemonControl {
|
|||||||
GetAddressResult::new(address)
|
GetAddressResult::new(address)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a list of all known coins.
|
/// Get a list of all known coins, optionally by status and/or outpoint.
|
||||||
pub fn list_coins(&self) -> ListCoinsResult {
|
pub fn list_coins(
|
||||||
|
&self,
|
||||||
|
statuses: &[CoinStatus],
|
||||||
|
outpoints: &[bitcoin::OutPoint],
|
||||||
|
) -> ListCoinsResult {
|
||||||
let mut db_conn = self.db.connection();
|
let mut db_conn = self.db.connection();
|
||||||
let coins: Vec<ListCoinsEntry> = db_conn
|
let coins: Vec<ListCoinsEntry> = db_conn
|
||||||
.coins(CoinType::All)
|
.coins(statuses, outpoints)
|
||||||
.into_values()
|
.into_values()
|
||||||
.map(|coin| {
|
.map(|coin| {
|
||||||
let Coin {
|
let Coin {
|
||||||
@ -747,12 +751,15 @@ impl DaemonControl {
|
|||||||
let timelock =
|
let timelock =
|
||||||
timelock.unwrap_or_else(|| self.config.main_descriptor.first_timelock_value());
|
timelock.unwrap_or_else(|| self.config.main_descriptor.first_timelock_value());
|
||||||
let height_delta: i32 = timelock.try_into().expect("Must fit, it's a u16");
|
let height_delta: i32 = timelock.try_into().expect("Must fit, it's a u16");
|
||||||
let sweepable_coins = db_conn.coins(CoinType::Unspent).into_values().filter(|c| {
|
let sweepable_coins = db_conn
|
||||||
// We are interested in coins available at the *next* block
|
.coins(&[CoinStatus::Unconfirmed, CoinStatus::Confirmed], &[])
|
||||||
c.block_info
|
.into_values()
|
||||||
.map(|b| current_height + 1 >= b.height + height_delta)
|
.filter(|c| {
|
||||||
.unwrap_or(false)
|
// We are interested in coins available at the *next* block
|
||||||
});
|
c.block_info
|
||||||
|
.map(|b| current_height + 1 >= b.height + height_delta)
|
||||||
|
.unwrap_or(false)
|
||||||
|
});
|
||||||
|
|
||||||
// Fill-in the transaction inputs and PSBT inputs information. Record the value
|
// Fill-in the transaction inputs and PSBT inputs information. Record the value
|
||||||
// that is fed to the transaction while doing so, to compute the fees afterward.
|
// that is fed to the transaction while doing so, to compute the fees afterward.
|
||||||
|
|||||||
@ -85,7 +85,11 @@ pub trait DatabaseConnection {
|
|||||||
) -> Option<(bip32::ChildNumber, bool)>;
|
) -> Option<(bip32::ChildNumber, bool)>;
|
||||||
|
|
||||||
/// Get all our coins, past or present, spent or not.
|
/// Get all our coins, past or present, spent or not.
|
||||||
fn coins(&mut self, coin_type: CoinType) -> HashMap<bitcoin::OutPoint, Coin>;
|
fn coins(
|
||||||
|
&mut self,
|
||||||
|
statuses: &[CoinStatus],
|
||||||
|
outpoints: &[bitcoin::OutPoint],
|
||||||
|
) -> HashMap<bitcoin::OutPoint, Coin>;
|
||||||
|
|
||||||
/// List coins that are being spent and whose spending transaction is still unconfirmed.
|
/// List coins that are being spent and whose spending transaction is still unconfirmed.
|
||||||
fn list_spending_coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin>;
|
fn list_spending_coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin>;
|
||||||
@ -192,8 +196,12 @@ impl DatabaseConnection for SqliteConn {
|
|||||||
self.complete_wallet_rescan()
|
self.complete_wallet_rescan()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn coins(&mut self, coin_type: CoinType) -> HashMap<bitcoin::OutPoint, Coin> {
|
fn coins(
|
||||||
self.coins(coin_type)
|
&mut self,
|
||||||
|
statuses: &[CoinStatus],
|
||||||
|
outpoints: &[bitcoin::OutPoint],
|
||||||
|
) -> HashMap<bitcoin::OutPoint, Coin> {
|
||||||
|
self.coins(statuses, outpoints)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|db_coin| (db_coin.outpoint, db_coin.into()))
|
.map(|db_coin| (db_coin.outpoint, db_coin.into()))
|
||||||
.collect()
|
.collect()
|
||||||
@ -348,13 +356,31 @@ impl Coin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Possible (mutually exclusive) status of a coin.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum CoinType {
|
pub enum CoinStatus {
|
||||||
All,
|
/// Has not yet been included in a block and has no spend transaction.
|
||||||
Unspent,
|
Unconfirmed,
|
||||||
|
/// Has been included in a block and has no spend transaction.
|
||||||
|
Confirmed,
|
||||||
|
/// Has an unconfirmed spend transaction, but coin itself may not yet have been included in a block.
|
||||||
|
Spending,
|
||||||
|
/// Has a confirmed spend transaction.
|
||||||
Spent,
|
Spent,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl CoinStatus {
|
||||||
|
pub fn from_arg(s: &str) -> Option<CoinStatus> {
|
||||||
|
match s {
|
||||||
|
"unconfirmed" => Some(CoinStatus::Unconfirmed),
|
||||||
|
"confirmed" => Some(CoinStatus::Confirmed),
|
||||||
|
"spending" => Some(CoinStatus::Spending),
|
||||||
|
"spent" => Some(CoinStatus::Spent),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub enum LabelItem {
|
pub enum LabelItem {
|
||||||
Address(bitcoin::Address),
|
Address(bitcoin::Address),
|
||||||
|
|||||||
@ -23,7 +23,7 @@ use crate::{
|
|||||||
maybe_apply_migration, LOOK_AHEAD_LIMIT,
|
maybe_apply_migration, LOOK_AHEAD_LIMIT,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Coin, CoinType, LabelItem,
|
Coin, CoinStatus, LabelItem,
|
||||||
},
|
},
|
||||||
descriptors::LianaDescriptor,
|
descriptors::LianaDescriptor,
|
||||||
};
|
};
|
||||||
@ -357,30 +357,72 @@ impl SqliteConn {
|
|||||||
.expect("Database must be available");
|
.expect("Database must be available");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all the coins from DB.
|
/// Get all the coins from DB, optionally filtered by coin status and/or outpoint.
|
||||||
pub fn coins(&mut self, coin_type: CoinType) -> Vec<DbCoin> {
|
pub fn coins(
|
||||||
db_query(
|
&mut self,
|
||||||
&mut self.conn,
|
statuses: &[CoinStatus],
|
||||||
match coin_type {
|
outpoints: &[bitcoin::OutPoint],
|
||||||
CoinType::All => "SELECT * FROM coins",
|
) -> Vec<DbCoin> {
|
||||||
CoinType::Unspent => "SELECT * FROM coins WHERE spend_txid IS NULL",
|
let status_condition = statuses
|
||||||
CoinType::Spent => "SELECT * FROM coins WHERE spend_txid IS NOT NULL",
|
.iter()
|
||||||
},
|
.map(|c| {
|
||||||
rusqlite::params![],
|
format!(
|
||||||
|row| row.try_into(),
|
"({})",
|
||||||
)
|
match c {
|
||||||
|
CoinStatus::Unconfirmed => {
|
||||||
|
"blocktime IS NULL AND spend_txid IS NULL"
|
||||||
|
}
|
||||||
|
CoinStatus::Confirmed => {
|
||||||
|
"blocktime IS NOT NULL AND spend_txid IS NULL"
|
||||||
|
}
|
||||||
|
CoinStatus::Spending => {
|
||||||
|
"spend_txid IS NOT NULL AND spend_block_time IS NULL"
|
||||||
|
}
|
||||||
|
CoinStatus::Spent => "spend_block_time IS NOT NULL",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(" OR ");
|
||||||
|
// SELECT * FROM coins WHERE (txid, vout) IN ((txidA, voutA), (txidB, voutB));
|
||||||
|
let op_condition = if !outpoints.is_empty() {
|
||||||
|
let mut cond = "(txid, vout) IN (VALUES ".to_string();
|
||||||
|
for (i, outpoint) in outpoints.iter().enumerate() {
|
||||||
|
// NOTE: SQLite doesn't know Satoshi decided txids would be displayed as little-endian
|
||||||
|
// hex.
|
||||||
|
cond += &format!(
|
||||||
|
"(x'{}', {})",
|
||||||
|
FrontwardHexTxid(outpoint.txid),
|
||||||
|
outpoint.vout
|
||||||
|
);
|
||||||
|
if i != outpoints.len() - 1 {
|
||||||
|
cond += ", ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cond += ")";
|
||||||
|
cond
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
let where_clause = if !status_condition.is_empty() && !op_condition.is_empty() {
|
||||||
|
format!(" WHERE ({}) AND ({})", status_condition, op_condition)
|
||||||
|
} else if status_condition.is_empty() && !op_condition.is_empty() {
|
||||||
|
format!(" WHERE {}", op_condition)
|
||||||
|
} else if !status_condition.is_empty() && op_condition.is_empty() {
|
||||||
|
format!(" WHERE {}", status_condition)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
let query = format!("SELECT * FROM coins{}", where_clause);
|
||||||
|
db_query(&mut self.conn, &query, rusqlite::params![], |row| {
|
||||||
|
row.try_into()
|
||||||
|
})
|
||||||
.expect("Db must not fail")
|
.expect("Db must not fail")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List coins that are being spent and whose spending transaction is still unconfirmed.
|
/// List coins that are being spent and whose spending transaction is still unconfirmed.
|
||||||
pub fn list_spending_coins(&mut self) -> Vec<DbCoin> {
|
pub fn list_spending_coins(&mut self) -> Vec<DbCoin> {
|
||||||
db_query(
|
self.coins(&[CoinStatus::Spending], &[])
|
||||||
&mut self.conn,
|
|
||||||
"SELECT * FROM coins WHERE spend_txid IS NOT NULL AND spend_block_time IS NULL",
|
|
||||||
rusqlite::params![],
|
|
||||||
|row| row.try_into(),
|
|
||||||
)
|
|
||||||
.expect("Db must not fail")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: don't take the whole coin, we don't need it.
|
// FIXME: don't take the whole coin, we don't need it.
|
||||||
@ -445,7 +487,7 @@ impl SqliteConn {
|
|||||||
.expect("Database must be available")
|
.expect("Database must be available")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mark a set of coins as spent.
|
/// Mark a set of coins as spending.
|
||||||
pub fn spend_coins<'a>(
|
pub fn spend_coins<'a>(
|
||||||
&mut self,
|
&mut self,
|
||||||
outpoints: impl IntoIterator<Item = &'a (bitcoin::OutPoint, bitcoin::Txid)>,
|
outpoints: impl IntoIterator<Item = &'a (bitcoin::OutPoint, bitcoin::Txid)>,
|
||||||
@ -504,26 +546,7 @@ impl SqliteConn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn db_coins(&mut self, outpoints: &[bitcoin::OutPoint]) -> Vec<DbCoin> {
|
pub fn db_coins(&mut self, outpoints: &[bitcoin::OutPoint]) -> Vec<DbCoin> {
|
||||||
// SELECT * FROM coins WHERE (txid, vout) IN ((txidA, voutA), (txidB, voutB));
|
self.coins(&[], outpoints)
|
||||||
let mut query = "SELECT * FROM coins WHERE (txid, vout) IN (VALUES ".to_string();
|
|
||||||
for (i, outpoint) in outpoints.iter().enumerate() {
|
|
||||||
// NOTE: SQLite doesn't know Satoshi decided txids would be displayed as little-endian
|
|
||||||
// hex.
|
|
||||||
query += &format!(
|
|
||||||
"(x'{}', {})",
|
|
||||||
FrontwardHexTxid(outpoint.txid),
|
|
||||||
outpoint.vout
|
|
||||||
);
|
|
||||||
if i != outpoints.len() - 1 {
|
|
||||||
query += ", ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
query += ")";
|
|
||||||
|
|
||||||
db_query(&mut self.conn, &query, rusqlite::params![], |row| {
|
|
||||||
row.try_into()
|
|
||||||
})
|
|
||||||
.expect("Db must not fail")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn db_spend(&mut self, txid: &bitcoin::Txid) -> Option<DbSpendTransaction> {
|
pub fn db_spend(&mut self, txid: &bitcoin::Txid) -> Option<DbSpendTransaction> {
|
||||||
@ -889,6 +912,315 @@ CREATE TABLE spend_transactions (
|
|||||||
fs::remove_dir_all(tmp_dir).unwrap();
|
fs::remove_dir_all(tmp_dir).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn db_coins() {
|
||||||
|
let (tmp_dir, _, _, db) = dummy_db();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut conn = db.connection().unwrap();
|
||||||
|
|
||||||
|
// Necessarily empty at first.
|
||||||
|
assert!(conn.coins(&[], &[]).is_empty());
|
||||||
|
|
||||||
|
// Add one unconfirmed coin.
|
||||||
|
let outpoint_a = bitcoin::OutPoint::from_str(
|
||||||
|
"6f0dc85a369b44458eba3a1f0ea5b5935d563afb6994f70f5b0094e05be1676c:1",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let coin_a = Coin {
|
||||||
|
outpoint: outpoint_a,
|
||||||
|
is_immature: false,
|
||||||
|
block_info: None,
|
||||||
|
amount: bitcoin::Amount::from_sat(10000),
|
||||||
|
derivation_index: bip32::ChildNumber::from_normal_idx(10).unwrap(),
|
||||||
|
is_change: false,
|
||||||
|
spend_txid: None,
|
||||||
|
spend_block: None,
|
||||||
|
};
|
||||||
|
conn.new_unspent_coins(&[coin_a]);
|
||||||
|
// We can query by status and/or outpoint.
|
||||||
|
assert!(vec![
|
||||||
|
conn.coins(&[], &[]),
|
||||||
|
conn.coins(&[CoinStatus::Unconfirmed], &[]),
|
||||||
|
conn.coins(&[CoinStatus::Unconfirmed], &[outpoint_a]),
|
||||||
|
conn.coins(&[], &[outpoint_a]),
|
||||||
|
conn.db_coins(&[outpoint_a]),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.all(|res| res.len() == 1 && res[0].outpoint == coin_a.outpoint));
|
||||||
|
// It will not be returned if we filter for other statuses.
|
||||||
|
assert!(conn
|
||||||
|
.coins(
|
||||||
|
&[
|
||||||
|
CoinStatus::Confirmed,
|
||||||
|
CoinStatus::Spending,
|
||||||
|
CoinStatus::Spent
|
||||||
|
],
|
||||||
|
&[]
|
||||||
|
)
|
||||||
|
.is_empty());
|
||||||
|
// Filtering also for its outpoint will still not return it if status does not match.
|
||||||
|
assert!(conn
|
||||||
|
.coins(
|
||||||
|
&[
|
||||||
|
CoinStatus::Confirmed,
|
||||||
|
CoinStatus::Spending,
|
||||||
|
CoinStatus::Spent
|
||||||
|
],
|
||||||
|
&[outpoint_a]
|
||||||
|
)
|
||||||
|
.is_empty());
|
||||||
|
|
||||||
|
// Add a second coin.
|
||||||
|
let outpoint_b = bitcoin::OutPoint::from_str(
|
||||||
|
"61db3e276b095e5b05f1849dd6bfffb4e7e5ec1c4a4210099b98fce01571936f:12",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let coin_b = Coin {
|
||||||
|
outpoint: outpoint_b,
|
||||||
|
is_immature: false,
|
||||||
|
block_info: None,
|
||||||
|
amount: bitcoin::Amount::from_sat(1111),
|
||||||
|
derivation_index: bip32::ChildNumber::from_normal_idx(103).unwrap(),
|
||||||
|
is_change: true,
|
||||||
|
spend_txid: None,
|
||||||
|
spend_block: None,
|
||||||
|
};
|
||||||
|
conn.new_unspent_coins(&[coin_b]);
|
||||||
|
// Both coins are unconfirmed.
|
||||||
|
assert!(vec![
|
||||||
|
conn.coins(&[], &[]),
|
||||||
|
conn.coins(&[CoinStatus::Unconfirmed], &[]),
|
||||||
|
conn.coins(&[CoinStatus::Unconfirmed], &[outpoint_a, outpoint_b]),
|
||||||
|
conn.coins(&[], &[outpoint_a, outpoint_b]),
|
||||||
|
conn.db_coins(&[outpoint_a, outpoint_b]),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.all(|c| c.len() == 2
|
||||||
|
&& c[0].outpoint == coin_a.outpoint
|
||||||
|
&& c[1].outpoint == coin_b.outpoint));
|
||||||
|
// We can filter for just the first coin.
|
||||||
|
assert!(vec![
|
||||||
|
conn.coins(&[CoinStatus::Unconfirmed], &[outpoint_a]),
|
||||||
|
conn.coins(&[], &[outpoint_a]),
|
||||||
|
conn.db_coins(&[outpoint_a])
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.all(|res| res.len() == 1 && res[0].outpoint == coin_a.outpoint));
|
||||||
|
// Or we can filter for just the second coin.
|
||||||
|
assert!(vec![
|
||||||
|
conn.coins(&[CoinStatus::Unconfirmed], &[outpoint_b]),
|
||||||
|
conn.coins(&[], &[outpoint_b]),
|
||||||
|
conn.db_coins(&[outpoint_b])
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.all(|res| res.len() == 1 && res[0].outpoint == coin_b.outpoint));
|
||||||
|
// There are no coins with other statuses.
|
||||||
|
assert!(conn
|
||||||
|
.coins(
|
||||||
|
&[
|
||||||
|
CoinStatus::Confirmed,
|
||||||
|
CoinStatus::Spending,
|
||||||
|
CoinStatus::Spent
|
||||||
|
],
|
||||||
|
&[]
|
||||||
|
)
|
||||||
|
.is_empty());
|
||||||
|
// Now if we confirm one, it'll be marked as such.
|
||||||
|
conn.confirm_coins(&[(coin_a.outpoint, 174500, 174500)]);
|
||||||
|
assert!(vec![
|
||||||
|
conn.coins(&[CoinStatus::Confirmed], &[]),
|
||||||
|
conn.coins(&[CoinStatus::Confirmed], &[outpoint_a]),
|
||||||
|
conn.coins(&[], &[outpoint_a]),
|
||||||
|
conn.db_coins(&[outpoint_a]),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.all(|res| res.len() == 1 && res[0].outpoint == coin_a.outpoint));
|
||||||
|
// We can get both confirmed and unconfirmed.
|
||||||
|
assert!(vec![
|
||||||
|
conn.coins(&[], &[]),
|
||||||
|
conn.coins(&[CoinStatus::Unconfirmed, CoinStatus::Confirmed], &[]),
|
||||||
|
conn.coins(
|
||||||
|
&[CoinStatus::Unconfirmed, CoinStatus::Confirmed],
|
||||||
|
&[outpoint_a, outpoint_b]
|
||||||
|
),
|
||||||
|
conn.coins(&[], &[outpoint_a, outpoint_b]),
|
||||||
|
conn.db_coins(&[outpoint_a, outpoint_b]),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.all(|c| c.len() == 2
|
||||||
|
&& c[0].outpoint == coin_a.outpoint
|
||||||
|
&& c[1].outpoint == coin_b.outpoint));
|
||||||
|
|
||||||
|
// Now if we spend one, it'll be marked as such.
|
||||||
|
conn.spend_coins(&[(
|
||||||
|
coin_a.outpoint,
|
||||||
|
bitcoin::Txid::from_slice(&[0; 32][..]).unwrap(),
|
||||||
|
)]);
|
||||||
|
assert!(vec![
|
||||||
|
conn.coins(&[CoinStatus::Spending], &[]),
|
||||||
|
conn.coins(&[CoinStatus::Spending], &[outpoint_a]),
|
||||||
|
conn.coins(&[], &[outpoint_a]),
|
||||||
|
conn.list_spending_coins(),
|
||||||
|
conn.db_coins(&[outpoint_a])
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.all(|res| res.len() == 1 && res[0].outpoint == coin_a.outpoint));
|
||||||
|
// The second coin is still unconfirmed.
|
||||||
|
assert!(vec![
|
||||||
|
conn.coins(&[CoinStatus::Unconfirmed], &[]),
|
||||||
|
conn.coins(&[CoinStatus::Unconfirmed], &[outpoint_b]),
|
||||||
|
conn.coins(&[], &[outpoint_b]),
|
||||||
|
conn.db_coins(&[outpoint_b])
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.all(|res| res.len() == 1 && res[0].outpoint == coin_b.outpoint));
|
||||||
|
|
||||||
|
// Now we confirm the spend.
|
||||||
|
conn.confirm_spend(&[(
|
||||||
|
coin_a.outpoint,
|
||||||
|
bitcoin::Txid::from_slice(&[0; 32][..]).unwrap(),
|
||||||
|
128_097,
|
||||||
|
3_000_000,
|
||||||
|
)]);
|
||||||
|
// The coin no longer has spending status.
|
||||||
|
assert!(vec![
|
||||||
|
conn.coins(&[CoinStatus::Spending], &[]),
|
||||||
|
conn.coins(&[CoinStatus::Spending], &[outpoint_a]),
|
||||||
|
conn.list_spending_coins(),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.all(|res| res.is_empty()));
|
||||||
|
|
||||||
|
// Both coins are still in DB.
|
||||||
|
assert!(vec![
|
||||||
|
conn.coins(&[], &[]),
|
||||||
|
conn.coins(&[CoinStatus::Unconfirmed, CoinStatus::Spent], &[]),
|
||||||
|
conn.coins(
|
||||||
|
&[CoinStatus::Unconfirmed, CoinStatus::Spent],
|
||||||
|
&[outpoint_a, outpoint_b]
|
||||||
|
),
|
||||||
|
conn.coins(&[], &[outpoint_a, outpoint_b]),
|
||||||
|
conn.db_coins(&[outpoint_a, outpoint_b]),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.all(|c| c.len() == 2
|
||||||
|
&& c[0].outpoint == coin_a.outpoint
|
||||||
|
&& c[1].outpoint == coin_b.outpoint));
|
||||||
|
|
||||||
|
// Add a third and fourth coin.
|
||||||
|
let outpoint_c = bitcoin::OutPoint::from_str(
|
||||||
|
"61db3e276b095e5b05f1849dd6bfffb4e7e5ec1c4a4210099b98fce01571937a:42",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let coin_c = Coin {
|
||||||
|
outpoint: outpoint_c,
|
||||||
|
is_immature: false,
|
||||||
|
block_info: None,
|
||||||
|
amount: bitcoin::Amount::from_sat(30000),
|
||||||
|
derivation_index: bip32::ChildNumber::from_normal_idx(4103).unwrap(),
|
||||||
|
is_change: false,
|
||||||
|
spend_txid: None,
|
||||||
|
spend_block: None,
|
||||||
|
};
|
||||||
|
let outpoint_d = bitcoin::OutPoint::from_str(
|
||||||
|
"61db3e276b095e5b05f1849dd6bfffb4e7e5ec1c4a4210099b98fce01571937a:43",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let coin_d = Coin {
|
||||||
|
outpoint: outpoint_d,
|
||||||
|
is_immature: false,
|
||||||
|
block_info: None,
|
||||||
|
amount: bitcoin::Amount::from_sat(40000),
|
||||||
|
derivation_index: bip32::ChildNumber::from_normal_idx(4104).unwrap(),
|
||||||
|
is_change: false,
|
||||||
|
spend_txid: None,
|
||||||
|
spend_block: None,
|
||||||
|
};
|
||||||
|
conn.new_unspent_coins(&[coin_c, coin_d]);
|
||||||
|
|
||||||
|
// We can get all three unconfirmed coins with different status/outpoint filters.
|
||||||
|
assert!(vec![
|
||||||
|
conn.coins(&[CoinStatus::Unconfirmed], &[]),
|
||||||
|
conn.coins(
|
||||||
|
&[CoinStatus::Unconfirmed],
|
||||||
|
&[outpoint_b, outpoint_c, outpoint_d]
|
||||||
|
),
|
||||||
|
conn.coins(&[], &[outpoint_b, outpoint_c, outpoint_d]),
|
||||||
|
conn.db_coins(&[outpoint_b, outpoint_c, outpoint_d]),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.all(|coin| coin.len() == 3
|
||||||
|
&& coin[0].outpoint == coin_b.outpoint
|
||||||
|
&& coin[1].outpoint == coin_c.outpoint
|
||||||
|
&& coin[2].outpoint == coin_d.outpoint));
|
||||||
|
|
||||||
|
// We can also get two of the three unconfirmed coins by filtering for their outpoints.
|
||||||
|
assert!(vec![
|
||||||
|
conn.coins(&[CoinStatus::Unconfirmed], &[outpoint_b, outpoint_c]),
|
||||||
|
conn.coins(&[], &[outpoint_b, outpoint_c]),
|
||||||
|
conn.db_coins(&[outpoint_b, outpoint_c]),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.all(|coin| coin.len() == 2
|
||||||
|
&& coin[0].outpoint == coin_b.outpoint
|
||||||
|
&& coin[1].outpoint == coin_c.outpoint));
|
||||||
|
|
||||||
|
// Now spend second coin, even though it is still unconfirmed.
|
||||||
|
conn.spend_coins(&[(
|
||||||
|
coin_b.outpoint,
|
||||||
|
bitcoin::Txid::from_slice(&[1; 32][..]).unwrap(),
|
||||||
|
)]);
|
||||||
|
// The coin shows as spending.
|
||||||
|
assert!(vec![
|
||||||
|
conn.coins(&[CoinStatus::Spending], &[]),
|
||||||
|
conn.coins(&[CoinStatus::Spending], &[outpoint_b]),
|
||||||
|
conn.coins(&[], &[outpoint_b]),
|
||||||
|
conn.list_spending_coins(),
|
||||||
|
conn.db_coins(&[outpoint_b])
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.all(|res| res.len() == 1 && res[0].outpoint == coin_b.outpoint));
|
||||||
|
|
||||||
|
// Now confirm the third coin.
|
||||||
|
conn.confirm_coins(&[(coin_c.outpoint, 175500, 175500)]);
|
||||||
|
|
||||||
|
// We now only have one unconfirmed coin.
|
||||||
|
assert!(vec![
|
||||||
|
conn.coins(&[CoinStatus::Unconfirmed], &[]),
|
||||||
|
conn.coins(
|
||||||
|
&[CoinStatus::Unconfirmed],
|
||||||
|
&[outpoint_a, outpoint_b, outpoint_c, outpoint_d]
|
||||||
|
),
|
||||||
|
conn.coins(&[], &[outpoint_d]),
|
||||||
|
conn.db_coins(&[outpoint_d]),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.all(|c| c.len() == 1 && c[0].outpoint == coin_d.outpoint));
|
||||||
|
|
||||||
|
// There is now one coin for each status.
|
||||||
|
assert!(vec![
|
||||||
|
conn.coins(&[CoinStatus::Unconfirmed], &[]),
|
||||||
|
conn.coins(&[CoinStatus::Unconfirmed], &[outpoint_d]),
|
||||||
|
conn.coins(&[CoinStatus::Confirmed], &[]),
|
||||||
|
conn.coins(&[CoinStatus::Confirmed], &[outpoint_c]),
|
||||||
|
conn.coins(&[CoinStatus::Spending], &[]),
|
||||||
|
conn.coins(&[CoinStatus::Spending], &[outpoint_b]),
|
||||||
|
conn.coins(&[CoinStatus::Spent], &[]),
|
||||||
|
conn.coins(&[CoinStatus::Spent], &[outpoint_a]),
|
||||||
|
conn.coins(&[], &[outpoint_a]),
|
||||||
|
conn.coins(&[], &[outpoint_b]),
|
||||||
|
conn.coins(&[], &[outpoint_c]),
|
||||||
|
conn.coins(&[], &[outpoint_d]),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.len())
|
||||||
|
.all(|length| length == 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::remove_dir_all(tmp_dir).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn db_coins_update() {
|
fn db_coins_update() {
|
||||||
let (tmp_dir, _, _, db) = dummy_db();
|
let (tmp_dir, _, _, db) = dummy_db();
|
||||||
@ -897,7 +1229,7 @@ CREATE TABLE spend_transactions (
|
|||||||
let mut conn = db.connection().unwrap();
|
let mut conn = db.connection().unwrap();
|
||||||
|
|
||||||
// Necessarily empty at first.
|
// Necessarily empty at first.
|
||||||
assert!(conn.coins(CoinType::All).is_empty());
|
assert!(conn.coins(&[], &[]).is_empty());
|
||||||
|
|
||||||
// Add one, we'll get it.
|
// Add one, we'll get it.
|
||||||
let coin_a = Coin {
|
let coin_a = Coin {
|
||||||
@ -914,11 +1246,11 @@ CREATE TABLE spend_transactions (
|
|||||||
spend_block: None,
|
spend_block: None,
|
||||||
};
|
};
|
||||||
conn.new_unspent_coins(&[coin_a]);
|
conn.new_unspent_coins(&[coin_a]);
|
||||||
assert_eq!(conn.coins(CoinType::All)[0].outpoint, coin_a.outpoint);
|
assert_eq!(conn.coins(&[], &[])[0].outpoint, coin_a.outpoint);
|
||||||
|
|
||||||
// We can also remove it. Say the unconfirmed tx that created it got replaced.
|
// We can also remove it. Say the unconfirmed tx that created it got replaced.
|
||||||
conn.remove_coins(&[coin_a.outpoint]);
|
conn.remove_coins(&[coin_a.outpoint]);
|
||||||
assert!(conn.coins(CoinType::All).is_empty());
|
assert!(conn.coins(&[], &[]).is_empty());
|
||||||
|
|
||||||
// Add it back for the rest of the test.
|
// Add it back for the rest of the test.
|
||||||
conn.new_unspent_coins(&[coin_a]);
|
conn.new_unspent_coins(&[coin_a]);
|
||||||
@ -928,9 +1260,21 @@ CREATE TABLE spend_transactions (
|
|||||||
assert_eq!(coins.len(), 1);
|
assert_eq!(coins.len(), 1);
|
||||||
assert_eq!(coins[0].outpoint, coin_a.outpoint);
|
assert_eq!(coins[0].outpoint, coin_a.outpoint);
|
||||||
|
|
||||||
// It is unspent.
|
// It is unconfirmed.
|
||||||
assert_eq!(conn.coins(CoinType::Unspent)[0].outpoint, coin_a.outpoint);
|
assert_eq!(
|
||||||
assert!(conn.coins(CoinType::Spent).is_empty());
|
conn.coins(&[CoinStatus::Unconfirmed], &[])[0].outpoint,
|
||||||
|
coin_a.outpoint
|
||||||
|
);
|
||||||
|
assert!(conn
|
||||||
|
.coins(
|
||||||
|
&[
|
||||||
|
CoinStatus::Confirmed,
|
||||||
|
CoinStatus::Spending,
|
||||||
|
CoinStatus::Spent
|
||||||
|
],
|
||||||
|
&[]
|
||||||
|
)
|
||||||
|
.is_empty());
|
||||||
|
|
||||||
// Add a second one (this one is change), we'll get both.
|
// Add a second one (this one is change), we'll get both.
|
||||||
let coin_b = Coin {
|
let coin_b = Coin {
|
||||||
@ -948,7 +1292,7 @@ CREATE TABLE spend_transactions (
|
|||||||
};
|
};
|
||||||
conn.new_unspent_coins(&[coin_b]);
|
conn.new_unspent_coins(&[coin_b]);
|
||||||
let outpoints: HashSet<bitcoin::OutPoint> = conn
|
let outpoints: HashSet<bitcoin::OutPoint> = conn
|
||||||
.coins(CoinType::All)
|
.coins(&[], &[])
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|c| c.outpoint)
|
.map(|c| c.outpoint)
|
||||||
.collect();
|
.collect();
|
||||||
@ -967,15 +1311,24 @@ CREATE TABLE spend_transactions (
|
|||||||
assert!(coins.iter().any(|c| c.outpoint == coin_a.outpoint));
|
assert!(coins.iter().any(|c| c.outpoint == coin_a.outpoint));
|
||||||
assert!(coins.iter().any(|c| c.outpoint == coin_b.outpoint));
|
assert!(coins.iter().any(|c| c.outpoint == coin_b.outpoint));
|
||||||
|
|
||||||
// They are both unspent
|
// They are both unconfirmed.
|
||||||
assert_eq!(conn.coins(CoinType::Unspent).len(), 2);
|
assert_eq!(conn.coins(&[CoinStatus::Unconfirmed], &[]).len(), 2);
|
||||||
assert!(conn.coins(CoinType::Spent).is_empty());
|
assert!(conn
|
||||||
|
.coins(
|
||||||
|
&[
|
||||||
|
CoinStatus::Confirmed,
|
||||||
|
CoinStatus::Spending,
|
||||||
|
CoinStatus::Spent
|
||||||
|
],
|
||||||
|
&[]
|
||||||
|
)
|
||||||
|
.is_empty());
|
||||||
|
|
||||||
// Now if we confirm one, it'll be marked as such.
|
// Now if we confirm one, it'll be marked as such.
|
||||||
let height = 174500;
|
let height = 174500;
|
||||||
let time = 174500;
|
let time = 174500;
|
||||||
conn.confirm_coins(&[(coin_a.outpoint, height, time)]);
|
conn.confirm_coins(&[(coin_a.outpoint, height, time)]);
|
||||||
let coins = conn.coins(CoinType::All);
|
let coins = conn.coins(&[], &[]);
|
||||||
assert_eq!(coins[0].block_info, Some(DbBlockInfo { height, time }));
|
assert_eq!(coins[0].block_info, Some(DbBlockInfo { height, time }));
|
||||||
assert!(coins[1].block_info.is_none());
|
assert!(coins[1].block_info.is_none());
|
||||||
|
|
||||||
@ -985,7 +1338,7 @@ CREATE TABLE spend_transactions (
|
|||||||
bitcoin::Txid::from_slice(&[0; 32][..]).unwrap(),
|
bitcoin::Txid::from_slice(&[0; 32][..]).unwrap(),
|
||||||
)]);
|
)]);
|
||||||
let coins_map: HashMap<bitcoin::OutPoint, DbCoin> = conn
|
let coins_map: HashMap<bitcoin::OutPoint, DbCoin> = conn
|
||||||
.coins(CoinType::All)
|
.coins(&[], &[])
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|c| (c.outpoint, c))
|
.map(|c| (c.outpoint, c))
|
||||||
.collect();
|
.collect();
|
||||||
@ -1003,9 +1356,15 @@ CREATE TABLE spend_transactions (
|
|||||||
.collect();
|
.collect();
|
||||||
assert!(outpoints.contains(&coin_a.outpoint));
|
assert!(outpoints.contains(&coin_a.outpoint));
|
||||||
|
|
||||||
// The first one is spent, not the second one.
|
// The first one is spending, not the second one.
|
||||||
assert_eq!(conn.coins(CoinType::Spent)[0].outpoint, coin_a.outpoint);
|
assert_eq!(
|
||||||
assert_eq!(conn.coins(CoinType::Unspent)[0].outpoint, coin_b.outpoint);
|
conn.coins(&[CoinStatus::Spending], &[])[0].outpoint,
|
||||||
|
coin_a.outpoint
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
conn.coins(&[CoinStatus::Unconfirmed], &[])[0].outpoint,
|
||||||
|
coin_b.outpoint
|
||||||
|
);
|
||||||
|
|
||||||
// Now if we confirm the spend.
|
// Now if we confirm the spend.
|
||||||
let height = 128_097;
|
let height = 128_097;
|
||||||
@ -1051,7 +1410,7 @@ CREATE TABLE spend_transactions (
|
|||||||
};
|
};
|
||||||
conn.new_unspent_coins(&[coin_imma]);
|
conn.new_unspent_coins(&[coin_imma]);
|
||||||
let outpoints: HashSet<bitcoin::OutPoint> = conn
|
let outpoints: HashSet<bitcoin::OutPoint> = conn
|
||||||
.coins(CoinType::All)
|
.coins(&[], &[])
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|c| c.outpoint)
|
.map(|c| c.outpoint)
|
||||||
.collect();
|
.collect();
|
||||||
@ -1709,7 +2068,7 @@ CREATE TABLE spend_transactions (
|
|||||||
spend_txid: None,
|
spend_txid: None,
|
||||||
spend_block: None,
|
spend_block: None,
|
||||||
}]);
|
}]);
|
||||||
let coins = conn.coins(CoinType::All);
|
let coins = conn.coins(&[], &[]);
|
||||||
assert_eq!(coins.len(), 3);
|
assert_eq!(coins.len(), 3);
|
||||||
assert_eq!(coins.iter().filter(|c| !c.is_immature).count(), 2);
|
assert_eq!(coins.iter().filter(|c| !c.is_immature).count(), 2);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
commands::LabelItem,
|
commands::{CoinStatus, LabelItem},
|
||||||
jsonrpc::{Error, Params, Request, Response},
|
jsonrpc::{Error, Params, Request, Response},
|
||||||
DaemonControl,
|
DaemonControl,
|
||||||
};
|
};
|
||||||
@ -87,6 +87,55 @@ fn broadcast_spend(control: &DaemonControl, params: Params) -> Result<serde_json
|
|||||||
Ok(serde_json::json!({}))
|
Ok(serde_json::json!({}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn list_coins(control: &DaemonControl, params: Option<Params>) -> Result<serde_json::Value, Error> {
|
||||||
|
let statuses_arg = params
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|p| p.get(0, "statuses"))
|
||||||
|
.and_then(|statuses| statuses.as_array());
|
||||||
|
let statuses: Vec<CoinStatus> = if let Some(statuses_arg) = statuses_arg {
|
||||||
|
statuses_arg
|
||||||
|
.iter()
|
||||||
|
.map(|status_arg| {
|
||||||
|
status_arg
|
||||||
|
.as_str()
|
||||||
|
.and_then(CoinStatus::from_arg)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Error::invalid_params(format!(
|
||||||
|
"Invalid value {} in 'statuses' parameter.",
|
||||||
|
status_arg
|
||||||
|
))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<CoinStatus>, Error>>()?
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
let outpoints_arg = params
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|p| p.get(1, "outpoints"))
|
||||||
|
.and_then(|op| op.as_array());
|
||||||
|
let outpoints: Vec<bitcoin::OutPoint> = if let Some(outpoints_arg) = outpoints_arg {
|
||||||
|
outpoints_arg
|
||||||
|
.iter()
|
||||||
|
.map(|op_arg| {
|
||||||
|
op_arg
|
||||||
|
.as_str()
|
||||||
|
.and_then(|op| bitcoin::OutPoint::from_str(op).map_or_else(|_| None, Some))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Error::invalid_params(format!(
|
||||||
|
"Invalid value {} in 'outpoints' parameter.",
|
||||||
|
op_arg
|
||||||
|
))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<bitcoin::OutPoint>, Error>>()?
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
let res = control.list_coins(&statuses, &outpoints);
|
||||||
|
Ok(serde_json::json!(&res))
|
||||||
|
}
|
||||||
|
|
||||||
fn list_confirmed(control: &DaemonControl, params: Params) -> Result<serde_json::Value, Error> {
|
fn list_confirmed(control: &DaemonControl, params: Params) -> Result<serde_json::Value, Error> {
|
||||||
let start: u32 = params
|
let start: u32 = params
|
||||||
.get(0, "start")
|
.get(0, "start")
|
||||||
@ -258,7 +307,10 @@ pub fn handle_request(control: &DaemonControl, req: Request) -> Result<Response,
|
|||||||
}
|
}
|
||||||
"getinfo" => serde_json::json!(&control.get_info()),
|
"getinfo" => serde_json::json!(&control.get_info()),
|
||||||
"getnewaddress" => serde_json::json!(&control.get_new_address()),
|
"getnewaddress" => serde_json::json!(&control.get_new_address()),
|
||||||
"listcoins" => serde_json::json!(&control.list_coins()),
|
"listcoins" => {
|
||||||
|
let params = req.params;
|
||||||
|
list_coins(control, params)?
|
||||||
|
}
|
||||||
"listconfirmed" => {
|
"listconfirmed" => {
|
||||||
let params = req.params.ok_or_else(|| {
|
let params = req.params.ok_or_else(|| {
|
||||||
Error::invalid_params(
|
Error::invalid_params(
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
bitcoin::{BitcoinInterface, Block, BlockChainTip, UTxO},
|
bitcoin::{BitcoinInterface, Block, BlockChainTip, UTxO},
|
||||||
config::{BitcoinConfig, Config},
|
config::{BitcoinConfig, Config},
|
||||||
database::{BlockInfo, Coin, CoinType, DatabaseConnection, DatabaseInterface, LabelItem},
|
database::{BlockInfo, Coin, CoinStatus, DatabaseConnection, DatabaseInterface, LabelItem},
|
||||||
descriptors, DaemonHandle,
|
descriptors, DaemonHandle,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -195,19 +195,43 @@ impl DatabaseConnection for DummyDatabase {
|
|||||||
self.db.write().unwrap().change_index = index;
|
self.db.write().unwrap().change_index = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn coins(&mut self, coin_type: CoinType) -> HashMap<bitcoin::OutPoint, Coin> {
|
fn coins(
|
||||||
let coins = self.db.read().unwrap().coins.clone();
|
&mut self,
|
||||||
match coin_type {
|
statuses: &[CoinStatus],
|
||||||
CoinType::All => coins,
|
outpoints: &[bitcoin::OutPoint],
|
||||||
CoinType::Unspent => coins
|
) -> HashMap<bitcoin::OutPoint, Coin> {
|
||||||
.into_iter()
|
self.db
|
||||||
.filter(|(_, c)| c.spend_txid.is_none())
|
.read()
|
||||||
.collect(),
|
.unwrap()
|
||||||
CoinType::Spent => coins
|
.coins
|
||||||
.into_iter()
|
.clone()
|
||||||
.filter(|(_, c)| c.spend_txid.is_some())
|
.into_iter()
|
||||||
.collect(),
|
.filter_map(|(op, c)| {
|
||||||
}
|
if (c.block_info.is_none()
|
||||||
|
&& c.spend_txid.is_none()
|
||||||
|
&& statuses.contains(&CoinStatus::Unconfirmed))
|
||||||
|
|| (c.block_info.is_some()
|
||||||
|
&& c.spend_txid.is_none()
|
||||||
|
&& statuses.contains(&CoinStatus::Confirmed))
|
||||||
|
|| (c.spend_txid.is_some()
|
||||||
|
&& c.spend_block.is_none()
|
||||||
|
&& statuses.contains(&CoinStatus::Spending))
|
||||||
|
|| (c.spend_block.is_some() && statuses.contains(&CoinStatus::Spent))
|
||||||
|
|| statuses.is_empty()
|
||||||
|
{
|
||||||
|
Some((op, c))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter_map(|(op, c)| {
|
||||||
|
if outpoints.contains(&op) || outpoints.is_empty() {
|
||||||
|
Some((op, c))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_spending_coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin> {
|
fn list_spending_coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin> {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from fixtures import *
|
from fixtures import *
|
||||||
@ -44,20 +45,46 @@ def test_listcoins(lianad, bitcoind):
|
|||||||
|
|
||||||
# If we send a coin, we'll get a new entry. Note we monitor for unconfirmed
|
# If we send a coin, we'll get a new entry. Note we monitor for unconfirmed
|
||||||
# funds as well.
|
# funds as well.
|
||||||
addr = lianad.rpc.getnewaddress()["address"]
|
addr_a = lianad.rpc.getnewaddress()["address"]
|
||||||
txid = bitcoind.rpc.sendtoaddress(addr, 1)
|
txid_a = bitcoind.rpc.sendtoaddress(addr_a, 1)
|
||||||
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 1)
|
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 1)
|
||||||
res = lianad.rpc.listcoins()["coins"]
|
res = lianad.rpc.listcoins()["coins"]
|
||||||
assert txid == res[0]["outpoint"][:64]
|
outpoint_a = res[0]["outpoint"]
|
||||||
|
assert txid_a == outpoint_a[:64]
|
||||||
assert res[0]["amount"] == 1 * COIN
|
assert res[0]["amount"] == 1 * COIN
|
||||||
assert res[0]["block_height"] is None
|
assert res[0]["block_height"] is None
|
||||||
assert res[0]["spend_info"] is None
|
assert res[0]["spend_info"] is None
|
||||||
|
|
||||||
|
assert len(lianad.rpc.listcoins(["confirmed", "spent", "spending"])["coins"]) == 0
|
||||||
|
assert (
|
||||||
|
lianad.rpc.listcoins()
|
||||||
|
== lianad.rpc.listcoins([], [outpoint_a])
|
||||||
|
== lianad.rpc.listcoins(["unconfirmed"])
|
||||||
|
== lianad.rpc.listcoins(["unconfirmed"], [outpoint_a])
|
||||||
|
== lianad.rpc.listcoins(["unconfirmed", "confirmed"])
|
||||||
|
== lianad.rpc.listcoins(["spent", "unconfirmed", "confirmed"])
|
||||||
|
== lianad.rpc.listcoins(["spent", "unconfirmed", "confirmed"], [outpoint_a])
|
||||||
|
)
|
||||||
# If the coin gets confirmed, it'll be marked as such.
|
# If the coin gets confirmed, it'll be marked as such.
|
||||||
bitcoind.generate_block(1, wait_for_mempool=txid)
|
bitcoind.generate_block(1, wait_for_mempool=txid_a)
|
||||||
block_height = bitcoind.rpc.getblockcount()
|
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)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
len(lianad.rpc.listcoins())
|
||||||
|
== len(lianad.rpc.listcoins(["confirmed"])["coins"])
|
||||||
|
== 1
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
lianad.rpc.listcoins()
|
||||||
|
== lianad.rpc.listcoins([], [outpoint_a])
|
||||||
|
== lianad.rpc.listcoins(["confirmed"])
|
||||||
|
== lianad.rpc.listcoins(["confirmed"], [outpoint_a])
|
||||||
|
== lianad.rpc.listcoins(["unconfirmed", "confirmed"])
|
||||||
|
== lianad.rpc.listcoins(["spent", "unconfirmed", "confirmed"])
|
||||||
|
== lianad.rpc.listcoins(["spent", "unconfirmed", "confirmed"], [outpoint_a])
|
||||||
|
)
|
||||||
|
|
||||||
# Same if the coin gets spent.
|
# Same if the coin gets spent.
|
||||||
spend_tx = spend_coins(lianad, bitcoind, (res[0],))
|
spend_tx = spend_coins(lianad, bitcoind, (res[0],))
|
||||||
spend_txid = get_txid(spend_tx)
|
spend_txid = get_txid(spend_tx)
|
||||||
@ -65,6 +92,8 @@ def test_listcoins(lianad, bitcoind):
|
|||||||
spend_info = lianad.rpc.listcoins()["coins"][0]["spend_info"]
|
spend_info = lianad.rpc.listcoins()["coins"][0]["spend_info"]
|
||||||
assert spend_info["txid"] == spend_txid
|
assert spend_info["txid"] == spend_txid
|
||||||
assert spend_info["height"] is None
|
assert spend_info["height"] is None
|
||||||
|
assert len(lianad.rpc.listcoins(["spent"])["coins"]) == 0
|
||||||
|
assert len(lianad.rpc.listcoins(["spending"])["coins"]) == 1
|
||||||
|
|
||||||
# And if this spending tx gets confirmed.
|
# And if this spending tx gets confirmed.
|
||||||
bitcoind.generate_block(1, wait_for_mempool=spend_txid)
|
bitcoind.generate_block(1, wait_for_mempool=spend_txid)
|
||||||
@ -73,6 +102,220 @@ def test_listcoins(lianad, bitcoind):
|
|||||||
spend_info = lianad.rpc.listcoins()["coins"][0]["spend_info"]
|
spend_info = lianad.rpc.listcoins()["coins"][0]["spend_info"]
|
||||||
assert spend_info["txid"] == spend_txid
|
assert spend_info["txid"] == spend_txid
|
||||||
assert spend_info["height"] == curr_height
|
assert spend_info["height"] == curr_height
|
||||||
|
assert len(lianad.rpc.listcoins(["unconfirmed", "confirmed"])["coins"]) == 0
|
||||||
|
assert (
|
||||||
|
lianad.rpc.listcoins()
|
||||||
|
== lianad.rpc.listcoins(["spent"])
|
||||||
|
== lianad.rpc.listcoins(["spent", "unconfirmed", "confirmed"])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add a second coin.
|
||||||
|
addr_b = lianad.rpc.getnewaddress()["address"]
|
||||||
|
txid_b = bitcoind.rpc.sendtoaddress(addr_b, 2)
|
||||||
|
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 2)
|
||||||
|
res = lianad.rpc.listcoins(["unconfirmed"], [])["coins"]
|
||||||
|
outpoint_b = res[0]["outpoint"]
|
||||||
|
|
||||||
|
# We have one unconfirmed coin and one spent coin.
|
||||||
|
assert (
|
||||||
|
len(lianad.rpc.listcoins()["coins"])
|
||||||
|
== len(lianad.rpc.listcoins([], [outpoint_a, outpoint_b])["coins"])
|
||||||
|
== len(lianad.rpc.listcoins(["unconfirmed", "spent"])["coins"])
|
||||||
|
== len(
|
||||||
|
lianad.rpc.listcoins(["unconfirmed", "spent"], [outpoint_a, outpoint_b])[
|
||||||
|
"coins"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
== 2
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
lianad.rpc.listcoins([], [outpoint_b])
|
||||||
|
== lianad.rpc.listcoins(["unconfirmed"])
|
||||||
|
== lianad.rpc.listcoins(["unconfirmed"], [outpoint_b])
|
||||||
|
== lianad.rpc.listcoins(["unconfirmed", "confirmed"])
|
||||||
|
== lianad.rpc.listcoins(["spending", "unconfirmed", "confirmed"])
|
||||||
|
== lianad.rpc.listcoins(["spending", "unconfirmed", "confirmed"], [outpoint_b])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now confirm the second coin.
|
||||||
|
bitcoind.generate_block(1, wait_for_mempool=txid_b)
|
||||||
|
block_height = bitcoind.rpc.getblockcount()
|
||||||
|
wait_for(
|
||||||
|
lambda: lianad.rpc.listcoins([], [outpoint_b])["coins"][0]["block_height"]
|
||||||
|
== block_height
|
||||||
|
)
|
||||||
|
|
||||||
|
# We have one confirmed coin and one spent coin.
|
||||||
|
assert (
|
||||||
|
len(lianad.rpc.listcoins()["coins"])
|
||||||
|
== len(lianad.rpc.listcoins([], [outpoint_a, outpoint_b])["coins"])
|
||||||
|
== len(lianad.rpc.listcoins(["confirmed", "spent"])["coins"])
|
||||||
|
== len(
|
||||||
|
lianad.rpc.listcoins(["confirmed", "spent"], [outpoint_a, outpoint_b])[
|
||||||
|
"coins"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
== 2
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
lianad.rpc.listcoins([], [outpoint_b])
|
||||||
|
== lianad.rpc.listcoins(["confirmed"])
|
||||||
|
== lianad.rpc.listcoins(["confirmed"], [outpoint_b])
|
||||||
|
== lianad.rpc.listcoins(["unconfirmed", "confirmed"])
|
||||||
|
== lianad.rpc.listcoins(["unconfirmed", "confirmed", "spending"])
|
||||||
|
== lianad.rpc.listcoins(["unconfirmed", "confirmed", "spending"], [outpoint_b])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add a third coin.
|
||||||
|
addr_c = lianad.rpc.getnewaddress()["address"]
|
||||||
|
txid_c = bitcoind.rpc.sendtoaddress(addr_c, 3)
|
||||||
|
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 3)
|
||||||
|
res = lianad.rpc.listcoins(["unconfirmed"], [])["coins"]
|
||||||
|
outpoint_c = res[0]["outpoint"]
|
||||||
|
|
||||||
|
# We have three different statuses: unconfirmed, confirmed and spent.
|
||||||
|
assert (
|
||||||
|
len(lianad.rpc.listcoins()["coins"])
|
||||||
|
== len(lianad.rpc.listcoins([], [outpoint_a, outpoint_b, outpoint_c])["coins"])
|
||||||
|
== len(lianad.rpc.listcoins(["unconfirmed", "confirmed", "spent"])["coins"])
|
||||||
|
== len(
|
||||||
|
lianad.rpc.listcoins(
|
||||||
|
["unconfirmed", "confirmed", "spent"],
|
||||||
|
[outpoint_a, outpoint_b, outpoint_c],
|
||||||
|
)["coins"]
|
||||||
|
)
|
||||||
|
== 3
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
lianad.rpc.listcoins([], [outpoint_c])
|
||||||
|
== lianad.rpc.listcoins(["unconfirmed"])
|
||||||
|
== lianad.rpc.listcoins(["unconfirmed"], [outpoint_c])
|
||||||
|
== lianad.rpc.listcoins(["unconfirmed", "spending"])
|
||||||
|
== lianad.rpc.listcoins(["spending", "unconfirmed"])
|
||||||
|
== lianad.rpc.listcoins(["spending", "unconfirmed", "confirmed"], [outpoint_c])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Spend third coin, even though it is still unconfirmed.
|
||||||
|
spend_tx = spend_coins(lianad, bitcoind, (res[0],))
|
||||||
|
spend_txid = get_txid(spend_tx)
|
||||||
|
wait_for(
|
||||||
|
lambda: lianad.rpc.listcoins([], [outpoint_c])["coins"][0]["spend_info"]
|
||||||
|
is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(lianad.rpc.listcoins(["unconfirmed"])["coins"]) == 0
|
||||||
|
assert (
|
||||||
|
len(lianad.rpc.listcoins()["coins"])
|
||||||
|
== len(lianad.rpc.listcoins([], [outpoint_a, outpoint_b, outpoint_c])["coins"])
|
||||||
|
== len(lianad.rpc.listcoins(["confirmed", "spending", "spent"])["coins"])
|
||||||
|
== len(
|
||||||
|
lianad.rpc.listcoins(
|
||||||
|
["confirmed", "spending", "spent"], [outpoint_a, outpoint_b, outpoint_c]
|
||||||
|
)["coins"]
|
||||||
|
)
|
||||||
|
== 3
|
||||||
|
)
|
||||||
|
# The unconfirmed coin now has spending status.
|
||||||
|
assert (
|
||||||
|
lianad.rpc.listcoins([], [outpoint_c])
|
||||||
|
== lianad.rpc.listcoins(["spending"])
|
||||||
|
== lianad.rpc.listcoins(["spending"], [outpoint_c])
|
||||||
|
== lianad.rpc.listcoins(["spending", "unconfirmed"])
|
||||||
|
== lianad.rpc.listcoins(["spending", "unconfirmed"], [outpoint_c])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add a fourth coin.
|
||||||
|
addr_d = lianad.rpc.getnewaddress()["address"]
|
||||||
|
txid_d = bitcoind.rpc.sendtoaddress(addr_d, 4)
|
||||||
|
wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 4)
|
||||||
|
res = lianad.rpc.listcoins(["unconfirmed"], [])["coins"]
|
||||||
|
outpoint_d = res[0]["outpoint"]
|
||||||
|
|
||||||
|
# We now have all four statuses.
|
||||||
|
assert (
|
||||||
|
len(lianad.rpc.listcoins(["unconfirmed"])["coins"])
|
||||||
|
== len(lianad.rpc.listcoins(["confirmed"])["coins"])
|
||||||
|
== len(lianad.rpc.listcoins(["spending"])["coins"])
|
||||||
|
== len(lianad.rpc.listcoins(["spent"])["coins"])
|
||||||
|
== 1
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
len(lianad.rpc.listcoins()["coins"])
|
||||||
|
== len(
|
||||||
|
lianad.rpc.listcoins([], [outpoint_a, outpoint_b, outpoint_c, outpoint_d])[
|
||||||
|
"coins"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
== len(
|
||||||
|
lianad.rpc.listcoins(["unconfirmed", "confirmed", "spending", "spent"])[
|
||||||
|
"coins"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
== len(
|
||||||
|
lianad.rpc.listcoins(
|
||||||
|
["unconfirmed", "confirmed", "spending", "spent"],
|
||||||
|
[outpoint_a, outpoint_b, outpoint_c, outpoint_d],
|
||||||
|
)["coins"]
|
||||||
|
)
|
||||||
|
== 4
|
||||||
|
)
|
||||||
|
|
||||||
|
# We can filter for specific statuses/outpoints.
|
||||||
|
assert (
|
||||||
|
sorted(
|
||||||
|
lianad.rpc.listcoins(["spending", "spent"])["coins"],
|
||||||
|
key=lambda c: c["outpoint"],
|
||||||
|
)
|
||||||
|
== sorted(
|
||||||
|
lianad.rpc.listcoins(["spending", "spent"], [outpoint_a, outpoint_c])[
|
||||||
|
"coins"
|
||||||
|
],
|
||||||
|
key=lambda c: c["outpoint"],
|
||||||
|
)
|
||||||
|
== sorted(
|
||||||
|
lianad.rpc.listcoins(
|
||||||
|
["unconfirmed", "confirmed", "spending", "spent"],
|
||||||
|
[outpoint_a, outpoint_c],
|
||||||
|
)["coins"],
|
||||||
|
key=lambda c: c["outpoint"],
|
||||||
|
)
|
||||||
|
== sorted(
|
||||||
|
lianad.rpc.listcoins(
|
||||||
|
["spending", "spent"], [outpoint_a, outpoint_b, outpoint_c, outpoint_d]
|
||||||
|
)["coins"],
|
||||||
|
key=lambda c: c["outpoint"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Finally, check that we return errors for invalid parameter values.
|
||||||
|
for statuses, outpoints in [
|
||||||
|
(["fake_status"], []),
|
||||||
|
(["spent", "fake_status"], []),
|
||||||
|
(["fake_status", "fake_status_2"], []),
|
||||||
|
(["confirmed", "spending", "fake_status"], ["fake_outpoint"]),
|
||||||
|
(["fake_status"], [outpoint_a, outpoint_b]),
|
||||||
|
]:
|
||||||
|
with pytest.raises(
|
||||||
|
RpcError,
|
||||||
|
match=re.escape(
|
||||||
|
"Invalid params: Invalid value \"fake_status\" in \\'statuses\\' parameter."
|
||||||
|
),
|
||||||
|
):
|
||||||
|
lianad.rpc.listcoins(statuses, outpoints)
|
||||||
|
|
||||||
|
for statuses, outpoints in [
|
||||||
|
([], ["fake_outpoint"]),
|
||||||
|
([], [outpoint_a, "fake_outpoint", outpoint_b]),
|
||||||
|
([], [outpoint_a, "fake_outpoint", "fake_outpoint_2"]),
|
||||||
|
([], [outpoint_a, outpoint_b, "fake_outpoint"]),
|
||||||
|
]:
|
||||||
|
with pytest.raises(
|
||||||
|
RpcError,
|
||||||
|
match=re.escape(
|
||||||
|
"Invalid params: Invalid value \"fake_outpoint\" in \\'outpoints\\' parameter."
|
||||||
|
),
|
||||||
|
):
|
||||||
|
lianad.rpc.listcoins(statuses, outpoints)
|
||||||
|
|
||||||
|
|
||||||
def test_jsonrpc_server(lianad, bitcoind):
|
def test_jsonrpc_server(lianad, bitcoind):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user