Skip to content

Commit e968d90

Browse files
committed
Add gettransaction and z_viewtransaction
Closes #38.
1 parent 1eabd80 commit e968d90

File tree

3 files changed

+575
-0
lines changed

3 files changed

+575
-0
lines changed

zallet/src/components/json_rpc/methods.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ use crate::components::{
1313

1414
mod get_address_for_account;
1515
mod get_notes_count;
16+
mod get_transaction;
1617
mod get_wallet_info;
1718
mod list_accounts;
1819
mod list_addresses;
1920
mod list_unified_receivers;
2021
mod list_unspent;
2122
mod lock_wallet;
2223
mod unlock_wallet;
24+
mod view_transaction;
2325

2426
#[rpc(server)]
2527
pub(crate) trait Rpc {
@@ -102,6 +104,41 @@ pub(crate) trait Rpc {
102104
#[method(name = "z_listunifiedreceivers")]
103105
fn list_unified_receivers(&self, unified_address: &str) -> list_unified_receivers::Response;
104106

107+
/// Returns detailed information about in-wallet transaction `txid`.
108+
///
109+
/// This does not include complete information about shielded components of the
110+
/// transaction; to obtain details about shielded components of the transaction use
111+
/// `z_viewtransaction`.
112+
///
113+
/// # Parameters
114+
///
115+
/// - `includeWatchonly` (bool, optional, default=false): Whether to include watchonly
116+
/// addresses in balance calculation and `details`.
117+
/// - `verbose`: Must be `false` or omitted.
118+
/// - `asOfHeight` (numeric, optional, default=-1): Execute the query as if it were
119+
/// run when the blockchain was at the height specified by this argument. The
120+
/// default is to use the entire blockchain that the node is aware of. -1 can be
121+
/// used as in other RPC calls to indicate the current height (including the
122+
/// mempool), but this does not support negative values in general. A “future”
123+
/// height will fall back to the current height. Any explicit value will cause the
124+
/// mempool to be ignored, meaning no unconfirmed tx will be considered.
125+
///
126+
/// # Bitcoin compatibility
127+
///
128+
/// Compatible up to three arguments, but can only use the default value for `verbose`.
129+
#[method(name = "gettransaction")]
130+
async fn get_transaction(
131+
&self,
132+
txid: &str,
133+
include_watchonly: Option<bool>,
134+
verbose: Option<bool>,
135+
as_of_height: Option<i64>,
136+
) -> get_transaction::Response;
137+
138+
/// Returns detailed shielded information about in-wallet transaction `txid`.
139+
#[method(name = "z_viewtransaction")]
140+
async fn view_transaction(&self, txid: &str) -> view_transaction::Response;
141+
105142
/// Returns an array of unspent shielded notes with between minconf and maxconf
106143
/// (inclusive) confirmations.
107144
///
@@ -198,6 +235,26 @@ impl RpcServer for RpcImpl {
198235
list_unified_receivers::call(unified_address)
199236
}
200237

238+
async fn get_transaction(
239+
&self,
240+
txid: &str,
241+
include_watchonly: Option<bool>,
242+
verbose: Option<bool>,
243+
as_of_height: Option<i64>,
244+
) -> get_transaction::Response {
245+
get_transaction::call(
246+
self.wallet().await?.as_ref(),
247+
txid,
248+
include_watchonly.unwrap_or(false),
249+
verbose.unwrap_or(false),
250+
as_of_height.unwrap_or(-1),
251+
)
252+
}
253+
254+
async fn view_transaction(&self, txid: &str) -> view_transaction::Response {
255+
view_transaction::call(self.wallet().await?.as_ref(), txid)
256+
}
257+
201258
async fn list_unspent(&self) -> list_unspent::Response {
202259
list_unspent::call(self.wallet().await?.as_ref())
203260
}
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
use jsonrpsee::{
2+
core::RpcResult,
3+
types::{ErrorCode as RpcErrorCode, ErrorObjectOwned as RpcError},
4+
};
5+
use serde::Serialize;
6+
use zcash_client_backend::data_api::WalletRead;
7+
use zcash_protocol::{consensus::BlockHeight, TxId};
8+
9+
use crate::components::{database::DbConnection, json_rpc::server::LegacyCode};
10+
11+
/// Response to a `gettransaction` RPC request.
12+
pub(crate) type Response = RpcResult<Transaction>;
13+
14+
#[derive(Clone, Debug, Serialize)]
15+
pub(crate) struct Transaction {
16+
/// The transaction ID.
17+
txid: String,
18+
19+
/// The transaction status.
20+
///
21+
/// One of 'mined', 'waiting', 'expiringsoon' or 'expired'.
22+
status: &'static str,
23+
24+
/// The transaction version.
25+
version: String,
26+
27+
/// The transaction amount in ZEC.
28+
amount: f64,
29+
30+
/// The amount in zatoshis.
31+
#[serde(rename = "amountZat")]
32+
amount_zat: u64,
33+
34+
#[serde(skip_serializing_if = "Option::is_none")]
35+
fee: Option<u64>,
36+
37+
/// The number of confirmations.
38+
///
39+
/// - A positive value is the number of blocks that have been mined including the
40+
/// transaction in the chain. For example, 1 confirmation means the transaction is
41+
/// in the block currently at the chain tip.
42+
/// - 0 means the transaction is in the mempool. If `asOfHeight` was set, this case
43+
/// will not occur.
44+
/// - -1 means the transaction cannot be mined.
45+
confirmations: i32,
46+
47+
#[serde(skip_serializing_if = "Option::is_none")]
48+
generated: Option<bool>,
49+
50+
/// The block hash.
51+
#[serde(skip_serializing_if = "Option::is_none")]
52+
blockhash: Option<String>,
53+
54+
/// The block index.
55+
#[serde(skip_serializing_if = "Option::is_none")]
56+
blockindex: Option<u16>,
57+
58+
/// The time in seconds since epoch (1 Jan 1970 GMT).
59+
#[serde(skip_serializing_if = "Option::is_none")]
60+
blocktime: Option<u64>,
61+
62+
#[serde(skip_serializing_if = "Option::is_none")]
63+
expiryheight: Option<u64>,
64+
65+
walletconflicts: Vec<String>,
66+
67+
/// The transaction time in seconds since epoch (1 Jan 1970 GMT).
68+
time: u64,
69+
70+
/// The time received in seconds since epoch (1 Jan 1970 GMT).
71+
timereceived: u64,
72+
73+
details: Vec<Detail>,
74+
75+
/// Raw data for transaction.
76+
hex: String,
77+
}
78+
79+
#[derive(Clone, Debug, Serialize)]
80+
struct Detail {
81+
/// The Zcash address involved in the transaction.
82+
address: String,
83+
84+
/// The category.
85+
///
86+
/// One of 'send' or 'receive'.
87+
category: String,
88+
89+
/// The amount in ZEC.
90+
amount: f64,
91+
92+
/// The amount in zatoshis.
93+
#[serde(rename = "amountZat")]
94+
amount_zat: u64,
95+
96+
/// The vout value.
97+
vout: u64,
98+
}
99+
100+
pub(crate) fn call(
101+
wallet: &DbConnection,
102+
txid_str: &str,
103+
include_watchonly: bool,
104+
verbose: bool,
105+
as_of_height: i64,
106+
) -> Response {
107+
let txid: TxId = txid_str.parse()?;
108+
109+
if verbose {
110+
return Err(LegacyCode::InvalidParameter.with_static("verbose must be set to false"));
111+
}
112+
113+
let as_of_height = match as_of_height {
114+
// The default, do nothing.
115+
-1 => Ok(None),
116+
..0 => Err(LegacyCode::InvalidParameter
117+
.with_static("Can not perform the query as of a negative block height")),
118+
0 => Err(LegacyCode::InvalidParameter
119+
.with_static("Can not perform the query as of the genesis block")),
120+
1.. => u32::try_from(as_of_height).map(Some).map_err(|_| {
121+
LegacyCode::InvalidParameter.with_static("asOfHeight parameter is too big")
122+
}),
123+
}?;
124+
125+
let tx = wallet
126+
.get_transaction(txid)
127+
.map_err(|e| LegacyCode::Database.with_message(e.to_string()))?
128+
.ok_or(LegacyCode::InvalidParameter.with_static("Invalid or non-wallet transaction id"))?;
129+
130+
let chain_height = wallet
131+
.chain_height()
132+
.map_err(|e| LegacyCode::Database.with_message(e.to_string()))?
133+
.ok_or_else(|| LegacyCode::InWarmup.into())?;
134+
135+
let mined_height = wallet
136+
.get_tx_height(txid)
137+
.map_err(|e| LegacyCode::Database.with_message(e.to_string()))?;
138+
139+
let confirmations = {
140+
let effective_chain_height = as_of_height
141+
.map(BlockHeight::from_u32)
142+
.unwrap_or(chain_height)
143+
.min(chain_height);
144+
match mined_height {
145+
Some(mined_height) => (effective_chain_height + 1 - mined_height) as i32,
146+
None => {
147+
// TODO: Also check if the transaction is in the mempool for this branch.
148+
if as_of_height.is_some() {
149+
-1
150+
} else {
151+
0
152+
}
153+
}
154+
}
155+
};
156+
157+
let generated = if tx
158+
.transparent_bundle()
159+
.is_some_and(|bundle| bundle.is_coinbase())
160+
{
161+
Some(true)
162+
} else {
163+
None
164+
};
165+
166+
let mut status = "waiting";
167+
168+
let (blockhash, blockindex, blocktime, expiryheight) = if let Some(height) = mined_height {
169+
status = "mined";
170+
let block = wallet
171+
.block_metadata(height)
172+
.map_err(|e| LegacyCode::Database.with_message(e.to_string()))?
173+
// This would be a race condition between this and a reorg.
174+
.ok_or(RpcErrorCode::InternalError)?;
175+
(
176+
Some(block.block_hash().to_string()),
177+
None, // TODO: Some(wtx.nIndex),
178+
None, // TODO: Some(mapBlockIndex[wtx.hashBlock].GetBlockTime()),
179+
Some(tx.expiry_height().into()),
180+
)
181+
} else {
182+
match (
183+
is_expired_tx(&tx, chain_height),
184+
is_expiring_soon_tx(&tx, chain_height + 1),
185+
) {
186+
(false, true) => status = "expiringsoon",
187+
(true, _) => status = "expired",
188+
_ => (),
189+
}
190+
(None, None, None, None)
191+
};
192+
193+
let walletconflicts = vec![];
194+
195+
let details = vec![];
196+
197+
let hex_tx = {
198+
let mut bytes = vec![];
199+
tx.write(&mut bytes).expect("can write to Vec");
200+
hex::encode(bytes)
201+
};
202+
203+
Ok(Transaction {
204+
txid: txid_str.into(),
205+
status,
206+
version: tx.version().header() & 0x7FFFFFFF,
207+
amount: (),
208+
amount_zat: (),
209+
fee: None,
210+
confirmations,
211+
generated,
212+
blockhash,
213+
blockindex,
214+
blocktime,
215+
expiryheight,
216+
walletconflicts,
217+
time: (),
218+
timereceived: (),
219+
details,
220+
hex: hex_tx,
221+
})
222+
}
223+
224+
/// The number of blocks within expiry height when a tx is considered to be expiring soon.
225+
const TX_EXPIRING_SOON_THRESHOLD: u32 = 3;
226+
227+
fn is_expired_tx(tx: &zcash_primitives::transaction::Transaction, height: BlockHeight) -> bool {
228+
if tx.expiry_height() == 0.into()
229+
|| tx
230+
.transparent_bundle()
231+
.is_some_and(|bundle| bundle.is_coinbase())
232+
{
233+
false
234+
} else {
235+
height > tx.expiry_height()
236+
}
237+
}
238+
239+
fn is_expiring_soon_tx(
240+
tx: &zcash_primitives::transaction::Transaction,
241+
next_height: BlockHeight,
242+
) -> bool {
243+
is_expired_tx(tx, next_height + TX_EXPIRING_SOON_THRESHOLD)
244+
}

0 commit comments

Comments
 (0)