|
| 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