Skip to content

Commit eb6e543

Browse files
committed
Add gettransaction
Closes #38.
1 parent c5f09c9 commit eb6e543

File tree

5 files changed

+636
-1
lines changed

5 files changed

+636
-1
lines changed

zallet/src/components/json_rpc.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use crate::{
1717
use super::{TaskHandle, chain_view::ChainView, database::Database, keystore::KeyStore};
1818

1919
mod asyncop;
20+
mod balance;
2021
pub(crate) mod methods;
2122
mod payments;
2223
pub(crate) mod server;
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
use rusqlite::named_params;
2+
use transparent::bundle::TxOut;
3+
use zaino_state::{FetchServiceSubscriber, MempoolKey};
4+
use zcash_client_backend::data_api::WalletRead;
5+
use zcash_client_sqlite::error::SqliteClientError;
6+
use zcash_primitives::transaction::Transaction;
7+
use zcash_protocol::{consensus::BlockHeight, value::Zatoshis};
8+
9+
use crate::components::database::DbConnection;
10+
11+
/// Equivalent to `CTransaction::GetValueOut` in `zcashd`.
12+
pub(super) fn wtx_get_value_out(tx: &Transaction) -> Option<Zatoshis> {
13+
tx.transparent_bundle()
14+
.map(|bundle| bundle.vout.iter().map(|txout| txout.value).sum())
15+
.unwrap_or(Some(Zatoshis::ZERO))
16+
}
17+
18+
/// Equivalent to [`CWalletTx::GetDebit`] in `zcashd`.
19+
///
20+
/// [`CWalletTx::GetDebit`]: https://github.com/zcash/zcash/blob/2352fbc1ed650ac4369006bea11f7f20ee046b84/src/wallet/wallet.cpp#L4822
21+
pub(super) fn wtx_get_debit(
22+
wallet: &DbConnection,
23+
tx: &Transaction,
24+
is_mine: impl Fn(&DbConnection, &TxOut) -> bool,
25+
) -> Option<Zatoshis> {
26+
match tx.transparent_bundle() {
27+
None => Some(Zatoshis::ZERO),
28+
Some(bundle) if bundle.vin.is_empty() => Some(Zatoshis::ZERO),
29+
// Equivalent to `CWallet::GetDebit(CTransaction)` in `zcashd`.
30+
Some(bundle) => bundle
31+
.vin
32+
.iter()
33+
.map(|txin| {
34+
// Equivalent to `CWallet::GetDebit(CTxIn)` in `zcashd`.
35+
wallet
36+
.get_transaction(*txin.prevout.txid())
37+
.ok()
38+
.flatten()
39+
.as_ref()
40+
.and_then(|prev_tx| prev_tx.transparent_bundle())
41+
.and_then(|bundle| bundle.vout.get(txin.prevout.n() as usize))
42+
.filter(|txout| is_mine(wallet, txout))
43+
.map(|txout| txout.value)
44+
.unwrap_or(Zatoshis::ZERO)
45+
})
46+
.sum::<Option<Zatoshis>>(),
47+
}
48+
}
49+
50+
/// Equivalent to [`CWalletTx::GetCredit`] in `zcashd`.
51+
///
52+
/// [`CWalletTx::GetCredit`]: https://github.com/zcash/zcash/blob/2352fbc1ed650ac4369006bea11f7f20ee046b84/src/wallet/wallet.cpp#L4853
53+
pub(super) async fn wtx_get_credit(
54+
wallet: &DbConnection,
55+
chain: &FetchServiceSubscriber,
56+
tx: &Transaction,
57+
as_of_height: Option<BlockHeight>,
58+
is_mine: impl Fn(&DbConnection, &TxOut) -> bool,
59+
) -> Result<Option<Zatoshis>, SqliteClientError> {
60+
Ok(match tx.transparent_bundle() {
61+
None => Some(Zatoshis::ZERO),
62+
// Must wait until coinbase is safely deep enough in the chain before valuing it.
63+
Some(bundle)
64+
if bundle.is_coinbase()
65+
&& wtx_get_blocks_to_maturity(wallet, chain, tx, as_of_height).await? > 0 =>
66+
{
67+
Some(Zatoshis::ZERO)
68+
}
69+
// Equivalent to `CWallet::GetCredit(CTransaction)` in `zcashd`.
70+
Some(bundle) => bundle
71+
.vout
72+
.iter()
73+
.map(|txout| {
74+
// Equivalent to `CWallet::GetCredit(CTxOut)` in `zcashd`.
75+
if is_mine(wallet, txout) {
76+
txout.value
77+
} else {
78+
Zatoshis::ZERO
79+
}
80+
})
81+
.sum::<Option<Zatoshis>>(),
82+
})
83+
}
84+
85+
/// Equivalent to [`CWalletTx::IsFromMe`] in `zcashd`.
86+
///
87+
/// [`CWalletTx::IsFromMe`]: https://github.com/zcash/zcash/blob/2352fbc1ed650ac4369006bea11f7f20ee046b84/src/wallet/wallet.cpp#L4967
88+
pub(super) fn wtx_is_from_me(
89+
wallet: &DbConnection,
90+
tx: &Transaction,
91+
is_mine: impl Fn(&DbConnection, &TxOut) -> bool,
92+
) -> Result<bool, SqliteClientError> {
93+
if wtx_get_debit(wallet, tx, is_mine).ok_or_else(|| {
94+
SqliteClientError::BalanceError(zcash_protocol::value::BalanceError::Overflow)
95+
})? > Zatoshis::ZERO
96+
{
97+
return Ok(true);
98+
}
99+
100+
wallet.with_raw(|conn| {
101+
if let Some(bundle) = tx.sapling_bundle() {
102+
let mut stmt_note_exists = conn.prepare(
103+
"SELECT EXISTS(
104+
SELECT 1
105+
FROM sapling_received_notes
106+
WHERE nf = :nf
107+
)",
108+
)?;
109+
110+
for spend in bundle.shielded_spends() {
111+
if stmt_note_exists
112+
.query_row(named_params! {":nf": spend.nullifier().0}, |row| row.get(0))?
113+
{
114+
return Ok(true);
115+
}
116+
}
117+
}
118+
119+
// TODO: Fix bug in `zcashd` where we forgot to add Orchard here.
120+
if let Some(bundle) = tx.orchard_bundle() {
121+
let mut stmt_note_exists = conn.prepare(
122+
"SELECT EXISTS(
123+
SELECT 1
124+
FROM orchard_received_notes
125+
WHERE nf = :nf
126+
)",
127+
)?;
128+
129+
for action in bundle.actions() {
130+
if stmt_note_exists.query_row(
131+
named_params! {":nf": action.nullifier().to_bytes()},
132+
|row| row.get(0),
133+
)? {
134+
return Ok(true);
135+
}
136+
}
137+
}
138+
139+
Ok(false)
140+
})
141+
}
142+
143+
/// Equivalent to [`CMerkleTx::GetBlocksToMaturity`] in `zcashd`.
144+
///
145+
/// [`CMerkleTx::GetBlocksToMaturity`]: https://github.com/zcash/zcash/blob/2352fbc1ed650ac4369006bea11f7f20ee046b84/src/wallet/wallet.cpp#L6915
146+
async fn wtx_get_blocks_to_maturity(
147+
wallet: &DbConnection,
148+
chain: &FetchServiceSubscriber,
149+
tx: &Transaction,
150+
as_of_height: Option<BlockHeight>,
151+
) -> Result<u32, SqliteClientError> {
152+
Ok(
153+
if tx.transparent_bundle().map_or(false, |b| b.is_coinbase()) {
154+
if let Some(depth) =
155+
wtx_get_depth_in_main_chain(wallet, chain, tx, as_of_height).await?
156+
{
157+
(COINBASE_MATURITY + 1).saturating_sub(depth)
158+
} else {
159+
// TODO: Confirm this is what `zcashd` computes for an orphaned coinbase.
160+
COINBASE_MATURITY + 2
161+
}
162+
} else {
163+
0
164+
},
165+
)
166+
}
167+
168+
/// Returns depth of transaction in blockchain.
169+
///
170+
/// - `None` : not in blockchain, and not in memory pool (conflicted transaction)
171+
/// - `Some(0)` : in memory pool, waiting to be included in a block (never returned if `as_of_height` is set)
172+
/// - `Some(1..)` : this many blocks deep in the main chain
173+
async fn wtx_get_depth_in_main_chain(
174+
wallet: &DbConnection,
175+
chain: &FetchServiceSubscriber,
176+
tx: &Transaction,
177+
as_of_height: Option<BlockHeight>,
178+
) -> Result<Option<u32>, SqliteClientError> {
179+
let chain_height = wallet
180+
.chain_height()?
181+
.ok_or_else(|| SqliteClientError::ChainHeightUnknown)?;
182+
183+
let effective_chain_height = chain_height.min(as_of_height.unwrap_or(chain_height));
184+
185+
let depth = if let Some(mined_height) = wallet.get_tx_height(tx.txid())? {
186+
Some(effective_chain_height + 1 - mined_height)
187+
} else if as_of_height.is_none()
188+
&& chain
189+
.mempool
190+
.contains_txid(&MempoolKey(tx.txid().to_string()))
191+
.await
192+
{
193+
Some(0)
194+
} else {
195+
None
196+
};
197+
198+
Ok(depth)
199+
}
200+
201+
const COINBASE_MATURITY: u32 = 100;

zallet/src/components/json_rpc/methods.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ mod get_address_for_account;
1919
mod get_new_account;
2020
mod get_notes_count;
2121
mod get_operation;
22+
mod get_transaction;
2223
mod get_wallet_info;
2324
mod help;
2425
mod list_accounts;
@@ -210,6 +211,37 @@ pub(crate) trait Rpc {
210211
#[method(name = "z_listunifiedreceivers")]
211212
fn list_unified_receivers(&self, unified_address: &str) -> list_unified_receivers::Response;
212213

214+
/// Returns detailed information about in-wallet transaction `txid`.
215+
///
216+
/// This does not include complete information about shielded components of the
217+
/// transaction; to obtain details about shielded components of the transaction use
218+
/// `z_viewtransaction`.
219+
///
220+
/// # Parameters
221+
///
222+
/// - `includeWatchonly` (bool, optional, default=false): Whether to include watchonly
223+
/// addresses in balance calculation and `details`.
224+
/// - `verbose`: Must be `false` or omitted.
225+
/// - `asOfHeight` (numeric, optional, default=-1): Execute the query as if it were
226+
/// run when the blockchain was at the height specified by this argument. The
227+
/// default is to use the entire blockchain that the node is aware of. -1 can be
228+
/// used as in other RPC calls to indicate the current height (including the
229+
/// mempool), but this does not support negative values in general. A "future"
230+
/// height will fall back to the current height. Any explicit value will cause the
231+
/// mempool to be ignored, meaning no unconfirmed tx will be considered.
232+
///
233+
/// # Bitcoin compatibility
234+
///
235+
/// Compatible up to three arguments, but can only use the default value for `verbose`.
236+
#[method(name = "gettransaction")]
237+
async fn get_transaction(
238+
&self,
239+
txid: &str,
240+
include_watchonly: Option<bool>,
241+
verbose: Option<bool>,
242+
as_of_height: Option<i64>,
243+
) -> get_transaction::Response;
244+
213245
/// Returns detailed shielded information about in-wallet transaction `txid`.
214246
#[method(name = "z_viewtransaction")]
215247
async fn view_transaction(&self, txid: &str) -> view_transaction::Response;
@@ -441,6 +473,24 @@ impl RpcServer for RpcImpl {
441473
list_unified_receivers::call(unified_address)
442474
}
443475

476+
async fn get_transaction(
477+
&self,
478+
txid: &str,
479+
include_watchonly: Option<bool>,
480+
verbose: Option<bool>,
481+
as_of_height: Option<i64>,
482+
) -> get_transaction::Response {
483+
get_transaction::call(
484+
self.wallet().await?.as_ref(),
485+
self.chain().await?,
486+
txid,
487+
include_watchonly.unwrap_or(false),
488+
verbose.unwrap_or(false),
489+
as_of_height,
490+
)
491+
.await
492+
}
493+
444494
async fn view_transaction(&self, txid: &str) -> view_transaction::Response {
445495
view_transaction::call(self.wallet().await?.as_ref(), txid)
446496
}

0 commit comments

Comments
 (0)