//! Contains structures and functions related to transaction creation.
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::string::ToString;
use alloc::vec::Vec;

use miden_protocol::account::AccountId;
use miden_protocol::asset::{Asset, FungibleAsset};
use miden_protocol::block::BlockNumber;
use miden_protocol::crypto::merkle::InnerNodeInfo;
use miden_protocol::crypto::merkle::store::MerkleStore;
use miden_protocol::crypto::rand::FeltRng;
use miden_protocol::errors::NoteError;
use miden_protocol::note::{
    Note,
    NoteAssets,
    NoteAttachment,
    NoteAttachments,
    NoteDetails,
    NoteDetailsCommitment,
    NoteId,
    NoteRecipient,
    NoteScript,
    NoteStorage,
    NoteTag,
    NoteType,
    PartialNote,
    PartialNoteMetadata,
};
use miden_protocol::transaction::TransactionScript;
use miden_protocol::vm::AdviceMap;
use miden_protocol::{Felt, Word};
use miden_standards::note::{
    P2idNote,
    P2ideNote,
    P2ideNoteStorage,
    PswapNote,
    PswapNoteStorage,
    SwapNote,
};

use super::{
    ForeignAccount,
    NoteArgs,
    TransactionRequest,
    TransactionRequestError,
    TransactionScriptTemplate,
};
use crate::ClientRng;

// TRANSACTION REQUEST BUILDER
// ================================================================================================

/// A builder for a [`TransactionRequest`].
///
/// Use this builder to construct a [`TransactionRequest`] by adding input notes, specifying
/// scripts, and setting other transaction parameters.
#[derive(Clone, Debug)]
pub struct TransactionRequestBuilder {
    /// Notes to be consumed by the transaction.
    /// Notes whose inclusion proof is present in the store are will be consumed as authenticated;
    /// the ones that do not have proofs will be consumed as unauthenticated.
    input_notes: Vec<Note>,
    /// Optional arguments of the Notes to be consumed by the transaction. This
    /// includes both authenticated and unauthenticated notes.
    input_notes_args: Vec<(NoteId, Option<NoteArgs>)>,
    /// Notes to be created by the transaction. The full note data is needed internally
    /// to build the transaction script template.
    own_output_notes: Vec<Note>,
    /// A map of recipients of the output notes expected to be generated by the transaction.
    expected_output_recipients: BTreeMap<Word, NoteRecipient>,
    /// A map of details and tags of notes we expect to be created as part of future transactions
    /// with their respective tags.
    ///
    /// For example, after a swap note is consumed, a payback note is expected to be created.
    expected_future_notes: BTreeMap<NoteDetailsCommitment, (NoteDetails, NoteTag)>,
    /// Custom transaction script to be used.
    custom_script: Option<TransactionScript>,
    /// Initial state of the `AdviceMap` that provides data during runtime.
    advice_map: AdviceMap,
    /// Initial state of the `MerkleStore` that provides data during runtime.
    merkle_store: MerkleStore,
    /// Foreign account data requirements. At execution time, account data will be retrieved from
    /// the network, and injected as advice inputs. Additionally, the account's code will be
    /// added to the executor and prover.
    foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
    /// The number of blocks in relation to the transaction's reference block after which the
    /// transaction will expire. If `None`, the transaction will not expire.
    expiration_delta: Option<u16>,
    /// Indicates whether to **silently** ignore invalid input notes when executing the
    /// transaction. This will allow the transaction to be executed even if some input notes
    /// are invalid.
    ignore_invalid_input_notes: bool,
    /// Optional [`Word`] that will be pushed to the operand stack before the transaction script
    /// execution. If the advice map is extended with some user defined entries, this script
    /// argument could be used as a key to access the corresponding value.
    script_arg: Option<Word>,
    /// Optional [`Word`] that will be pushed to the stack for the authentication procedure
    /// during transaction execution.
    auth_arg: Option<Word>,
    /// Note scripts that the node's NTX builder will need in its script registry.
    ///
    /// See [`TransactionRequestBuilder::expected_ntx_scripts`] for details.
    expected_ntx_scripts: Vec<NoteScript>,
}

impl TransactionRequestBuilder {
    // CONSTRUCTORS
    // --------------------------------------------------------------------------------------------

    /// Creates a new, empty [`TransactionRequestBuilder`].
    pub fn new() -> Self {
        Self {
            input_notes: vec![],
            input_notes_args: vec![],
            own_output_notes: Vec::new(),
            expected_output_recipients: BTreeMap::new(),
            expected_future_notes: BTreeMap::new(),
            custom_script: None,
            advice_map: AdviceMap::default(),
            merkle_store: MerkleStore::default(),
            expiration_delta: None,
            foreign_accounts: BTreeMap::default(),
            ignore_invalid_input_notes: false,
            script_arg: None,
            auth_arg: None,
            expected_ntx_scripts: vec![],
        }
    }

    /// Adds the specified notes as input notes to the transaction request.
    #[must_use]
    pub fn input_notes(
        mut self,
        notes: impl IntoIterator<Item = (Note, Option<NoteArgs>)>,
    ) -> Self {
        for (note, argument) in notes {
            self.input_notes_args.push((note.id(), argument));
            self.input_notes.push(note);
        }
        self
    }

    /// Specifies the output notes that should be created in the transaction script and will
    /// be used as a transaction script template. These notes will also be added to the expected
    /// output recipients of the transaction.
    ///
    /// If a transaction script template is already set (e.g. by calling `with_custom_script`), the
    /// [`TransactionRequestBuilder::build`] method will return an error.
    #[must_use]
    pub fn own_output_notes(mut self, notes: impl IntoIterator<Item = Note>) -> Self {
        for note in notes {
            self.expected_output_recipients
                .insert(note.recipient().digest(), note.recipient().clone());
            self.own_output_notes.push(note);
        }

        self
    }

    /// Specifies a custom transaction script to be used.
    ///
    /// If a script template is already set (e.g. by calling `with_own_output_notes`), the
    /// [`TransactionRequestBuilder::build`] method will return an error.
    #[must_use]
    pub fn custom_script(mut self, script: TransactionScript) -> Self {
        self.custom_script = Some(script);
        self
    }

    /// Specifies one or more foreign accounts (public or private) that contain data
    /// utilized by the transaction.
    ///
    /// At execution, the client queries the node and retrieves the appropriate data,
    /// depending on whether each foreign account is public or private:
    ///
    /// - **Public accounts**: the node retrieves the state and code for the account and injects
    ///   them as advice inputs. Public accounts can be omitted here, as they will be lazily loaded
    ///   through RPC calls. Undeclared accounts may trigger additional RPC calls for storage map
    ///   accesses during execution.
    /// - **Private accounts**: the node retrieves a proof of the account's existence and injects
    ///   that as advice inputs. Private accounts must always be declared here with their
    ///   [`PartialAccount`](miden_protocol::account::PartialAccount) state.
    #[must_use]
    pub fn foreign_accounts(
        mut self,
        foreign_accounts: impl IntoIterator<Item = impl Into<ForeignAccount>>,
    ) -> Self {
        for account in foreign_accounts {
            let foreign_account: ForeignAccount = account.into();
            self.foreign_accounts.insert(foreign_account.account_id(), foreign_account);
        }

        self
    }

    /// Specifies a transaction's expected output note recipients.
    ///
    /// The set of specified recipients is treated as a subset of the recipients for notes that may
    /// be created by a transaction. That is, the transaction must create notes for all the
    /// specified expected recipients, but it may also create notes for other recipients not
    /// included in this set.
    #[must_use]
    pub fn expected_output_recipients(mut self, recipients: Vec<NoteRecipient>) -> Self {
        self.expected_output_recipients = recipients
            .into_iter()
            .map(|recipient| (recipient.digest(), recipient))
            .collect::<BTreeMap<_, _>>();
        self
    }

    /// Specifies a set of notes which may be created when a transaction's output notes are
    /// consumed.
    ///
    /// For example, after a SWAP note is consumed, a payback note is expected to be created. This
    /// allows the client to track this note accordingly.
    #[must_use]
    pub fn expected_future_notes(mut self, notes: Vec<(NoteDetails, NoteTag)>) -> Self {
        self.expected_future_notes = notes
            .into_iter()
            .map(|note| (note.0.commitment(), note))
            .collect::<BTreeMap<_, _>>();
        self
    }

    /// Extends the advice map with the specified `([Word], Vec<[Felt]>)` pairs.
    #[must_use]
    pub fn extend_advice_map<I, V>(mut self, iter: I) -> Self
    where
        I: IntoIterator<Item = (Word, V)>,
        V: AsRef<[Felt]>,
    {
        self.advice_map.extend(iter.into_iter().map(|(w, v)| (w, v.as_ref().to_vec())));
        self
    }

    /// Extends the merkle store with the specified [`InnerNodeInfo`] elements.
    #[must_use]
    pub fn extend_merkle_store<T: IntoIterator<Item = InnerNodeInfo>>(mut self, iter: T) -> Self {
        self.merkle_store.extend(iter);
        self
    }

    /// The number of blocks in relation to the transaction's reference block after which the
    /// transaction will expire. By default, the transaction will not expire.
    ///
    /// Setting transaction expiration delta defines an upper bound for transaction expiration,
    /// but other code executed during the transaction may impose an even smaller transaction
    /// expiration delta.
    #[must_use]
    pub fn expiration_delta(mut self, expiration_delta: u16) -> Self {
        self.expiration_delta = Some(expiration_delta);
        self
    }

    /// The resulting transaction will **silently** ignore invalid input notes when being executed.
    /// By default, this will not happen.
    #[must_use]
    pub fn ignore_invalid_input_notes(mut self) -> Self {
        self.ignore_invalid_input_notes = true;
        self
    }

    /// Sets an optional [`Word`] that will be pushed to the operand stack before the transaction
    /// script execution. If the advice map is extended with some user defined entries, this script
    /// argument could be used as a key to access the corresponding value.
    #[must_use]
    pub fn script_arg(mut self, script_arg: Word) -> Self {
        self.script_arg = Some(script_arg);
        self
    }

    /// Sets an optional [`Word`] that will be pushed to the stack for the authentication
    /// procedure during transaction execution.
    #[must_use]
    pub fn auth_arg(mut self, auth_arg: Word) -> Self {
        self.auth_arg = Some(auth_arg);
        self
    }

    /// Specifies note scripts that the node's network transaction (NTX) builder will need in
    /// its script registry.
    ///
    /// When a transaction creates notes destined for a network account, the node's NTX builder
    /// must have the scripts of any public output notes in its registry. If a required script
    /// is missing, the NTX will silently fail on the node side.
    ///
    /// When this field is set, the client will check each script against the node before
    /// executing the main transaction. For any script not yet registered, the client
    /// automatically creates and submits a separate registration transaction (a public note
    /// carrying that script) so the node's registry is populated before the NTX executes.
    ///
    /// Standard note scripts are ignored here — the NTX builder resolves them directly.
    #[must_use]
    pub fn expected_ntx_scripts(mut self, scripts: Vec<NoteScript>) -> Self {
        self.expected_ntx_scripts = scripts;
        self
    }

    // STANDARDIZED REQUESTS
    // --------------------------------------------------------------------------------------------

    /// Consumes the builder and returns a [`TransactionRequest`] for a transaction to consume the
    /// specified notes.
    ///
    /// - `notes` is a list of notes to be consumed.
    pub fn build_consume_notes(
        self,
        notes: Vec<Note>,
    ) -> Result<TransactionRequest, TransactionRequestError> {
        let input_notes = notes.into_iter().map(|id| (id, None));
        self.input_notes(input_notes).build()
    }

    /// Consumes the builder and returns a [`TransactionRequest`] for a transaction to mint fungible
    /// assets. This request must be executed against a fungible faucet account.
    ///
    /// - `asset` is the fungible asset to be minted.
    /// - `target_id` is the account ID of the account to receive the minted asset.
    /// - `note_type` determines the visibility of the note to be created.
    /// - `rng` is the random number generator used to generate the serial number for the created
    ///   note.
    ///
    /// This function cannot be used with a previously set custom script.
    pub fn build_mint_fungible_asset(
        self,
        asset: FungibleAsset,
        target_id: AccountId,
        note_type: NoteType,
        rng: &mut ClientRng,
    ) -> Result<TransactionRequest, TransactionRequestError> {
        let created_note = P2idNote::create(
            asset.faucet_id(),
            target_id,
            vec![asset.into()],
            note_type,
            NoteAttachments::empty(),
            rng,
        )?;

        self.own_output_notes(vec![created_note]).build()
    }

    /// Consumes the builder and returns a [`TransactionRequest`] for a transaction to send a P2ID
    /// or P2IDE note. This request must be executed against the wallet sender account.
    ///
    /// - `payment_data` is the data for the payment transaction that contains the asset to be
    ///   transferred, the sender account ID, and the target account ID. If the recall or timelock
    ///   heights are set, a P2IDE note will be created; otherwise, a P2ID note will be created.
    /// - `note_type` determines the visibility of the note to be created.
    /// - `rng` is the random number generator used to generate the serial number for the created
    ///   note.
    ///
    /// This function cannot be used with a previously set custom script.
    pub fn build_pay_to_id(
        self,
        payment_data: PaymentNoteDescription,
        note_type: NoteType,
        rng: &mut ClientRng,
    ) -> Result<TransactionRequest, TransactionRequestError> {
        if payment_data
            .assets()
            .iter()
            .all(|asset| asset.is_fungible() && asset.unwrap_fungible().amount().as_u64() == 0)
        {
            return Err(TransactionRequestError::P2IDNoteWithoutAsset);
        }

        let created_note = payment_data.into_note(note_type, rng)?;

        self.own_output_notes(vec![created_note]).build()
    }

    /// Consumes the builder and returns a [`TransactionRequest`] for a transaction to send a SWAP
    /// note. This request must be executed against the wallet sender account.
    ///
    /// - `swap_data` is the data for the swap transaction that contains the sender account ID, the
    ///   offered asset, and the requested asset.
    /// - `note_type` determines the visibility of the note to be created.
    /// - `payback_note_type` determines the visibility of the payback note.
    /// - `rng` is the random number generator used to generate the serial number for the created
    ///   note.
    ///
    /// This function cannot be used with a previously set custom script.
    pub fn build_swap(
        self,
        swap_data: &SwapTransactionData,
        note_type: NoteType,
        payback_note_type: NoteType,
        rng: &mut ClientRng,
    ) -> Result<TransactionRequest, TransactionRequestError> {
        // The created note is the one that we need as the output of the tx, the other one is the
        // one that we expect to receive and consume eventually.
        let (created_note, payback_note_details) = SwapNote::create(
            swap_data.account_id(),
            swap_data.offered_asset(),
            swap_data.requested_asset(),
            note_type,
            NoteAttachments::empty(),
            payback_note_type,
            rng,
        )?;

        let payback_tag = NoteTag::with_account_target(swap_data.account_id());

        self.expected_future_notes(vec![(payback_note_details, payback_tag)])
            .own_output_notes(vec![created_note])
            .build()
    }

    /// Consumes the builder and returns a [`TransactionRequest`] for a transaction that registers
    /// note scripts in the node's script registry.
    ///
    /// This creates one public output note per script, each with empty assets and storage. The
    /// node indexes the script of every public note it processes, so submitting this transaction
    /// makes the scripts available for future network transactions (NTX).
    ///
    /// - `sender_account_id` is the account executing the transaction.
    /// - `scripts` is the list of note scripts to register.
    /// - `rng` is used to generate serial numbers for the registration notes.
    ///
    /// This function cannot be used with a previously set custom script.
    pub fn build_register_note_scripts(
        self,
        sender_account_id: AccountId,
        scripts: Vec<NoteScript>,
        rng: &mut ClientRng,
    ) -> Result<TransactionRequest, TransactionRequestError> {
        let registration_notes: Vec<Note> = scripts
            .into_iter()
            .map(|script| {
                let serial_num = rng.draw_word();
                let note_storage = NoteStorage::new(vec![])?;
                let recipient = NoteRecipient::new(serial_num, script, note_storage);
                let note_assets = NoteAssets::new(vec![])?;
                let metadata = PartialNoteMetadata::new(sender_account_id, NoteType::Public);
                Ok(Note::new(note_assets, metadata, recipient))
            })
            .collect::<Result<_, NoteError>>()?;

        self.own_output_notes(registration_notes).build()
    }

    /// Consumes the builder and returns a [`TransactionRequest`] for a transaction that creates a
    /// partial swap (PSWAP) note. This request must be executed against the creator account.
    ///
    /// - `pswap_data` is the data for the partial swap that contains the creator account ID, the
    ///   offered fungible asset, and the requested fungible asset.
    /// - `note_type` determines the visibility of the PSWAP note itself.
    /// - `payback_note_type` determines the visibility of the payback note that fillers emit back
    ///   to the creator. Typically [`NoteType::Private`] (cheaper; the fill amount is already
    ///   visible in the executing transaction).
    /// - `note_attachment` is the optional attachment for the PSWAP note. Pass `None` when there is
    ///   nothing to attach.
    /// - `rng` is the random number generator used to generate the serial number for the created
    ///   note.
    ///
    /// This function cannot be used with a previously set custom script.
    pub fn build_pswap_create(
        self,
        pswap_data: &PswapTransactionData,
        note_type: NoteType,
        payback_note_type: NoteType,
        note_attachment: Option<NoteAttachment>,
        rng: &mut ClientRng,
    ) -> Result<TransactionRequest, TransactionRequestError> {
        let storage = PswapNoteStorage::builder()
            .requested_asset(pswap_data.requested_asset())
            .creator_account_id(pswap_data.creator_account_id())
            .payback_note_type(payback_note_type)
            .build();

        let pswap_note = PswapNote::builder()
            .sender(pswap_data.creator_account_id())
            .storage(storage)
            .serial_number(rng.draw_word())
            .note_type(note_type)
            .offered_asset(pswap_data.offered_asset())
            .maybe_attachment(note_attachment)
            .build()
            .map_err(TransactionRequestError::NoteCreationError)?;

        let note: Note = pswap_note.into();
        self.own_output_notes(vec![note]).build()
    }

    /// Consumes the builder and returns a [`TransactionRequest`] for a transaction that consumes
    /// (fills) a partial swap (PSWAP) note. This request must be executed against the consumer
    /// account.
    ///
    /// - `pswap_note` is the PSWAP note being consumed.
    /// - `consumer_account_id` is the account consuming the swap.
    /// - `account_fill_amount` is the amount of the requested asset being provided by the consumer
    ///   account.
    /// - `note_fill_amount` is any additional amount being provided by other (in-flight) notes.
    ///
    /// This function cannot be used with a previously set custom script.
    pub fn build_pswap_consume(
        self,
        pswap_note: &Note,
        consumer_account_id: AccountId,
        account_fill_amount: u64,
        note_fill_amount: u64,
    ) -> Result<TransactionRequest, TransactionRequestError> {
        let pswap = PswapNote::try_from(pswap_note)
            .map_err(TransactionRequestError::NoteValidationError)?;

        let requested_faucet_id = pswap.storage().requested_asset().faucet_id();

        let account_fill_asset = FungibleAsset::new(requested_faucet_id, account_fill_amount)?;
        let note_fill_asset = FungibleAsset::new(requested_faucet_id, note_fill_amount)?;

        let (payback_note, remainder_pswap) = pswap
            .execute(consumer_account_id, Some(account_fill_asset), Some(note_fill_asset))
            .map_err(TransactionRequestError::NoteExecutionError)?;

        let note_args = PswapNote::create_args(account_fill_amount, note_fill_amount)
            .map_err(TransactionRequestError::NoteArgError)?;

        // Register output notes as expected future notes (created by the script, not the account).
        let payback_details = NoteDetails::from(&payback_note);
        let payback_tag = payback_note.metadata().tag();
        let payback_recipient = payback_note.recipient().clone();

        let mut expected_future_notes = vec![(payback_details, payback_tag)];
        let mut expected_recipients = vec![payback_recipient];

        if let Some(remainder) = remainder_pswap {
            let remainder_note: Note = remainder.into();
            expected_future_notes
                .push((NoteDetails::from(&remainder_note), remainder_note.metadata().tag()));
            expected_recipients.push(remainder_note.recipient().clone());
        }

        self.input_notes(vec![(pswap_note.clone(), Some(note_args))])
            .expected_future_notes(expected_future_notes)
            .expected_output_recipients(expected_recipients)
            .build()
    }

    /// Consumes the builder and returns a [`TransactionRequest`] for a transaction that cancels a
    /// partial swap (PSWAP) note. This request must be executed against the creator account.
    ///
    /// - `pswap_note` is the PSWAP note to cancel.
    /// - `creator_account_id` is the account that created the note. The note's stored creator must
    ///   match this ID; this is the account the resulting transaction must be executed against.
    ///
    /// This function cannot be used with a previously set custom script.
    pub fn build_pswap_cancel(
        self,
        pswap_note: Note,
        creator_account_id: AccountId,
    ) -> Result<TransactionRequest, TransactionRequestError> {
        let pswap = PswapNote::try_from(&pswap_note)
            .map_err(TransactionRequestError::NoteValidationError)?;

        let note_creator = pswap.storage().creator_account_id();
        if note_creator != creator_account_id {
            return Err(TransactionRequestError::PswapCancelCreatorMismatch {
                expected: note_creator,
                actual: creator_account_id,
            });
        }

        self.input_notes(vec![(pswap_note, None)]).build()
    }

    // FINALIZE BUILDER
    // --------------------------------------------------------------------------------------------

    /// Consumes the builder and returns a [`TransactionRequest`].
    ///
    /// # Errors
    /// - If both a custom script and own output notes are set.
    /// - If an expiration delta is set when a custom script is set.
    /// - If an invalid note variant is encountered in the own output notes.
    pub fn build(self) -> Result<TransactionRequest, TransactionRequestError> {
        let mut seen_input_notes = BTreeSet::new();
        for (note_id, _) in &self.input_notes_args {
            if !seen_input_notes.insert(note_id) {
                return Err(TransactionRequestError::DuplicateInputNote(*note_id));
            }
        }

        let script_template = match (self.custom_script, self.own_output_notes.is_empty()) {
            (Some(_), false) => {
                return Err(TransactionRequestError::ScriptTemplateError(
                    "Cannot set both a custom script and own output notes".to_string(),
                ));
            },
            (Some(script), true) => {
                if self.expiration_delta.is_some() {
                    return Err(TransactionRequestError::ScriptTemplateError(
                        "Cannot set expiration delta when a custom script is set".to_string(),
                    ));
                }

                Some(TransactionScriptTemplate::CustomScript(script))
            },
            (None, false) => {
                let partial_notes: Vec<PartialNote> =
                    self.own_output_notes.into_iter().map(Into::into).collect();

                Some(TransactionScriptTemplate::SendNotes(partial_notes))
            },
            (None, true) => None,
        };

        Ok(TransactionRequest {
            input_notes: self.input_notes,
            input_notes_args: self.input_notes_args,
            script_template,
            expected_output_recipients: self.expected_output_recipients,
            expected_future_notes: self.expected_future_notes,
            advice_map: self.advice_map,
            merkle_store: self.merkle_store,
            foreign_accounts: self.foreign_accounts,
            expiration_delta: self.expiration_delta,
            ignore_invalid_input_notes: self.ignore_invalid_input_notes,
            script_arg: self.script_arg,
            auth_arg: self.auth_arg,
            expected_ntx_scripts: self.expected_ntx_scripts,
        })
    }
}

// PAYMENT NOTE DESCRIPTION
// ================================================================================================

/// Contains information needed to create a payment note.
#[derive(Clone, Debug)]
pub struct PaymentNoteDescription {
    /// Assets that are meant to be sent to the target account.
    assets: Vec<Asset>,
    /// Account ID of the sender account.
    sender_account_id: AccountId,
    /// Account ID of the receiver account.
    target_account_id: AccountId,
    /// Optional reclaim height for the P2IDE note. It allows the possibility for the sender to
    /// reclaim the assets if the note has not been consumed by the target before this height.
    reclaim_height: Option<BlockNumber>,
    /// Optional timelock height for the P2IDE note. It allows the possibility to add a timelock to
    /// the asset transfer, meaning that the note can only be consumed after this height.
    timelock_height: Option<BlockNumber>,
}

impl PaymentNoteDescription {
    // CONSTRUCTORS
    // --------------------------------------------------------------------------------------------

    /// Creates a new [`PaymentNoteDescription`].
    pub fn new(
        assets: Vec<Asset>,
        sender_account_id: AccountId,
        target_account_id: AccountId,
    ) -> PaymentNoteDescription {
        PaymentNoteDescription {
            assets,
            sender_account_id,
            target_account_id,
            reclaim_height: None,
            timelock_height: None,
        }
    }

    /// Modifies the [`PaymentNoteDescription`] to set a reclaim height for payment note.
    #[must_use]
    pub fn with_reclaim_height(mut self, reclaim_height: BlockNumber) -> PaymentNoteDescription {
        self.reclaim_height = Some(reclaim_height);
        self
    }

    /// Modifies the [`PaymentNoteDescription`] to set a timelock height for payment note.
    #[must_use]
    pub fn with_timelock_height(mut self, timelock_height: BlockNumber) -> PaymentNoteDescription {
        self.timelock_height = Some(timelock_height);
        self
    }

    /// Returns the executor [`AccountId`].
    pub fn account_id(&self) -> AccountId {
        self.sender_account_id
    }

    /// Returns the target [`AccountId`].
    pub fn target_account_id(&self) -> AccountId {
        self.target_account_id
    }

    /// Returns the transaction's list of [`Asset`].
    pub fn assets(&self) -> &Vec<Asset> {
        &self.assets
    }

    /// Returns the reclaim height for the P2IDE note, if set.
    pub fn reclaim_height(&self) -> Option<BlockNumber> {
        self.reclaim_height
    }

    /// Returns the timelock height for the P2IDE note, if set.
    pub fn timelock_height(&self) -> Option<BlockNumber> {
        self.timelock_height
    }

    // CONVERSION
    // --------------------------------------------------------------------------------------------

    /// Converts the payment transaction data into a [`Note`] based on the specified fields. If the
    /// reclaim and timelock heights are not set, a P2ID note is created; otherwise, a P2IDE note is
    /// created.
    pub(crate) fn into_note(
        self,
        note_type: NoteType,
        rng: &mut ClientRng,
    ) -> Result<Note, NoteError> {
        if self.reclaim_height.is_none() && self.timelock_height.is_none() {
            // Create a P2ID note
            P2idNote::create(
                self.sender_account_id,
                self.target_account_id,
                self.assets,
                note_type,
                NoteAttachments::empty(),
                rng,
            )
        } else {
            // Create a P2IDE note
            P2ideNote::create(
                self.sender_account_id,
                P2ideNoteStorage::new(
                    self.target_account_id,
                    self.reclaim_height,
                    self.timelock_height,
                ),
                self.assets,
                note_type,
                NoteAttachments::empty(),
                rng,
            )
        }
    }
}

// SWAP TRANSACTION DATA
// ================================================================================================

/// Contains information related to a swap transaction.
///
/// A swap transaction involves creating a SWAP note, which will carry the offered asset and which,
/// when consumed, will create a payback note that carries the requested asset taken from the
/// consumer account's vault.
#[derive(Clone, Debug)]
pub struct SwapTransactionData {
    /// Account ID of the sender account.
    sender_account_id: AccountId,
    /// Asset that is offered in the swap.
    offered_asset: Asset,
    /// Asset that is expected in the payback note generated as a result of the swap.
    requested_asset: Asset,
}

impl SwapTransactionData {
    // CONSTRUCTORS
    // --------------------------------------------------------------------------------------------

    /// Creates a new [`SwapTransactionData`].
    pub fn new(
        sender_account_id: AccountId,
        offered_asset: Asset,
        requested_asset: Asset,
    ) -> SwapTransactionData {
        SwapTransactionData {
            sender_account_id,
            offered_asset,
            requested_asset,
        }
    }

    /// Returns the executor [`AccountId`].
    pub fn account_id(&self) -> AccountId {
        self.sender_account_id
    }

    /// Returns the transaction offered [`Asset`].
    pub fn offered_asset(&self) -> Asset {
        self.offered_asset
    }

    /// Returns the transaction requested [`Asset`].
    pub fn requested_asset(&self) -> Asset {
        self.requested_asset
    }
}

// PSWAP TRANSACTION DATA
// ================================================================================================

/// Contains information related to a partial swap (PSWAP) transaction.
///
/// A PSWAP transaction involves creating a PSWAP note that carries the offered fungible asset
/// and, when consumed (filled), produces a payback note carrying the requested fungible asset
/// taken from the filler's vault. Both legs are restricted to fungible assets so that fills can
/// be denominated in arbitrary amounts.
#[derive(Clone, Debug)]
pub struct PswapTransactionData {
    /// Account ID of the creator account.
    creator_account_id: AccountId,
    /// Fungible asset offered in the swap.
    offered_asset: FungibleAsset,
    /// Fungible asset expected in the payback note generated when the PSWAP is filled.
    requested_asset: FungibleAsset,
}

impl PswapTransactionData {
    // CONSTRUCTORS
    // --------------------------------------------------------------------------------------------

    /// Creates a new [`PswapTransactionData`].
    pub fn new(
        creator_account_id: AccountId,
        offered_asset: FungibleAsset,
        requested_asset: FungibleAsset,
    ) -> PswapTransactionData {
        PswapTransactionData {
            creator_account_id,
            offered_asset,
            requested_asset,
        }
    }

    /// Returns the creator [`AccountId`].
    pub fn creator_account_id(&self) -> AccountId {
        self.creator_account_id
    }

    /// Returns the offered [`FungibleAsset`].
    pub fn offered_asset(&self) -> FungibleAsset {
        self.offered_asset
    }

    /// Returns the requested [`FungibleAsset`].
    pub fn requested_asset(&self) -> FungibleAsset {
        self.requested_asset
    }
}
