Skip to content

Commit d01c42f

Browse files
committed
Add gettransaction and z_viewtransaction
Closes #38.
1 parent 75c8e85 commit d01c42f

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
@@ -4,10 +4,12 @@ use jsonrpsee::{core::RpcResult, proc_macros::rpc};
44
use crate::components::database::{Database, DbHandle};
55

66
mod get_notes_count;
7+
mod get_transaction;
78
mod get_wallet_info;
89
mod list_accounts;
910
mod list_unified_receivers;
1011
mod list_unspent;
12+
mod view_transaction;
1113

1214
#[rpc(server)]
1315
pub(crate) trait Rpc {
@@ -20,6 +22,41 @@ pub(crate) trait Rpc {
2022
#[method(name = "z_listunifiedreceivers")]
2123
fn list_unified_receivers(&self, unified_address: &str) -> list_unified_receivers::Response;
2224

25+
/// Returns detailed information about in-wallet transaction `txid`.
26+
///
27+
/// This does not include complete information about shielded components of the
28+
/// transaction; to obtain details about shielded components of the transaction use
29+
/// `z_viewtransaction`.
30+
///
31+
/// # Parameters
32+
///
33+
/// - `includeWatchonly` (bool, optional, default=false): Whether to include watchonly
34+
/// addresses in balance calculation and `details`.
35+
/// - `verbose`: Must be `false` or omitted.
36+
/// - `asOfHeight` (numeric, optional, default=-1): Execute the query as if it were
37+
/// run when the blockchain was at the height specified by this argument. The
38+
/// default is to use the entire blockchain that the node is aware of. -1 can be
39+
/// used as in other RPC calls to indicate the current height (including the
40+
/// mempool), but this does not support negative values in general. A “future”
41+
/// height will fall back to the current height. Any explicit value will cause the
42+
/// mempool to be ignored, meaning no unconfirmed tx will be considered.
43+
///
44+
/// # Bitcoin compatibility
45+
///
46+
/// Compatible up to three arguments, but can only use the default value for `verbose`.
47+
#[method(name = "gettransaction")]
48+
async fn get_transaction(
49+
&self,
50+
txid: &str,
51+
include_watchonly: Option<bool>,
52+
verbose: Option<bool>,
53+
as_of_height: Option<i64>,
54+
) -> get_transaction::Response;
55+
56+
/// Returns detailed shielded information about in-wallet transaction `txid`.
57+
#[method(name = "z_viewtransaction")]
58+
async fn view_transaction(&self, txid: &str) -> view_transaction::Response;
59+
2360
/// Returns an array of unspent shielded notes with between minconf and maxconf
2461
/// (inclusive) confirmations.
2562
///
@@ -72,6 +109,26 @@ impl RpcServer for RpcImpl {
72109
list_unified_receivers::call(unified_address)
73110
}
74111

112+
async fn get_transaction(
113+
&self,
114+
txid: &str,
115+
include_watchonly: Option<bool>,
116+
verbose: Option<bool>,
117+
as_of_height: Option<i64>,
118+
) -> get_transaction::Response {
119+
get_transaction::call(
120+
self.wallet().await?.as_ref(),
121+
txid,
122+
include_watchonly.unwrap_or(false),
123+
verbose.unwrap_or(false),
124+
as_of_height.unwrap_or(-1),
125+
)
126+
}
127+
128+
async fn view_transaction(&self, txid: &str) -> view_transaction::Response {
129+
view_transaction::call(self.wallet().await?.as_ref(), txid)
130+
}
131+
75132
async fn list_unspent(&self) -> list_unspent::Response {
76133
list_unspent::call(self.wallet().await?.as_ref())
77134
}
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)