Add `list-tx` and `list-unspent` commands

This commit is contained in:
Jack Grigg 2023-09-01 00:52:37 +00:00
parent b5a5a1d68d
commit 44f151c750
6 changed files with 192 additions and 0 deletions

2
Cargo.lock generated
View File

@ -2747,8 +2747,10 @@ dependencies = [
"gumdrop",
"prost",
"rayon",
"rusqlite",
"schemer",
"secrecy",
"time",
"tokio",
"tonic",
"tracing",

View File

@ -12,8 +12,10 @@ futures-util = "0.3"
gumdrop = "0.8"
prost = "0.11"
rayon = "1.7"
rusqlite = { version = "0.25", features = ["time"] }
schemer = "0.2"
secrecy = "0.8"
time = "0.2"
tokio = { version = "1.21.0", features = ["fs", "macros", "rt-multi-thread"] }
tonic = { version = "0.9", features = ["gzip", "tls-webpki-roots"] }
tracing = "0.1"

View File

@ -1,5 +1,7 @@
pub(crate) mod balance;
pub(crate) mod init;
pub(crate) mod list_tx;
pub(crate) mod list_unspent;
pub(crate) mod send;
pub(crate) mod sync;
pub(crate) mod upgrade;

141
src/commands/list_tx.rs Normal file
View File

@ -0,0 +1,141 @@
use anyhow::anyhow;
use gumdrop::Options;
use rusqlite::{named_params, Connection};
use zcash_primitives::{
consensus::BlockHeight,
transaction::{
components::{amount::NonNegativeAmount, Amount},
TxId,
},
};
use crate::data::get_db_paths;
// Options accepted for the `list` command
#[derive(Debug, Options)]
pub(crate) struct Command {}
impl Command {
pub(crate) fn run(self, wallet_dir: Option<String>) -> anyhow::Result<()> {
let (_, db_data) = get_db_paths(wallet_dir);
let conn = Connection::open(db_data)?;
rusqlite::vtab::array::load_module(&conn)?;
let mut stmt_txs = conn.prepare(
"SELECT mined_height,
txid,
expiry_height,
fee_paid,
sent_note_count,
received_note_count,
memo_count,
block_time,
expired_unmined
FROM v_transactions
WHERE account_id = :account_id",
)?;
println!("Transactions:");
for row in stmt_txs.query_and_then(
named_params! {":account_id": 0},
|row| -> anyhow::Result<_> {
Transaction::from_parts(
row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3)?,
row.get(4)?,
row.get(5)?,
row.get(6)?,
row.get(7)?,
row.get(8)?,
)
},
)? {
let tx = row?;
println!("");
tx.print();
}
Ok(())
}
}
struct Transaction {
mined_height: Option<BlockHeight>,
txid: TxId,
expiry_height: Option<BlockHeight>,
fee_paid: Option<NonNegativeAmount>,
sent_note_count: usize,
received_note_count: usize,
memo_count: usize,
block_time: Option<i64>,
expired_unmined: bool,
}
impl Transaction {
fn from_parts(
mined_height: Option<u32>,
txid: Vec<u8>,
expiry_height: Option<u32>,
fee_paid: Option<u64>,
sent_note_count: usize,
received_note_count: usize,
memo_count: usize,
block_time: Option<i64>,
expired_unmined: bool,
) -> anyhow::Result<Self> {
Ok(Transaction {
mined_height: mined_height.map(BlockHeight::from_u32),
txid: TxId::from_bytes(txid.try_into().map_err(|_| anyhow!("Invalid TxId"))?),
expiry_height: expiry_height.map(BlockHeight::from_u32),
fee_paid: fee_paid
.map(|v| NonNegativeAmount::from_u64(v).map_err(|()| anyhow!("Fee out of range")))
.transpose()?,
sent_note_count,
received_note_count,
memo_count,
block_time,
expired_unmined,
})
}
fn print(&self) {
let height_to_str = |height: Option<BlockHeight>, def: &str| {
height.map(|h| h.to_string()).unwrap_or(def.to_owned())
};
println!("{}", self.txid);
if let Some((height, block_time)) = self.mined_height.zip(self.block_time) {
println!(
" Mined: {} ({})",
height,
time::OffsetDateTime::from_unix_timestamp(block_time),
);
} else {
println!(
" {} (expiry height: {})",
if self.expired_unmined {
" Expired"
} else {
" Unmined"
},
height_to_str(self.expiry_height, "Unknown"),
);
}
println!(
" Fee paid: {}",
self.fee_paid
.map(|v| format!("{} zatoshis", u64::from(Amount::from(v))))
.as_ref()
.map(|s| s.as_str())
.unwrap_or("Unknown"),
);
println!(
" Sent {} notes, received {} notes, {} memos",
self.sent_note_count, self.received_note_count, self.memo_count,
);
}
}

View File

@ -0,0 +1,37 @@
use anyhow::anyhow;
use gumdrop::Options;
use zcash_client_backend::data_api::WalletRead;
use zcash_client_sqlite::WalletDb;
use zcash_primitives::{consensus::Parameters, zip32::AccountId};
use crate::{data::get_db_paths, error, MIN_CONFIRMATIONS};
// Options accepted for the `balance` command
#[derive(Debug, Options)]
pub(crate) struct Command {}
impl Command {
pub(crate) fn run(
self,
params: impl Parameters + Copy + 'static,
wallet_dir: Option<String>,
) -> Result<(), anyhow::Error> {
let account = AccountId::from(0);
let (_, db_data) = get_db_paths(wallet_dir);
let db_data = WalletDb::for_path(db_data, params)?;
let (target_height, _) = db_data
.get_target_and_anchor_heights(MIN_CONFIRMATIONS)?
.ok_or(error::WalletErrorT::ScanRequired)
.map_err(|e| anyhow!("{:?}", e))?;
let notes = db_data.get_spendable_sapling_notes(account, target_height, &[])?;
for note in notes {
println!("{}: {} zatoshis", note.note_id, u64::from(note.note_value));
}
Ok(())
}
}

View File

@ -43,6 +43,12 @@ enum Command {
#[options(help = "get the balance in the wallet")]
Balance(commands::balance::Command),
#[options(help = "list the transactions in the wallet")]
ListTx(commands::list_tx::Command),
#[options(help = "list the unspent notes in the wallet")]
ListUnspent(commands::list_unspent::Command),
#[options(help = "send funds to the given address")]
Send(commands::send::Command),
}
@ -75,6 +81,8 @@ fn main() -> Result<(), anyhow::Error> {
Some(Command::Upgrade(command)) => command.run(params, opts.wallet_dir),
Some(Command::Sync(command)) => command.run(params, opts.wallet_dir).await,
Some(Command::Balance(command)) => command.run(params, opts.wallet_dir),
Some(Command::ListTx(command)) => command.run(opts.wallet_dir),
Some(Command::ListUnspent(command)) => command.run(params, opts.wallet_dir),
Some(Command::Send(command)) => command.run(params, opts.wallet_dir).await,
_ => Ok(()),
}