Skip to main content

zebra_rpc/methods/types/
transaction.rs

1//! Transaction-related types.
2
3use std::sync::Arc;
4
5use crate::methods::arrayhex;
6use chrono::{DateTime, Utc};
7use derive_getters::Getters;
8use derive_new::new;
9use hex::ToHex;
10use rand::rngs::OsRng;
11use zcash_script::script::Asm;
12
13use zcash_keys::address::Address;
14use zcash_primitives::transaction::{
15    builder::{BuildConfig, Builder},
16    fees::fixed::FeeRule,
17};
18use zcash_proofs::prover::LocalTxProver;
19use zcash_protocol::{consensus::BlockHeight, memo::MemoBytes, value::Zatoshis};
20use zebra_chain::{
21    amount::{self, Amount, NegativeAllowed, NegativeOrZero, NonNegative},
22    block::{self, merkle::AUTH_DIGEST_PLACEHOLDER, Height},
23    orchard,
24    parameters::{
25        subsidy::{block_subsidy, funding_stream_values, miner_subsidy},
26        Network, NetworkUpgrade,
27    },
28    primitives::ed25519,
29    sapling::ValueCommitment,
30    serialization::ZcashSerialize,
31    transaction::{self, SerializedTransaction, Transaction, VerifiedUnminedTx},
32    transparent::Script,
33};
34use zebra_consensus::{error::TransactionError, funding_stream_address};
35use zebra_script::Sigops;
36use zebra_state::IntoDisk;
37
38use super::zec::Zec;
39use super::{super::opthex, get_block_template::MinerParams};
40
41/// Transaction data and fields needed to generate blocks using the `getblocktemplate` RPC.
42#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
43#[serde(bound = "FeeConstraint: amount::Constraint + Clone")]
44pub struct TransactionTemplate<FeeConstraint>
45where
46    FeeConstraint: amount::Constraint + Clone + Copy,
47{
48    /// The hex-encoded serialized data for this transaction.
49    #[serde(with = "hex")]
50    pub(crate) data: SerializedTransaction,
51
52    /// The transaction ID of this transaction.
53    #[serde(with = "hex")]
54    #[getter(copy)]
55    pub(crate) hash: transaction::Hash,
56
57    /// The authorizing data digest of a v5 transaction, or a placeholder for older versions.
58    #[serde(rename = "authdigest")]
59    #[serde(with = "hex")]
60    #[getter(copy)]
61    pub(crate) auth_digest: transaction::AuthDigest,
62
63    /// The transactions in this block template that this transaction depends upon.
64    /// These are 1-based indexes in the `transactions` list.
65    ///
66    /// Zebra's mempool does not support transaction dependencies, so this list is always empty.
67    ///
68    /// We use `u16` because 2 MB blocks are limited to around 39,000 transactions.
69    pub(crate) depends: Vec<u16>,
70
71    /// The fee for this transaction.
72    ///
73    /// Non-coinbase transactions must be `NonNegative`.
74    /// The Coinbase transaction `fee` is the negative sum of the fees of the transactions in
75    /// the block, so their fee must be `NegativeOrZero`.
76    #[getter(copy)]
77    pub(crate) fee: Amount<FeeConstraint>,
78
79    /// The number of transparent signature operations in this transaction.
80    pub(crate) sigops: u32,
81
82    /// Is this transaction required in the block?
83    ///
84    /// Coinbase transactions are required, all other transactions are not.
85    pub(crate) required: bool,
86}
87
88// Convert from a mempool transaction to a non-coinbase transaction template.
89impl From<&VerifiedUnminedTx> for TransactionTemplate<NonNegative> {
90    fn from(tx: &VerifiedUnminedTx) -> Self {
91        assert!(
92            !tx.transaction.transaction.is_coinbase(),
93            "unexpected coinbase transaction in mempool"
94        );
95
96        Self {
97            data: tx.transaction.transaction.as_ref().into(),
98            hash: tx.transaction.id.mined_id(),
99            auth_digest: tx
100                .transaction
101                .id
102                .auth_digest()
103                .unwrap_or(AUTH_DIGEST_PLACEHOLDER),
104
105            // Always empty, not supported by Zebra's mempool.
106            depends: Vec::new(),
107
108            fee: tx.miner_fee,
109
110            // Report the full block-level sigop count (legacy + P2SH) so the template `sigops`
111            // field matches what the block verifier charges against `MAX_BLOCK_SIGOPS`.
112            sigops: tx.block_sigop_count(),
113
114            // Zebra does not require any transactions except the coinbase transaction.
115            required: false,
116        }
117    }
118}
119
120impl From<VerifiedUnminedTx> for TransactionTemplate<NonNegative> {
121    fn from(tx: VerifiedUnminedTx) -> Self {
122        Self::from(&tx)
123    }
124}
125
126impl TransactionTemplate<NegativeOrZero> {
127    /// Constructs a transaction template for a coinbase transaction.
128    pub fn new_coinbase(
129        net: &Network,
130        height: Height,
131        miner_params: &MinerParams,
132        txs_fee: Amount<NonNegative>,
133    ) -> Result<Self, TransactionError> {
134        let block_subsidy = block_subsidy(height, net)?;
135        let miner_reward = miner_subsidy(height, net, block_subsidy)? + txs_fee;
136        let miner_reward = Zatoshis::try_from(miner_reward?)?;
137
138        let mut builder = Builder::new(
139            net,
140            BlockHeight::from(height),
141            BuildConfig::Coinbase {
142                miner_data: miner_params.data().clone(),
143            },
144        );
145
146        let default_memo = MemoBytes::empty();
147        let memo = miner_params.memo().unwrap_or(&default_memo);
148
149        // ZIP-233 was dropped from the v6 transaction format, so no burn amount is set here. If the
150        // Network Sustainability Mechanism re-introduces a burn, it will be plumbed back through
151        // explicitly at that point.
152
153        macro_rules! trace_err {
154            ($res:expr, $type:expr) => {
155                $res.map_err(|err| tracing::error!("Failed to add {} output: {err}", $type))
156                    .ok()
157            };
158        }
159
160        // On NU6.3 onward the coinbase MUST have an empty Orchard component, and newly shielded
161        // coinbase value is routed to the Ironwood pool instead (see the Ironwood pool spec and
162        // `coinbase_orchard_component_empty` in zebra-consensus). Ironwood outputs use the same
163        // Orchard-shaped `orchard::Address` as their recipient, so a unified miner address with an
164        // Orchard receiver just gets routed to the Ironwood output builder from NU6.3 onward.
165        let use_ironwood = NetworkUpgrade::current(net, height) >= NetworkUpgrade::Nu6_3;
166
167        let add_shielded_reward = |builder: &mut Builder<_, _>, addr: &_| {
168            let ovk = Some(::orchard::keys::OutgoingViewingKey::from([0u8; 32]));
169            if use_ironwood {
170                trace_err!(
171                    builder.add_ironwood_output::<String>(ovk, *addr, miner_reward, memo.clone()),
172                    "Ironwood"
173                )
174            } else {
175                trace_err!(
176                    builder.add_orchard_output::<String>(ovk, *addr, miner_reward, memo.clone()),
177                    "Orchard"
178                )
179            }
180        };
181
182        let add_sapling_reward = |builder: &mut Builder<_, _>, addr: &_| {
183            trace_err!(
184                builder.add_sapling_output::<String>(
185                    Some(sapling_crypto::keys::OutgoingViewingKey([0u8; 32])),
186                    *addr,
187                    miner_reward,
188                    memo.clone(),
189                ),
190                "Sapling"
191            )
192        };
193
194        let add_transparent_reward = |builder: &mut Builder<_, _>, addr| {
195            trace_err!(
196                builder.add_transparent_output(addr, miner_reward),
197                "transparent"
198            )
199        };
200
201        match miner_params.addr() {
202            Address::Unified(addr) => addr
203                .orchard()
204                .and_then(|addr| add_shielded_reward(&mut builder, addr))
205                .or_else(|| {
206                    addr.sapling()
207                        .and_then(|addr| add_sapling_reward(&mut builder, addr))
208                })
209                .or_else(|| {
210                    addr.transparent()
211                        .and_then(|addr| add_transparent_reward(&mut builder, addr))
212                }),
213
214            Address::Sapling(addr) => add_sapling_reward(&mut builder, addr),
215
216            Address::Transparent(addr) => add_transparent_reward(&mut builder, addr),
217
218            _ => Err(TransactionError::CoinbaseConstruction(
219                "Address not supported for miner rewards".to_string(),
220            ))?,
221        }
222        .ok_or(TransactionError::CoinbaseConstruction(
223            "Could not construct output with miner reward".to_string(),
224        ))?;
225
226        let mut funding_streams = funding_stream_values(height, net, block_subsidy)?
227            .into_iter()
228            .filter_map(|(receiver, amount)| {
229                Some((*funding_stream_address(height, net, receiver)?, amount))
230            })
231            .chain(net.lockbox_disbursements(height))
232            .filter_map(|(addr, amount)| {
233                Some((Zatoshis::try_from(amount).ok()?, addr.try_into().ok()?))
234            })
235            .collect::<Vec<_>>();
236
237        funding_streams.sort();
238
239        for (fs_amount, fs_addr) in funding_streams {
240            builder.add_transparent_output(&fs_addr, fs_amount)?;
241        }
242
243        let sapling_prover = LocalTxProver::bundled();
244        let build_result = builder.build(
245            &Default::default(),
246            Default::default(),
247            Default::default(),
248            OsRng,
249            &sapling_prover,
250            &sapling_prover,
251            &FeeRule::non_standard(Zatoshis::ZERO),
252        )?;
253
254        let tx = build_result.transaction();
255        let mut data = vec![];
256        tx.write(&mut data)?;
257
258        Ok(Self {
259            data: data.into(),
260            hash: tx.txid().as_ref().into(),
261            auth_digest: tx.auth_commitment().as_ref().try_into()?,
262            depends: Vec::new(),
263            fee: (-txs_fee).constrain()?,
264            sigops: tx.sigops()?,
265            required: true,
266        })
267    }
268}
269
270/// A Transaction object as returned by `getrawtransaction` and `getblock` RPC
271/// requests.
272#[allow(clippy::too_many_arguments)]
273#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
274pub struct TransactionObject {
275    /// Whether specified block is in the active chain or not (only present with
276    /// explicit "blockhash" argument)
277    #[serde(skip_serializing_if = "Option::is_none")]
278    #[getter(copy)]
279    pub(crate) in_active_chain: Option<bool>,
280    /// The raw transaction, encoded as hex bytes.
281    #[serde(with = "hex")]
282    pub(crate) hex: SerializedTransaction,
283    /// The height of the block in the best chain that contains the tx, -1 if
284    /// it's in a side chain block, or `None` if the tx is in the mempool.
285    #[serde(skip_serializing_if = "Option::is_none")]
286    #[getter(copy)]
287    pub(crate) height: Option<i32>,
288    /// The height diff between the block containing the tx and the best chain
289    /// tip + 1, 0 if it's in a side chain, or `None` if the tx is in the
290    /// mempool.
291    #[serde(skip_serializing_if = "Option::is_none")]
292    #[getter(copy)]
293    pub(crate) confirmations: Option<i64>,
294
295    /// Transparent inputs of the transaction.
296    #[serde(rename = "vin")]
297    pub(crate) inputs: Vec<Input>,
298
299    /// Transparent outputs of the transaction.
300    #[serde(rename = "vout")]
301    pub(crate) outputs: Vec<Output>,
302
303    /// Sapling spends of the transaction.
304    #[serde(rename = "vShieldedSpend")]
305    pub(crate) shielded_spends: Vec<ShieldedSpend>,
306
307    /// Sapling outputs of the transaction.
308    #[serde(rename = "vShieldedOutput")]
309    pub(crate) shielded_outputs: Vec<ShieldedOutput>,
310
311    /// Transparent outputs of the transaction.
312    #[serde(rename = "vjoinsplit")]
313    pub(crate) joinsplits: Vec<JoinSplit>,
314
315    /// Sapling binding signature of the transaction.
316    #[serde(
317        skip_serializing_if = "Option::is_none",
318        with = "opthex",
319        default,
320        rename = "bindingSig"
321    )]
322    #[getter(copy)]
323    pub(crate) binding_sig: Option<[u8; 64]>,
324
325    /// JoinSplit public key of the transaction.
326    #[serde(
327        skip_serializing_if = "Option::is_none",
328        with = "opthex",
329        default,
330        rename = "joinSplitPubKey"
331    )]
332    #[getter(copy)]
333    pub(crate) joinsplit_pub_key: Option<[u8; 32]>,
334
335    /// JoinSplit signature of the transaction.
336    #[serde(
337        skip_serializing_if = "Option::is_none",
338        with = "opthex",
339        default,
340        rename = "joinSplitSig"
341    )]
342    #[getter(copy)]
343    pub(crate) joinsplit_sig: Option<[u8; ed25519::Signature::BYTE_SIZE]>,
344
345    /// Orchard actions of the transaction.
346    #[serde(rename = "orchard", skip_serializing_if = "Option::is_none")]
347    pub(crate) orchard: Option<Orchard>,
348
349    /// Ironwood actions of the transaction (v6 transactions from NU6.3 onward).
350    ///
351    /// The Ironwood pool reuses the Orchard-shaped bundle, so this uses the same [`Orchard`] object.
352    #[serde(rename = "ironwood", skip_serializing_if = "Option::is_none")]
353    pub(crate) ironwood: Option<Orchard>,
354
355    /// The net value of Sapling Spends minus Outputs in ZEC
356    #[serde(rename = "valueBalance", skip_serializing_if = "Option::is_none")]
357    #[getter(copy)]
358    pub(crate) value_balance: Option<f64>,
359
360    /// The net value of Sapling Spends minus Outputs in zatoshis
361    #[serde(rename = "valueBalanceZat", skip_serializing_if = "Option::is_none")]
362    #[getter(copy)]
363    pub(crate) value_balance_zat: Option<i64>,
364
365    /// The size of the transaction in bytes.
366    #[serde(skip_serializing_if = "Option::is_none")]
367    #[getter(copy)]
368    pub(crate) size: Option<i64>,
369
370    /// The time the transaction was included in a block.
371    #[serde(skip_serializing_if = "Option::is_none")]
372    #[getter(copy)]
373    pub(crate) time: Option<i64>,
374
375    /// The transaction identifier, encoded as hex bytes.
376    #[serde(with = "hex")]
377    #[getter(copy)]
378    pub txid: transaction::Hash,
379
380    /// The transaction's auth digest. For pre-v5 transactions this will be
381    /// ffff..ffff
382    #[serde(
383        rename = "authdigest",
384        with = "opthex",
385        skip_serializing_if = "Option::is_none",
386        default
387    )]
388    #[getter(copy)]
389    pub(crate) auth_digest: Option<transaction::AuthDigest>,
390
391    /// Whether the overwintered flag is set
392    pub(crate) overwintered: bool,
393
394    /// The version of the transaction.
395    pub(crate) version: u32,
396
397    /// The version group ID.
398    #[serde(
399        rename = "versiongroupid",
400        with = "opthex",
401        skip_serializing_if = "Option::is_none",
402        default
403    )]
404    pub(crate) version_group_id: Option<Vec<u8>>,
405
406    /// The lock time
407    #[serde(rename = "locktime")]
408    pub(crate) lock_time: u32,
409
410    /// The block height after which the transaction expires.
411    /// Included for Overwinter+ transactions (matching zcashd), omitted for V1/V2.
412    /// See: <https://github.com/zcash/zcash/blob/v6.11.0/src/rpc/rawtransaction.cpp#L224-L226>
413    #[serde(rename = "expiryheight", skip_serializing_if = "Option::is_none")]
414    #[getter(copy)]
415    pub(crate) expiry_height: Option<Height>,
416
417    /// The block hash
418    #[serde(
419        rename = "blockhash",
420        with = "opthex",
421        skip_serializing_if = "Option::is_none",
422        default
423    )]
424    #[getter(copy)]
425    pub(crate) block_hash: Option<block::Hash>,
426
427    /// The block height after which the transaction expires
428    #[serde(rename = "blocktime", skip_serializing_if = "Option::is_none")]
429    #[getter(copy)]
430    pub(crate) block_time: Option<i64>,
431}
432
433/// The transparent input of a transaction.
434#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
435#[serde(untagged)]
436pub enum Input {
437    /// A coinbase input.
438    Coinbase {
439        /// The coinbase scriptSig as hex.
440        #[serde(with = "hex")]
441        coinbase: Vec<u8>,
442        /// The script sequence number.
443        sequence: u32,
444    },
445    /// A non-coinbase input.
446    NonCoinbase {
447        /// The transaction id.
448        txid: String,
449        /// The vout index.
450        vout: u32,
451        /// The script.
452        #[serde(rename = "scriptSig")]
453        script_sig: ScriptSig,
454        /// The script sequence number.
455        sequence: u32,
456        /// The value of the output being spent in ZEC.
457        #[serde(skip_serializing_if = "Option::is_none")]
458        value: Option<f64>,
459        /// The value of the output being spent, in zats, named to match zcashd.
460        #[serde(rename = "valueSat", skip_serializing_if = "Option::is_none")]
461        value_zat: Option<i64>,
462        /// The address of the output being spent.
463        #[serde(skip_serializing_if = "Option::is_none")]
464        address: Option<String>,
465    },
466}
467
468/// The transparent output of a transaction.
469#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
470pub struct Output {
471    /// The value in ZEC.
472    value: f64,
473    /// The value in zats.
474    #[serde(rename = "valueZat")]
475    value_zat: i64,
476    /// index.
477    n: u32,
478    /// The scriptPubKey.
479    #[serde(rename = "scriptPubKey")]
480    script_pub_key: ScriptPubKey,
481}
482
483/// The output object returned by `gettxout` RPC requests.
484#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
485pub struct OutputObject {
486    #[serde(rename = "bestblock")]
487    best_block: String,
488    confirmations: u32,
489    value: f64,
490    #[serde(rename = "scriptPubKey")]
491    script_pub_key: ScriptPubKey,
492    version: u32,
493    coinbase: bool,
494}
495impl OutputObject {
496    pub fn from_output(
497        output: &zebra_chain::transparent::Output,
498        best_block: String,
499        confirmations: u32,
500        version: u32,
501        coinbase: bool,
502        network: &Network,
503    ) -> Self {
504        let lock_script = &output.lock_script;
505        let addresses = output.address(network).map(|addr| vec![addr.to_string()]);
506        let req_sigs = addresses.as_ref().map(|a| a.len() as u32);
507
508        let script_pub_key = ScriptPubKey::new(
509            zcash_script::script::Code(lock_script.as_raw_bytes().to_vec()).to_asm(false),
510            lock_script.clone(),
511            req_sigs,
512            zcash_script::script::Code(lock_script.as_raw_bytes().to_vec())
513                .to_component()
514                .ok()
515                .and_then(|c| c.refine().ok())
516                .and_then(|component| zcash_script::solver::standard(&component))
517                .map(|kind| match kind {
518                    zcash_script::solver::ScriptKind::PubKeyHash { .. } => "pubkeyhash",
519                    zcash_script::solver::ScriptKind::ScriptHash { .. } => "scripthash",
520                    zcash_script::solver::ScriptKind::MultiSig { .. } => "multisig",
521                    zcash_script::solver::ScriptKind::NullData { .. } => "nulldata",
522                    zcash_script::solver::ScriptKind::PubKey { .. } => "pubkey",
523                })
524                .unwrap_or("nonstandard")
525                .to_string(),
526            addresses,
527        );
528
529        Self {
530            best_block,
531            confirmations,
532            value: crate::methods::types::zec::Zec::from(output.value()).lossy_zec(),
533            script_pub_key,
534            version,
535            coinbase,
536        }
537    }
538}
539
540/// The scriptPubKey of a transaction output.
541#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
542pub struct ScriptPubKey {
543    /// the asm.
544    asm: String,
545    /// the hex.
546    #[serde(with = "hex")]
547    hex: Script,
548    /// The required sigs.
549    #[serde(rename = "reqSigs")]
550    #[serde(default)]
551    #[serde(skip_serializing_if = "Option::is_none")]
552    #[getter(copy)]
553    req_sigs: Option<u32>,
554    /// The type, eg 'pubkeyhash'.
555    r#type: String,
556    /// The addresses.
557    #[serde(default)]
558    #[serde(skip_serializing_if = "Option::is_none")]
559    addresses: Option<Vec<String>>,
560}
561
562/// The scriptSig of a transaction input.
563#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
564pub struct ScriptSig {
565    /// The asm.
566    asm: String,
567    /// The hex.
568    hex: Script,
569}
570
571/// A Sprout JoinSplit of a transaction.
572#[allow(clippy::too_many_arguments)]
573#[serde_with::serde_as]
574#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
575pub struct JoinSplit {
576    /// Public input value in ZEC.
577    #[serde(rename = "vpub_old")]
578    old_public_value: f64,
579    /// Public input value in zatoshis.
580    #[serde(rename = "vpub_oldZat")]
581    old_public_value_zat: i64,
582    /// Public input value in ZEC.
583    #[serde(rename = "vpub_new")]
584    new_public_value: f64,
585    /// Public input value in zatoshis.
586    #[serde(rename = "vpub_newZat")]
587    new_public_value_zat: i64,
588    /// Merkle root of the Sprout note commitment tree.
589    #[serde(with = "hex")]
590    #[getter(copy)]
591    anchor: [u8; 32],
592    /// The nullifier of the input notes.
593    #[serde_as(as = "Vec<serde_with::hex::Hex>")]
594    nullifiers: Vec<[u8; 32]>,
595    /// The commitments of the output notes.
596    #[serde_as(as = "Vec<serde_with::hex::Hex>")]
597    commitments: Vec<[u8; 32]>,
598    /// The onetime public key used to encrypt the ciphertexts
599    #[serde(rename = "onetimePubKey")]
600    #[serde(with = "hex")]
601    #[getter(copy)]
602    one_time_pubkey: [u8; 32],
603    /// The random seed
604    #[serde(rename = "randomSeed")]
605    #[serde(with = "hex")]
606    #[getter(copy)]
607    random_seed: [u8; 32],
608    /// The input notes MACs.
609    #[serde_as(as = "Vec<serde_with::hex::Hex>")]
610    macs: Vec<[u8; 32]>,
611    /// A zero-knowledge proof using the Sprout circuit.
612    #[serde(with = "hex")]
613    proof: Vec<u8>,
614    /// The output notes ciphertexts.
615    #[serde_as(as = "Vec<serde_with::hex::Hex>")]
616    ciphertexts: Vec<Vec<u8>>,
617}
618
619/// A Sapling spend of a transaction.
620#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
621pub struct ShieldedSpend {
622    /// Value commitment to the input note.
623    #[serde(with = "hex")]
624    #[getter(skip)]
625    cv: ValueCommitment,
626    /// Merkle root of the Sapling note commitment tree.
627    #[serde(with = "hex")]
628    #[getter(copy)]
629    anchor: [u8; 32],
630    /// The nullifier of the input note.
631    #[serde(with = "hex")]
632    #[getter(copy)]
633    nullifier: [u8; 32],
634    /// The randomized public key for spendAuthSig.
635    #[serde(with = "hex")]
636    #[getter(copy)]
637    rk: [u8; 32],
638    /// A zero-knowledge proof using the Sapling Spend circuit.
639    #[serde(with = "hex")]
640    #[getter(copy)]
641    proof: [u8; 192],
642    /// A signature authorizing this Spend.
643    #[serde(rename = "spendAuthSig", with = "hex")]
644    #[getter(copy)]
645    spend_auth_sig: [u8; 64],
646}
647
648// We can't use `#[getter(copy)]` as upstream `sapling_crypto::note::ValueCommitment` is not `Copy`.
649impl ShieldedSpend {
650    /// The value commitment to the input note.
651    pub fn cv(&self) -> ValueCommitment {
652        self.cv.clone()
653    }
654}
655
656/// A Sapling output of a transaction.
657#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
658pub struct ShieldedOutput {
659    /// Value commitment to the input note.
660    #[serde(with = "hex")]
661    #[getter(skip)]
662    cv: ValueCommitment,
663    /// The u-coordinate of the note commitment for the output note.
664    #[serde(rename = "cmu", with = "hex")]
665    cm_u: [u8; 32],
666    /// A Jubjub public key.
667    #[serde(rename = "ephemeralKey", with = "hex")]
668    ephemeral_key: [u8; 32],
669    /// The output note encrypted to the recipient.
670    #[serde(rename = "encCiphertext", with = "arrayhex")]
671    enc_ciphertext: [u8; 580],
672    /// A ciphertext enabling the sender to recover the output note.
673    #[serde(rename = "outCiphertext", with = "hex")]
674    out_ciphertext: [u8; 80],
675    /// A zero-knowledge proof using the Sapling Output circuit.
676    #[serde(with = "hex")]
677    proof: [u8; 192],
678}
679
680// We can't use `#[getter(copy)]` as upstream `sapling_crypto::note::ValueCommitment` is not `Copy`.
681impl ShieldedOutput {
682    /// The value commitment to the output note.
683    pub fn cv(&self) -> ValueCommitment {
684        self.cv.clone()
685    }
686}
687
688/// Object with Orchard-specific information.
689#[serde_with::serde_as]
690#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
691pub struct Orchard {
692    /// Array of Orchard actions.
693    actions: Vec<OrchardAction>,
694    /// The net value of Orchard Actions in ZEC.
695    #[serde(rename = "valueBalance")]
696    value_balance: f64,
697    /// The net value of Orchard Actions in zatoshis.
698    #[serde(rename = "valueBalanceZat")]
699    value_balance_zat: i64,
700    /// The flags.
701    #[serde(skip_serializing_if = "Option::is_none")]
702    flags: Option<OrchardFlags>,
703    /// A root of the Orchard note commitment tree at some block height in the past
704    #[serde_as(as = "Option<serde_with::hex::Hex>")]
705    #[serde(skip_serializing_if = "Option::is_none")]
706    #[getter(copy)]
707    anchor: Option<[u8; 32]>,
708    /// Encoding of aggregated zk-SNARK proofs for Orchard Actions
709    #[serde_as(as = "Option<serde_with::hex::Hex>")]
710    #[serde(skip_serializing_if = "Option::is_none")]
711    proof: Option<Vec<u8>>,
712    /// An Orchard binding signature on the SIGHASH transaction hash
713    #[serde(rename = "bindingSig")]
714    #[serde(skip_serializing_if = "Option::is_none")]
715    #[serde_as(as = "Option<serde_with::hex::Hex>")]
716    #[getter(copy)]
717    binding_sig: Option<[u8; 64]>,
718}
719
720/// Object with Orchard-specific information.
721#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
722pub struct OrchardFlags {
723    /// Whether Orchard outputs are enabled.
724    #[serde(rename = "enableOutputs")]
725    enable_outputs: bool,
726    /// Whether Orchard spends are enabled.
727    #[serde(rename = "enableSpends")]
728    enable_spends: bool,
729}
730
731/// The Orchard action of a transaction.
732#[allow(clippy::too_many_arguments)]
733#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
734pub struct OrchardAction {
735    /// A value commitment to the net value of the input note minus the output note.
736    #[serde(with = "hex")]
737    cv: [u8; 32],
738    /// The nullifier of the input note.
739    #[serde(with = "hex")]
740    nullifier: [u8; 32],
741    /// The randomized validating key for spendAuthSig.
742    #[serde(with = "hex")]
743    rk: [u8; 32],
744    /// The x-coordinate of the note commitment for the output note.
745    #[serde(rename = "cmx", with = "hex")]
746    cm_x: [u8; 32],
747    /// An encoding of an ephemeral Pallas public key.
748    #[serde(rename = "ephemeralKey", with = "hex")]
749    ephemeral_key: [u8; 32],
750    /// The output note encrypted to the recipient.
751    #[serde(rename = "encCiphertext", with = "arrayhex")]
752    enc_ciphertext: [u8; 580],
753    /// A ciphertext enabling the sender to recover the output note.
754    #[serde(rename = "spendAuthSig", with = "hex")]
755    spend_auth_sig: [u8; 64],
756    /// A signature authorizing the spend in this Action.
757    #[serde(rename = "outCiphertext", with = "hex")]
758    out_ciphertext: [u8; 80],
759}
760
761/// Builds the RPC object for an Orchard-shaped shielded pool (Orchard or Ironwood) from its
762/// shielded data and net value balance.
763///
764/// The Ironwood pool reuses the Orchard bundle shape, so both pools serialize through the same
765/// [`Orchard`] object; the caller selects which pool's shielded data and value balance to pass.
766fn orchard_shaped_object(
767    shielded_data: Option<&orchard::ShieldedData>,
768    value_balance: Amount<NegativeAllowed>,
769) -> Orchard {
770    let actions = shielded_data
771        .into_iter()
772        .flat_map(|data| data.actions.iter())
773        .map(|authorized_action| {
774            let action = &authorized_action.action;
775            OrchardAction {
776                cv: action.cv.into(),
777                nullifier: action.nullifier.into(),
778                rk: action.rk.into(),
779                cm_x: action.cm_x.into(),
780                ephemeral_key: action.ephemeral_key.into(),
781                enc_ciphertext: action.enc_ciphertext.into(),
782                spend_auth_sig: authorized_action.spend_auth_sig.into(),
783                out_ciphertext: action.out_ciphertext.into(),
784            }
785        })
786        .collect();
787
788    Orchard {
789        actions,
790        value_balance: Zec::from(value_balance).lossy_zec(),
791        value_balance_zat: value_balance.zatoshis(),
792        flags: shielded_data.map(|data| {
793            OrchardFlags::new(
794                data.flags.contains(orchard::Flags::ENABLE_OUTPUTS),
795                data.flags.contains(orchard::Flags::ENABLE_SPENDS),
796            )
797        }),
798        anchor: shielded_data.map(|data| data.shared_anchor.bytes_in_display_order()),
799        proof: shielded_data.map(|data| data.proof.bytes_in_display_order()),
800        binding_sig: shielded_data.map(|data| data.binding_sig.into()),
801    }
802}
803
804impl Default for TransactionObject {
805    fn default() -> Self {
806        Self {
807            hex: SerializedTransaction::from(
808                [0u8; zebra_chain::transaction::MIN_TRANSPARENT_TX_SIZE as usize].to_vec(),
809            ),
810            height: Option::default(),
811            confirmations: Option::default(),
812            inputs: Vec::new(),
813            outputs: Vec::new(),
814            shielded_spends: Vec::new(),
815            shielded_outputs: Vec::new(),
816            joinsplits: Vec::new(),
817            orchard: None,
818            ironwood: None,
819            binding_sig: None,
820            joinsplit_pub_key: None,
821            joinsplit_sig: None,
822            value_balance: None,
823            value_balance_zat: None,
824            size: None,
825            time: None,
826            txid: transaction::Hash::from([0u8; 32]),
827            in_active_chain: None,
828            auth_digest: None,
829            overwintered: false,
830            version: 4,
831            version_group_id: None,
832            lock_time: 0,
833            expiry_height: None,
834            block_hash: None,
835            block_time: None,
836        }
837    }
838}
839
840impl TransactionObject {
841    /// Converts `tx` and `height` into a new `GetRawTransaction` in the `verbose` format.
842    #[allow(clippy::unwrap_in_result)]
843    #[allow(clippy::too_many_arguments)]
844    pub fn from_transaction(
845        tx: Arc<Transaction>,
846        height: Option<block::Height>,
847        confirmations: Option<i64>,
848        network: &Network,
849        block_time: Option<DateTime<Utc>>,
850        block_hash: Option<block::Hash>,
851        in_active_chain: Option<bool>,
852        txid: transaction::Hash,
853    ) -> Self {
854        let block_time = block_time.map(|bt| bt.timestamp());
855        Self {
856            hex: tx.clone().into(),
857            height: if in_active_chain.unwrap_or_default() {
858                height.map(|height| height.0 as i32)
859            } else if block_hash.is_some() {
860                // Side chain
861                Some(-1)
862            } else {
863                // Mempool
864                None
865            },
866            confirmations: if in_active_chain.unwrap_or_default() {
867                confirmations
868            } else if block_hash.is_some() {
869                // Side chain
870                Some(0)
871            } else {
872                // Mempool
873                None
874            },
875            inputs: tx
876                .inputs()
877                .iter()
878                .map(|input| match input {
879                    zebra_chain::transparent::Input::Coinbase { sequence, .. } => Input::Coinbase {
880                        coinbase: input
881                            .coinbase_script()
882                            .expect("we know it is a valid coinbase script"),
883                        sequence: *sequence,
884                    },
885                    zebra_chain::transparent::Input::PrevOut {
886                        sequence,
887                        unlock_script,
888                        outpoint,
889                    } => Input::NonCoinbase {
890                        txid: outpoint.hash.encode_hex(),
891                        vout: outpoint.index,
892                        script_sig: ScriptSig {
893                            // https://github.com/zcash/zcash/blob/v6.11.0/src/rpc/rawtransaction.cpp#L240
894                            asm: zcash_script::script::Code(unlock_script.as_raw_bytes().to_vec())
895                                .to_asm(true),
896                            hex: unlock_script.clone(),
897                        },
898                        sequence: *sequence,
899                        value: None,
900                        value_zat: None,
901                        address: None,
902                    },
903                })
904                .collect(),
905            outputs: tx
906                .outputs()
907                .iter()
908                .enumerate()
909                .map(|output| {
910                    // Parse the scriptPubKey to find destination addresses.
911                    let (addresses, req_sigs) = output
912                        .1
913                        .address(network)
914                        .map(|address| (vec![address.to_string()], 1))
915                        .unzip();
916
917                    Output {
918                        value: Zec::from(output.1.value).lossy_zec(),
919                        value_zat: output.1.value.zatoshis(),
920                        n: output.0 as u32,
921                        script_pub_key: ScriptPubKey {
922                            // https://github.com/zcash/zcash/blob/v6.11.0/src/rpc/rawtransaction.cpp#L271
923                            // https://github.com/zcash/zcash/blob/v6.11.0/src/rpc/rawtransaction.cpp#L45
924                            asm: zcash_script::script::Code(
925                                output.1.lock_script.as_raw_bytes().to_vec(),
926                            )
927                            .to_asm(false),
928                            hex: output.1.lock_script.clone(),
929                            req_sigs,
930                            r#type: zcash_script::script::Code(
931                                output.1.lock_script.as_raw_bytes().to_vec(),
932                            )
933                            .to_component()
934                            .ok()
935                            .and_then(|c| c.refine().ok())
936                            .and_then(|component| zcash_script::solver::standard(&component))
937                            .map(|kind| match kind {
938                                zcash_script::solver::ScriptKind::PubKeyHash { .. } => "pubkeyhash",
939                                zcash_script::solver::ScriptKind::ScriptHash { .. } => "scripthash",
940                                zcash_script::solver::ScriptKind::MultiSig { .. } => "multisig",
941                                zcash_script::solver::ScriptKind::NullData { .. } => "nulldata",
942                                zcash_script::solver::ScriptKind::PubKey { .. } => "pubkey",
943                            })
944                            .unwrap_or("nonstandard")
945                            .to_string(),
946                            addresses,
947                        },
948                    }
949                })
950                .collect(),
951            shielded_spends: tx
952                .sapling_spends_per_anchor()
953                .map(|spend| {
954                    let mut anchor = spend.per_spend_anchor.as_bytes();
955                    anchor.reverse();
956
957                    let mut nullifier = spend.nullifier.as_bytes();
958                    nullifier.reverse();
959
960                    let mut rk: [u8; 32] = spend.clone().rk.into();
961                    rk.reverse();
962
963                    let spend_auth_sig: [u8; 64] = spend.spend_auth_sig.into();
964
965                    ShieldedSpend {
966                        cv: spend.cv.clone(),
967                        anchor,
968                        nullifier,
969                        rk,
970                        proof: spend.zkproof.0,
971                        spend_auth_sig,
972                    }
973                })
974                .collect(),
975            shielded_outputs: tx
976                .sapling_outputs()
977                .map(|output| {
978                    let mut cm_u: [u8; 32] = output.cm_u.to_bytes();
979                    cm_u.reverse();
980                    let mut ephemeral_key: [u8; 32] = output.ephemeral_key.into();
981                    ephemeral_key.reverse();
982                    let enc_ciphertext: [u8; 580] = output.enc_ciphertext.into();
983                    let out_ciphertext: [u8; 80] = output.out_ciphertext.into();
984
985                    ShieldedOutput {
986                        cv: output.cv.clone(),
987                        cm_u,
988                        ephemeral_key,
989                        enc_ciphertext,
990                        out_ciphertext,
991                        proof: output.zkproof.0,
992                    }
993                })
994                .collect(),
995            joinsplits: tx
996                .sprout_joinsplits()
997                .map(|joinsplit| {
998                    let mut ephemeral_key_bytes: [u8; 32] = joinsplit.ephemeral_key.to_bytes();
999                    ephemeral_key_bytes.reverse();
1000
1001                    JoinSplit {
1002                        old_public_value: Zec::from(joinsplit.vpub_old).lossy_zec(),
1003                        old_public_value_zat: joinsplit.vpub_old.zatoshis(),
1004                        new_public_value: Zec::from(joinsplit.vpub_new).lossy_zec(),
1005                        new_public_value_zat: joinsplit.vpub_new.zatoshis(),
1006                        anchor: joinsplit.anchor.bytes_in_display_order(),
1007                        nullifiers: joinsplit
1008                            .nullifiers
1009                            .iter()
1010                            .map(|n| n.bytes_in_display_order())
1011                            .collect(),
1012                        commitments: joinsplit
1013                            .commitments
1014                            .iter()
1015                            .map(|c| c.bytes_in_display_order())
1016                            .collect(),
1017                        one_time_pubkey: ephemeral_key_bytes,
1018                        random_seed: joinsplit.random_seed.bytes_in_display_order(),
1019                        macs: joinsplit
1020                            .vmacs
1021                            .iter()
1022                            .map(|m| m.bytes_in_display_order())
1023                            .collect(),
1024                        proof: joinsplit.zkproof.unwrap_or_default(),
1025                        ciphertexts: joinsplit
1026                            .enc_ciphertexts
1027                            .iter()
1028                            .map(|c| c.zcash_serialize_to_vec().unwrap_or_default())
1029                            .collect(),
1030                    }
1031                })
1032                .collect(),
1033            value_balance: Some(Zec::from(tx.sapling_value_balance().sapling_amount()).lossy_zec()),
1034            value_balance_zat: Some(tx.sapling_value_balance().sapling_amount().zatoshis()),
1035            orchard: Some(orchard_shaped_object(
1036                tx.orchard_shielded_data(),
1037                tx.orchard_value_balance().orchard_amount(),
1038            )),
1039            ironwood: tx.ironwood_shielded_data().map(|data| {
1040                orchard_shaped_object(Some(data), tx.ironwood_value_balance().ironwood_amount())
1041            }),
1042            binding_sig: tx.sapling_binding_sig().map(|raw_sig| raw_sig.into()),
1043            joinsplit_pub_key: tx.joinsplit_pub_key().map(|raw_key| {
1044                // Display order is reversed in the RPC output.
1045                let mut key: [u8; 32] = raw_key.into();
1046                key.reverse();
1047                key
1048            }),
1049            joinsplit_sig: tx.joinsplit_sig().map(|raw_sig| raw_sig.into()),
1050            size: tx.as_bytes().len().try_into().ok(),
1051            time: block_time,
1052            txid,
1053            in_active_chain,
1054            auth_digest: tx.auth_digest(),
1055            overwintered: tx.is_overwintered(),
1056            version: tx.version(),
1057            version_group_id: tx.version_group_id().map(|id| id.to_be_bytes().to_vec()),
1058            lock_time: tx.raw_lock_time(),
1059            // zcashd includes expiryheight only for Overwinter+ transactions.
1060            // For those, expiry_height of 0 means "no expiry" per ZIP-203.
1061            expiry_height: if tx.is_overwintered() {
1062                Some(tx.expiry_height().unwrap_or(Height(0)))
1063            } else {
1064                None
1065            },
1066            block_hash,
1067            block_time,
1068        }
1069    }
1070}