Skip to main content

dusk_core/
transfer.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4//
5// Copyright (c) DUSK NETWORK. All rights reserved.
6
7//! Types related to Dusk's transfer contract that are shared across the
8//! network.
9
10use alloc::string::String;
11use alloc::vec::Vec;
12use core::cmp::max;
13use core::fmt::Debug;
14
15use bytecheck::CheckBytes;
16use dusk_bytes::{DeserializableSlice, Error as BytesError};
17use poseidon_merkle::Opening;
18use rand::{CryptoRng, RngCore};
19use rkyv::{Archive, Deserialize, Serialize};
20#[cfg(feature = "serde")]
21use serde_with::hex::Hex;
22#[cfg(feature = "serde")]
23use serde_with::{As, DisplayFromStr, Same};
24
25use self::data::{
26    BlobData, BlobSidecar, ContractCall, ContractDeploy, TransactionData,
27};
28use self::moonlight::Transaction as MoonlightTransaction;
29use self::phoenix::{
30    NOTES_TREE_DEPTH, Note, Prove, PublicKey as PhoenixPublicKey,
31    SecretKey as PhoenixSecretKey, Sender, StealthAddress,
32    Transaction as PhoenixTransaction,
33};
34use self::withdraw::{Withdraw, WithdrawReceiver};
35use crate::abi::ContractId;
36use crate::error::TxPreconditionError;
37use crate::signatures::bls::{
38    PublicKey as AccountPublicKey, SecretKey as AccountSecretKey,
39};
40use crate::{BlsScalar, Error};
41
42pub mod data;
43pub mod moonlight;
44pub mod phoenix;
45pub mod withdraw;
46
47/// ID of the genesis transfer contract
48pub const TRANSFER_CONTRACT: ContractId = crate::reserved(0x1);
49
50/// Panic of "Nonce not ready to be used yet"
51pub const PANIC_NONCE_NOT_READY: &str = "Nonce not ready to be used yet";
52
53/// Topic for the moonlight transaction event.
54pub const MOONLIGHT_TOPIC: &str = "moonlight";
55/// Topic for the phoenix transaction event.
56pub const PHOENIX_TOPIC: &str = "phoenix";
57/// Topic for the contract to contract transaction event.
58pub const CONTRACT_TO_CONTRACT_TOPIC: &str = "contract_to_contract";
59/// Topic for the contract to account transaction event.
60pub const CONTRACT_TO_ACCOUNT_TOPIC: &str = "contract_to_account";
61/// Topic for the withdraw event.
62pub const WITHDRAW_TOPIC: &str = "withdraw";
63/// Topic for the deposit event.
64pub const DEPOSIT_TOPIC: &str = "deposit";
65/// Topic for the convert event.
66pub const CONVERT_TOPIC: &str = "convert";
67/// Topic for the mint event.
68pub const MINT_TOPIC: &str = "mint";
69/// Topic for the mint to contract event.
70pub const MINT_CONTRACT_TOPIC: &str = "mint_c";
71
72const BOREAS_FORMAT_VERSION: u8 = 2;
73
74#[derive(Debug, Clone, Copy, Eq, PartialEq)]
75#[repr(u8)]
76enum TransactionTag {
77    LegacyPhoenix = 0,
78    LegacyMoonlight = 1,
79    VersionedPhoenix = 2,
80    VersionedMoonlight = 3,
81}
82
83impl TryFrom<u8> for TransactionTag {
84    type Error = BytesError;
85
86    fn try_from(value: u8) -> Result<Self, Self::Error> {
87        match value {
88            0 => Ok(Self::LegacyPhoenix),
89            1 => Ok(Self::LegacyMoonlight),
90            2 => Ok(Self::VersionedPhoenix),
91            3 => Ok(Self::VersionedMoonlight),
92            _ => Err(BytesError::InvalidData),
93        }
94    }
95}
96
97impl TransactionTag {
98    fn from_transaction(
99        transaction: &Transaction,
100        format: TransactionFormat,
101    ) -> Self {
102        match (transaction, format) {
103            (Transaction::Phoenix(_), TransactionFormat::Boreas) => {
104                Self::VersionedPhoenix
105            }
106            (Transaction::Moonlight(_), TransactionFormat::Boreas) => {
107                Self::VersionedMoonlight
108            }
109            (Transaction::Phoenix(_), _) => Self::LegacyPhoenix,
110            (Transaction::Moonlight(_), _) => Self::LegacyMoonlight,
111        }
112    }
113
114    fn is_versioned(self) -> bool {
115        matches!(self, Self::VersionedPhoenix | Self::VersionedMoonlight)
116    }
117}
118
119/// Canonical protocol transaction format.
120#[derive(Debug, Clone, Copy, Eq, PartialEq)]
121pub enum TransactionFormat {
122    /// Historical transactions before AEGIS.
123    PreAegis,
124    /// Historical and produced transactions under AEGIS rules.
125    Aegis,
126    /// Explicitly versioned transactions introduced in the Boreas era.
127    Boreas,
128}
129
130/// Result of a version-aware transaction decode.
131#[derive(Debug, Clone, Eq, PartialEq)]
132pub struct DecodedTransaction {
133    /// Decoded semantic transaction.
134    pub transaction: Transaction,
135    /// Canonical transaction format used for the decoded bytes.
136    pub format: TransactionFormat,
137}
138
139/// The transaction used by the transfer contract.
140#[derive(Debug, Clone, Archive, PartialEq, Eq, Serialize, Deserialize)]
141#[archive_attr(derive(CheckBytes))]
142#[allow(clippy::large_enum_variant)]
143pub enum Transaction {
144    /// A phoenix transaction.
145    Phoenix(PhoenixTransaction),
146    /// A moonlight transaction.
147    Moonlight(MoonlightTransaction),
148}
149
150impl Transaction {
151    fn decode_tagged(
152        tag: TransactionTag,
153        buf: &[u8],
154        phoenix_tag: TransactionTag,
155        moonlight_tag: TransactionTag,
156        parse_phoenix: fn(&[u8]) -> Result<PhoenixTransaction, BytesError>,
157    ) -> Result<Transaction, BytesError> {
158        match tag {
159            tag if tag == phoenix_tag => Ok(Self::Phoenix(parse_phoenix(buf)?)),
160            tag if tag == moonlight_tag => {
161                Ok(Self::Moonlight(MoonlightTransaction::from_slice(buf)?))
162            }
163            _ => Err(BytesError::InvalidData),
164        }
165    }
166
167    fn decode_legacy(
168        format: TransactionFormat,
169        buf: &[u8],
170        parse_phoenix: fn(&[u8]) -> Result<PhoenixTransaction, BytesError>,
171    ) -> Result<DecodedTransaction, BytesError> {
172        let mut buf = buf;
173        let family_tag = TransactionTag::try_from(u8::from_reader(&mut buf)?)?;
174
175        let transaction = Self::decode_tagged(
176            family_tag,
177            buf,
178            TransactionTag::LegacyPhoenix,
179            TransactionTag::LegacyMoonlight,
180            parse_phoenix,
181        )?;
182
183        Ok(DecodedTransaction {
184            transaction,
185            format,
186        })
187    }
188
189    fn decode_boreas(buf: &[u8]) -> Result<DecodedTransaction, BytesError> {
190        let mut buf = buf;
191        let family_tag = TransactionTag::try_from(u8::from_reader(&mut buf)?)?;
192        let format_version = u8::from_reader(&mut buf)?;
193
194        if format_version != BOREAS_FORMAT_VERSION {
195            return Err(BytesError::InvalidData);
196        }
197
198        let transaction = Self::decode_tagged(
199            family_tag,
200            buf,
201            TransactionTag::VersionedPhoenix,
202            TransactionTag::VersionedMoonlight,
203            PhoenixTransaction::from_slice,
204        )?;
205
206        Ok(DecodedTransaction {
207            transaction,
208            format: TransactionFormat::Boreas,
209        })
210    }
211
212    /// Decode a transaction according to the selected protocol format.
213    ///
214    /// # Errors
215    /// Returns [`BytesError::InvalidData`] when the bytes do not match the
216    /// transaction families or formats accepted under `format`.
217    pub fn decode_with_format(
218        format: TransactionFormat,
219        buf: &[u8],
220    ) -> Result<DecodedTransaction, BytesError> {
221        match format {
222            TransactionFormat::PreAegis => Self::decode_legacy(
223                format,
224                buf,
225                PhoenixTransaction::from_slice_ledger_compat,
226            ),
227            TransactionFormat::Aegis => {
228                Self::decode_legacy(format, buf, PhoenixTransaction::from_slice)
229            }
230            TransactionFormat::Boreas => Self::decode_boreas(buf),
231        }
232    }
233
234    /// Decode a transaction for live ingress under the selected protocol
235    /// format.
236    ///
237    /// This path is intended for mempool and network admission, where only the
238    /// format valid at the target block height should be accepted.
239    ///
240    /// # Errors
241    /// Returns [`BytesError::InvalidData`] when the bytes do not match the
242    /// transaction families or format accepted under `format`.
243    pub fn decode_for_ingress(
244        format: TransactionFormat,
245        buf: &[u8],
246    ) -> Result<DecodedTransaction, BytesError> {
247        Self::decode_with_format(format, buf)
248    }
249
250    /// Decode a transaction in any currently supported format.
251    ///
252    /// This path is intended for historical ledger replay and storage decoding,
253    /// where legacy transactions from earlier protocol eras must remain
254    /// decodable.
255    ///
256    /// # Errors
257    /// Returns [`BytesError::InvalidData`] when the bytes do not encode a
258    /// supported transaction family or explicit format version.
259    pub fn decode_any(buf: &[u8]) -> Result<DecodedTransaction, BytesError> {
260        let tag = TransactionTag::try_from(
261            buf.first().copied().ok_or(BytesError::InvalidData)?,
262        )?;
263
264        if tag.is_versioned() {
265            Self::decode_boreas(buf)
266        } else {
267            Self::decode_legacy(
268                TransactionFormat::Aegis,
269                buf,
270                PhoenixTransaction::from_slice,
271            )
272            .or_else(|_| {
273                Self::decode_legacy(
274                    TransactionFormat::PreAegis,
275                    buf,
276                    PhoenixTransaction::from_slice_ledger_compat,
277                )
278            })
279        }
280    }
281
282    /// Create a new phoenix transaction.
283    ///
284    /// # Errors
285    /// The creation of a transaction is not possible and will error if:
286    /// - one of the input-notes doesn't belong to the `sender_sk`
287    /// - the transaction input doesn't cover the transaction costs
288    /// - the `inputs` vector is either empty or larger than 4 elements
289    /// - the `inputs` vector contains duplicate `Note`s
290    /// - the `Prove` trait is implemented incorrectly
291    #[allow(clippy::too_many_arguments)]
292    pub fn phoenix<R: RngCore + CryptoRng, P: Prove>(
293        rng: &mut R,
294        sender_sk: &PhoenixSecretKey,
295        refund_pk: &PhoenixPublicKey,
296        receiver_pk: &PhoenixPublicKey,
297        inputs: Vec<(Note, Opening<(), NOTES_TREE_DEPTH>)>,
298        root: BlsScalar,
299        transfer_value: u64,
300        obfuscated_transaction: bool,
301        deposit: u64,
302        gas_limit: u64,
303        gas_price: u64,
304        chain_id: u8,
305        data: Option<impl Into<TransactionData>>,
306        prover: &P,
307    ) -> Result<Self, Error> {
308        Ok(Self::Phoenix(PhoenixTransaction::new::<R, P>(
309            rng,
310            sender_sk,
311            refund_pk,
312            receiver_pk,
313            inputs,
314            root,
315            transfer_value,
316            obfuscated_transaction,
317            deposit,
318            gas_limit,
319            gas_price,
320            chain_id,
321            data,
322            prover,
323        )?))
324    }
325
326    /// Create a new moonlight transaction.
327    ///
328    /// # Errors
329    /// The creation of a transaction is not possible and will error if:
330    /// - the memo, if given, is too large
331    #[allow(clippy::too_many_arguments)]
332    pub fn moonlight(
333        sender_sk: &AccountSecretKey,
334        receiver: Option<AccountPublicKey>,
335        value: u64,
336        deposit: u64,
337        gas_limit: u64,
338        gas_price: u64,
339        nonce: u64,
340        chain_id: u8,
341        data: Option<impl Into<TransactionData>>,
342    ) -> Result<Self, Error> {
343        Ok(Self::Moonlight(MoonlightTransaction::new(
344            sender_sk, receiver, value, deposit, gas_limit, gas_price, nonce,
345            chain_id, data,
346        )?))
347    }
348
349    /// Return the sender of the account for Moonlight transactions.
350    #[must_use]
351    pub fn moonlight_sender(&self) -> Option<&AccountPublicKey> {
352        match self {
353            Self::Phoenix(_) => None,
354            Self::Moonlight(tx) => Some(tx.sender()),
355        }
356    }
357
358    /// Get the receiver of the transaction for Moonlight transactions, if it
359    /// exists.
360    ///
361    /// # Returns
362    ///
363    /// - `Some(&AccountPublicKey)` if the transaction is a Moonlight
364    ///   transaction and the receiver is different from the sender.
365    /// - `None` if the transaction is a Moonlight transaction and the receiver
366    ///   is the same as the sender.
367    /// - `None` if the transaction is a Phoenix transaction.
368    #[must_use]
369    pub fn moonlight_receiver(&self) -> Option<&AccountPublicKey> {
370        match self {
371            Self::Phoenix(_) => None,
372            Self::Moonlight(tx) => tx.receiver(),
373        }
374    }
375
376    /// Return the value transferred in a Moonlight transaction.
377    #[must_use]
378    pub fn value(&self) -> Option<u64> {
379        match self {
380            Self::Phoenix(_) => None,
381            Self::Moonlight(tx) => Some(tx.value()),
382        }
383    }
384
385    /// Returns the nullifiers of the transaction, if the transaction is a
386    /// moonlight transaction, the result will be empty.
387    #[must_use]
388    pub fn nullifiers(&self) -> &[BlsScalar] {
389        match self {
390            Self::Phoenix(tx) => tx.nullifiers(),
391            Self::Moonlight(_) => &[],
392        }
393    }
394
395    /// Return the root of the UTXO tree for Phoenix transactions.
396    #[must_use]
397    pub fn root(&self) -> Option<&BlsScalar> {
398        match self {
399            Self::Phoenix(tx) => Some(tx.root()),
400            Self::Moonlight(_) => None,
401        }
402    }
403
404    /// Return the UTXO outputs of the transaction.
405    #[must_use]
406    pub fn outputs(&self) -> &[Note] {
407        match self {
408            Self::Phoenix(tx) => &tx.outputs()[..],
409            Self::Moonlight(_) => &[],
410        }
411    }
412
413    /// Returns the sender data for Phoenix transactions.
414    #[must_use]
415    pub fn phoenix_sender(&self) -> Option<&Sender> {
416        match self {
417            Self::Phoenix(tx) => Some(tx.sender()),
418            Self::Moonlight(_) => None,
419        }
420    }
421
422    /// Returns the deposit of the transaction.
423    #[must_use]
424    pub fn deposit(&self) -> u64 {
425        match self {
426            Self::Phoenix(tx) => tx.deposit(),
427            Self::Moonlight(tx) => tx.deposit(),
428        }
429    }
430
431    /// Returns the gas limit of the transaction.
432    #[must_use]
433    pub fn gas_limit(&self) -> u64 {
434        match self {
435            Self::Phoenix(tx) => tx.gas_limit(),
436            Self::Moonlight(tx) => tx.gas_limit(),
437        }
438    }
439
440    /// Returns the gas price of the transaction.
441    #[must_use]
442    pub fn gas_price(&self) -> u64 {
443        match self {
444            Self::Phoenix(tx) => tx.gas_price(),
445            Self::Moonlight(tx) => tx.gas_price(),
446        }
447    }
448
449    /// Returns the refund-address of the transaction.
450    #[must_use]
451    pub fn refund_address(&self) -> RefundAddress<'_> {
452        match self {
453            Self::Phoenix(tx) => RefundAddress::Phoenix(tx.stealth_address()),
454            Self::Moonlight(tx) => {
455                RefundAddress::Moonlight(tx.refund_address())
456            }
457        }
458    }
459
460    /// Return the contract call data, if there is any.
461    ///
462    /// Call data is present only when inter-contract calls happen.
463    ///
464    /// # Returns
465    ///
466    /// - `Some(&ContractCall)` if the transaction invokes another call to a
467    ///   contract.
468    /// - `None` if the transaction is an entrypoint call to a protocol contract
469    ///   without a second call attached to it.
470    #[must_use]
471    pub fn call(&self) -> Option<&ContractCall> {
472        match self {
473            Self::Phoenix(tx) => tx.call(),
474            Self::Moonlight(tx) => tx.call(),
475        }
476    }
477
478    /// Return the contract deploy data, if there is any.
479    #[must_use]
480    pub fn deploy(&self) -> Option<&ContractDeploy> {
481        match self {
482            Self::Phoenix(tx) => tx.deploy(),
483            Self::Moonlight(tx) => tx.deploy(),
484        }
485    }
486
487    /// Returns the memo used with the transaction, if any.
488    #[must_use]
489    pub fn memo(&self) -> Option<&[u8]> {
490        match self {
491            Self::Phoenix(tx) => tx.memo(),
492            Self::Moonlight(tx) => tx.memo(),
493        }
494    }
495
496    /// Returns the Blob used with the transaction, if any.
497    #[must_use]
498    pub fn blob(&self) -> Option<&Vec<BlobData>> {
499        match self {
500            Self::Phoenix(tx) => tx.blob(),
501            Self::Moonlight(tx) => tx.blob(),
502        }
503    }
504
505    /// Returns the Blob used with the transaction, if any.
506    #[must_use]
507    pub fn blob_mut(&mut self) -> Option<&mut Vec<BlobData>> {
508        match self {
509            Self::Phoenix(tx) => tx.blob_mut(),
510            Self::Moonlight(tx) => tx.blob_mut(),
511        }
512    }
513
514    /// Extracts and removes the blob sidecar from the transaction, if any, and
515    /// returns it as a vector of tuples containing the blob hash and the
516    /// corresponding blob sidecar.
517    ///
518    /// This function mutably accesses the blob storage within the transaction,
519    /// clears the stored data, and returns the extracted parts while
520    /// preserving their hashes.
521    ///
522    /// Returns `None` if there are no blobs present in the transaction.
523    #[must_use]
524    pub fn strip_blobs(&mut self) -> Option<Vec<([u8; 32], BlobSidecar)>> {
525        let blob = match self {
526            Self::Phoenix(tx) => tx.blob_mut(),
527            Self::Moonlight(tx) => tx.blob_mut(),
528        }?;
529
530        let ret = blob
531            .iter_mut()
532            .filter_map(|b| b.take_sidecar().map(|d| (b.hash, d)))
533            .collect::<Vec<_>>();
534
535        Some(ret)
536    }
537
538    /// Creates a modified clone of this transaction if it contains a Blob,
539    /// clones all fields except for the Blob, whose versioned hashes are set as
540    /// Memo.
541    ///
542    /// Returns none if the transaction is not a Blob transaction.
543    #[must_use]
544    pub fn blob_to_memo(&self) -> Option<Self> {
545        Some(match self {
546            Transaction::Phoenix(tx) => {
547                Transaction::Phoenix(tx.blob_to_memo()?)
548            }
549            Transaction::Moonlight(tx) => {
550                Transaction::Moonlight(tx.blob_to_memo()?)
551            }
552        })
553    }
554
555    /// Creates a modified clone of this transaction if it contains data for
556    /// deployment, clones all fields except for the bytecode' 'bytes' part.
557    /// Returns none if the transaction is not a deployment transaction.
558    #[must_use]
559    pub fn strip_off_bytecode(&self) -> Option<Self> {
560        Some(match self {
561            Transaction::Phoenix(tx) => {
562                Transaction::Phoenix(tx.strip_off_bytecode()?)
563            }
564            Transaction::Moonlight(tx) => {
565                Transaction::Moonlight(tx.strip_off_bytecode()?)
566            }
567        })
568    }
569
570    /// Serialize the transaction into a byte buffer.
571    #[must_use]
572    pub fn encode_for_format(&self, format: TransactionFormat) -> Vec<u8> {
573        let mut bytes = Vec::new();
574        let tag = TransactionTag::from_transaction(self, format);
575
576        bytes.push(tag as u8);
577        if tag.is_versioned() {
578            bytes.push(BOREAS_FORMAT_VERSION);
579        }
580
581        match self {
582            Self::Phoenix(tx) => bytes.extend(tx.to_var_bytes()),
583            Self::Moonlight(tx) => bytes.extend(tx.to_var_bytes()),
584        }
585
586        bytes
587    }
588
589    /// Serialize the transaction into the stable client-facing network format.
590    ///
591    /// Nodes accept these bytes across supported hardforks and normalize them
592    /// internally, so ordinary clients do not need fork-aware envelope
593    /// selection just to submit a transaction.
594    #[must_use]
595    pub fn to_network_bytes(&self) -> Vec<u8> {
596        self.encode_for_format(TransactionFormat::Aegis)
597    }
598
599    /// Backward-compatible alias for [`Transaction::to_network_bytes`].
600    #[must_use]
601    pub fn to_var_bytes(&self) -> Vec<u8> {
602        self.to_network_bytes()
603    }
604
605    /// Deserialize the transaction from a byte slice.
606    ///
607    /// # Errors
608    /// Errors when the bytes are not canonical.
609    pub fn from_slice(buf: &[u8]) -> Result<Self, BytesError> {
610        Self::decode_any(buf).map(|decoded| decoded.transaction)
611    }
612
613    /// Return input bytes to hash the transaction.
614    ///
615    /// Note: The result of this function is *only* meant to be used as an input
616    /// for hashing and *cannot* be used to deserialize the transaction again.
617    #[must_use]
618    pub fn to_hash_input_bytes(&self) -> Vec<u8> {
619        match self {
620            Self::Phoenix(tx) => tx.to_hash_input_bytes(),
621            Self::Moonlight(tx) => tx.to_hash_input_bytes(),
622        }
623    }
624
625    /// Create the unique transaction hash.
626    #[must_use]
627    pub fn hash(&self) -> BlsScalar {
628        match self {
629            Self::Phoenix(tx) => tx.hash(),
630            Self::Moonlight(tx) => tx.hash(),
631        }
632    }
633
634    /// Returns the charge for a contract deployment. The deployment of a
635    /// contract will cost at least `min_deploy_points`.
636    /// If the transaction is not a deploy-transaction, the deploy-charge will
637    /// be 0.
638    ///
639    /// # Errors
640    /// Returns an error if the deploy charge overflows while calculating
641    /// `bytecode_len * gas_per_deploy_byte`.
642    pub fn deploy_charge(
643        &self,
644        gas_per_deploy_byte: u64,
645        min_deploy_points: u64,
646    ) -> Result<u64, TxPreconditionError> {
647        if let Some(deploy) = self.deploy() {
648            let bytecode_len = deploy.bytecode.bytes.len() as u64;
649            let deploy_charge =
650                bytecode_len
651                    .checked_mul(gas_per_deploy_byte)
652                    .ok_or(TxPreconditionError::DeployChargeOverflow)?;
653
654            Ok(max(deploy_charge, min_deploy_points))
655        } else {
656            Ok(0)
657        }
658    }
659
660    /// Returns the minimum gas charged for a blob transaction deployment.
661    /// If the transaction is not a blob transaction, it returns None.
662    ///
663    /// # Errors
664    /// Returns an error if the blob charge overflows while calculating
665    /// `blob_count * gas_per_blob`.
666    pub fn blob_charge(
667        &self,
668        gas_per_blob: u64,
669    ) -> Result<Option<u64>, TxPreconditionError> {
670        self.blob()
671            .map(|blobs| {
672                (blobs.len() as u64)
673                    .checked_mul(gas_per_blob)
674                    .ok_or(TxPreconditionError::BlobChargeOverflow)
675            })
676            .transpose()
677    }
678
679    /// Check the validity of the phoenix fee and return an error if it is
680    /// invalid.
681    ///
682    /// # Errors
683    /// Returns an error if the transaction is a Phoenix transaction and:
684    /// - the fee overflows when calculating `gas_limit * gas_price`
685    /// - the fee does not match the proven `max_fee`
686    pub fn phoenix_fee_check(&self) -> Result<(), TxPreconditionError> {
687        if let Transaction::Phoenix(tx) = self {
688            let max_fee = tx
689                .fee()
690                .gas_limit
691                .checked_mul(tx.fee().gas_price)
692                .ok_or(TxPreconditionError::PhoenixFeeOverflow)?;
693
694            if max_fee != tx.max_fee() {
695                return Err(TxPreconditionError::PhoenixFeeTampered);
696            }
697        }
698        Ok(())
699    }
700
701    /// Check that the phoenix fee's refund address matches the ZK-proven
702    /// change note (`outputs[1]`).
703    ///
704    /// This prevents a malicious block producer from redirecting the gas
705    /// refund to a different stealth address.
706    ///
707    /// # Errors
708    /// Returns an error if the transaction is a Phoenix transaction and
709    /// the fee's stealth address does not match `outputs[1]`.
710    pub fn phoenix_refund_check(&self) -> Result<(), TxPreconditionError> {
711        if let Transaction::Phoenix(tx) = self
712            && tx.fee().stealth_address != *tx.outputs()[1].stealth_address()
713        {
714            return Err(TxPreconditionError::PhoenixFeeRefundMismatch);
715        }
716        Ok(())
717    }
718
719    /// Check if the transaction is a deployment transaction and if it
720    /// meets the minimum requirements for gas price and gas limit.
721    ///
722    /// # Errors
723    /// Returns an error if the transaction is a deployment transaction and
724    /// the gas price is lower than the minimum required gas price or if the
725    /// gas limit is lower than the required deployment charge.
726    pub fn deploy_check(
727        &self,
728        gas_per_deploy_byte: u64,
729        min_deploy_gas_price: u64,
730        min_deploy_points: u64,
731    ) -> Result<(), TxPreconditionError> {
732        if self.deploy().is_some() {
733            let deploy_charge =
734                self.deploy_charge(gas_per_deploy_byte, min_deploy_points)?;
735
736            if self.gas_price() < min_deploy_gas_price {
737                return Err(TxPreconditionError::DeployLowPrice(
738                    min_deploy_gas_price,
739                ));
740            }
741            if self.gas_limit() < deploy_charge {
742                return Err(TxPreconditionError::DeployLowLimit(deploy_charge));
743            }
744        }
745
746        Ok(())
747    }
748
749    /// Check if the transaction is a blob transaction and if it meets the
750    /// minimum requirements for gas limit.
751    ///
752    /// # Returns
753    /// - `Ok(Some(u64))` the minimum gas amount to charge if the transaction is
754    ///   a blob transaction.
755    /// - `Ok(None)` if the transaction is not a blob transaction.
756    /// - `Err(TxPreconditionError)` in case of an error.
757    ///
758    /// # Errors
759    /// Returns an error if the transaction is a blob transaction and:
760    /// - the gas limit is lower than the minimum charge.
761    /// - the number of blobs is zero or greater than 6.
762    pub fn blob_check(
763        &self,
764        gas_per_blob: u64,
765    ) -> Result<Option<u64>, TxPreconditionError> {
766        if let Some(blobs) = self.blob() {
767            match blobs.len() {
768                0 => Err(TxPreconditionError::BlobEmpty),
769                n if n > 6 => Err(TxPreconditionError::BlobTooMany(n)),
770                _ => Ok(()),
771            }?;
772        } else {
773            return Ok(None);
774        }
775
776        let min_charge = self.blob_charge(gas_per_blob)?;
777        if let Some(min_charge) = min_charge
778            && self.gas_limit() < min_charge
779        {
780            return Err(TxPreconditionError::BlobLowLimit(min_charge));
781        }
782        Ok(min_charge)
783    }
784}
785
786impl From<PhoenixTransaction> for Transaction {
787    fn from(tx: PhoenixTransaction) -> Self {
788        Self::Phoenix(tx)
789    }
790}
791
792impl From<MoonlightTransaction> for Transaction {
793    fn from(tx: MoonlightTransaction) -> Self {
794        Self::Moonlight(tx)
795    }
796}
797
798/// Enum defining the address to refund unspent gas to for both Phoenix and
799/// Moonlight transactions.
800pub enum RefundAddress<'a> {
801    /// The address of the Phoenix refund note.
802    Phoenix(&'a StealthAddress),
803    /// The moonlight account to which to send the refund.
804    Moonlight(&'a AccountPublicKey),
805}
806
807/// The payload sent by a contract to the transfer contract to transfer some of
808/// its funds to another contract.
809#[derive(Debug, Clone, Archive, PartialEq, Eq, Serialize, Deserialize)]
810#[archive_attr(derive(CheckBytes))]
811#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
812pub struct ContractToContract {
813    /// Contract to transfer funds to.
814    pub contract: ContractId,
815    /// Amount to send to the contract.
816    #[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
817    pub value: u64,
818    /// Function name to call on the contract.
819    pub fn_name: String,
820    /// Extra data sent along with [`ReceiveFromContract`]
821    #[cfg_attr(feature = "serde", serde(with = "As::<Hex>"))]
822    pub data: Vec<u8>,
823}
824
825/// The payload sent by the transfer contract to a contract receiving funds from
826/// another contract.
827#[derive(Debug, Clone, Archive, PartialEq, Eq, Serialize, Deserialize)]
828#[archive_attr(derive(CheckBytes))]
829#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
830pub struct ReceiveFromContract {
831    /// Contract that sent the funds.
832    pub contract: ContractId,
833    /// Amount sent by the contract.
834    #[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
835    pub value: u64,
836    /// Extra data sent by the sender.
837    #[cfg_attr(feature = "serde", serde(with = "As::<Hex>"))]
838    pub data: Vec<u8>,
839}
840
841/// The payload sent by a contract to the transfer contract to transfer some of
842/// its funds to an account.
843#[derive(Debug, Clone, Archive, PartialEq, Eq, Serialize, Deserialize)]
844#[archive_attr(derive(CheckBytes))]
845#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
846pub struct ContractToAccount {
847    /// Account to transfer funds to.
848    pub account: AccountPublicKey,
849    /// Amount to send to the account.
850    #[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
851    pub value: u64,
852}
853
854/// Event data emitted on a withdrawal from a contract.
855#[derive(Debug, Clone, Archive, PartialEq, Serialize, Deserialize)]
856#[archive_attr(derive(CheckBytes))]
857#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
858pub struct WithdrawEvent {
859    /// The contract withdrawn from.
860    pub sender: ContractId,
861    /// The receiver of the value.
862    pub receiver: WithdrawReceiver,
863    /// The value withdrawn.
864    #[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
865    pub value: u64,
866}
867
868impl From<Withdraw> for WithdrawEvent {
869    fn from(w: Withdraw) -> Self {
870        Self {
871            sender: w.contract,
872            receiver: w.receiver,
873            value: w.value,
874        }
875    }
876}
877
878/// Event data emitted on a conversion from Phoenix to Moonlight, and
879/// vice-versa.
880#[derive(Debug, Clone, Archive, PartialEq, Serialize, Deserialize)]
881#[archive_attr(derive(CheckBytes))]
882#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
883pub struct ConvertEvent {
884    /// The originator of the conversion, if it is possible to determine. From
885    /// Moonlight to Phoenix it is possible, but the same cannot be done the
886    /// other way round.
887    pub sender: Option<AccountPublicKey>,
888    /// The receiver of the value.
889    pub receiver: WithdrawReceiver,
890    /// The value converted.
891    #[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
892    pub value: u64,
893}
894
895impl ConvertEvent {
896    /// Convert a sender and a withdraw into a conversion event.
897    #[must_use]
898    pub fn from_withdraw_and_sender(
899        sender: Option<AccountPublicKey>,
900        withdraw: &Withdraw,
901    ) -> Self {
902        Self {
903            sender,
904            receiver: withdraw.receiver,
905            value: withdraw.value,
906        }
907    }
908}
909
910/// Event data emitted on a deposit to a contract.
911#[derive(Debug, Clone, Archive, PartialEq, Serialize, Deserialize)]
912#[archive_attr(derive(CheckBytes))]
913#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
914pub struct DepositEvent {
915    /// The originator of the deposit, if it is possible to determine. If the
916    /// depositor is using Moonlight this will be available. If they're using
917    /// Phoenix it will not.
918    pub sender: Option<AccountPublicKey>,
919    /// The receiver of the value.
920    pub receiver: ContractId,
921    /// The value deposited.
922    #[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
923    pub value: u64,
924}
925
926/// Event data emitted on a transfer from a contract to a contract.
927#[derive(Debug, Clone, Archive, PartialEq, Serialize, Deserialize)]
928#[archive_attr(derive(CheckBytes))]
929#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
930pub struct ContractToContractEvent {
931    /// The sender of the funds.
932    pub sender: ContractId,
933    /// The receiver of the funds.
934    pub receiver: ContractId,
935    /// The value transferred.
936    #[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
937    pub value: u64,
938}
939
940/// Event data emitted on a transfer from a contract to a Moonlight account.
941#[derive(Debug, Clone, Archive, PartialEq, Serialize, Deserialize)]
942#[archive_attr(derive(CheckBytes))]
943#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
944pub struct ContractToAccountEvent {
945    /// The sender of the funds.
946    pub sender: ContractId,
947    /// The receiver of the funds.
948    pub receiver: AccountPublicKey,
949    /// The value transferred.
950    #[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
951    pub value: u64,
952}
953
954/// Event data emitted on a phoenix transaction's completion.
955#[derive(Debug, Clone, Archive, PartialEq, Serialize, Deserialize)]
956#[archive_attr(derive(CheckBytes))]
957#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
958pub struct PhoenixTransactionEvent {
959    /// Nullifiers of the notes spent during the transaction.
960    pub nullifiers: Vec<BlsScalar>,
961    /// Notes produced during the transaction.
962    pub notes: Vec<Note>,
963    /// The memo included in the transaction.
964    #[cfg_attr(feature = "serde", serde(with = "As::<Hex>"))]
965    pub memo: Vec<u8>,
966    /// Gas spent by the transaction.
967    #[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
968    pub gas_spent: u64,
969
970    /// Optional gas-refund note if the refund is positive.
971    pub refund_note: Option<Note>,
972}
973
974/// Event data emitted on a moonlight transaction's completion.
975#[derive(Debug, Clone, Archive, PartialEq, Serialize, Deserialize)]
976#[archive_attr(derive(CheckBytes))]
977#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
978pub struct MoonlightTransactionEvent {
979    /// The account that initiated the transaction.
980    pub sender: AccountPublicKey,
981    /// The receiver of the funds if any were transferred.
982    pub receiver: Option<AccountPublicKey>,
983    /// Transfer amount
984    #[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
985    pub value: u64,
986    /// The memo included in the transaction.
987    #[cfg_attr(feature = "serde", serde(with = "As::<Hex>"))]
988    pub memo: Vec<u8>,
989    /// Gas spent by the transaction.
990    #[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
991    pub gas_spent: u64,
992    /// Optional refund-info in the case that the refund-address is different
993    /// from the sender.
994    #[cfg_attr(
995        feature = "serde",
996        serde(with = "As::<Option<(Same, DisplayFromStr)>>")
997    )]
998    pub refund_info: Option<(AccountPublicKey, u64)>,
999}