namada_sdk/
tx.rs

1//! SDK functions to construct different types of transactions
2
3use std::borrow::Cow;
4use std::collections::BTreeMap;
5use std::fs::File;
6use std::path::{Path, PathBuf};
7use std::time::Duration;
8
9use borsh::BorshSerialize;
10use data::{Fee, GasLimit};
11use masp_primitives::asset_type::AssetType;
12use masp_primitives::transaction::Transaction as MaspTransaction;
13use masp_primitives::transaction::builder::Builder;
14use masp_primitives::transaction::components::I128Sum;
15use masp_primitives::transaction::components::sapling::builder::{
16    BuildParams, RngBuildParams,
17};
18use masp_primitives::transaction::components::sapling::fees::{
19    ConvertView, InputView as SaplingInputView, OutputView as SaplingOutputView,
20};
21use masp_primitives::transaction::components::transparent::fees::{
22    InputView as TransparentInputView, OutputView as TransparentOutputView,
23};
24use masp_primitives::zip32::PseudoExtendedKey;
25use namada_account::{InitAccount, UpdateAccount};
26use namada_core::address::{Address, IBC, MASP};
27use namada_core::arith::checked;
28use namada_core::chain::Epoch;
29use namada_core::collections::HashSet;
30use namada_core::dec::{Dec, POS_DECIMAL_PRECISION};
31use namada_core::hash::Hash;
32use namada_core::ibc::apps::nft_transfer::types::PrefixedClassId;
33use namada_core::ibc::apps::nft_transfer::types::msgs::transfer::MsgTransfer as IbcMsgNftTransfer;
34use namada_core::ibc::apps::nft_transfer::types::packet::PacketData as NftPacketData;
35use namada_core::ibc::apps::transfer::types::PrefixedCoin;
36use namada_core::ibc::apps::transfer::types::msgs::transfer::MsgTransfer as IbcMsgTransfer;
37use namada_core::ibc::apps::transfer::types::packet::PacketData;
38use namada_core::ibc::core::channel::types::timeout::{
39    TimeoutHeight, TimeoutTimestamp,
40};
41use namada_core::ibc::core::client::types::Height as IbcHeight;
42use namada_core::ibc::core::host::types::identifiers::{ChannelId, PortId};
43use namada_core::ibc::primitives::{IntoTimestamp, Timestamp as IbcTimestamp};
44use namada_core::key::{self, *};
45use namada_core::masp::{AssetData, MaspEpoch, TransferSource, TransferTarget};
46use namada_core::storage;
47use namada_core::time::DateTimeUtc;
48use namada_events::extend::EventAttributeEntry;
49use namada_governance::cli::onchain::{
50    DefaultProposal, OnChainProposal, PgfFundingProposal, PgfStewardProposal,
51};
52use namada_governance::pgf::cli::steward::Commission;
53use namada_governance::storage::proposal::{
54    InitProposalData, ProposalType, VoteProposalData,
55};
56use namada_governance::storage::vote::ProposalVote;
57use namada_ibc::storage::channel_key;
58use namada_ibc::trace::is_nft_trace;
59use namada_ibc::{MsgNftTransfer, MsgTransfer};
60use namada_io::{Client, Io, display_line, edisplay_line};
61use namada_proof_of_stake::parameters::{
62    MAX_VALIDATOR_METADATA_LEN, PosParams,
63};
64use namada_proof_of_stake::types::{CommissionPair, ValidatorState};
65use namada_token as token;
66use namada_token::DenominatedAmount;
67use namada_token::masp::shielded_wallet::ShieldedApi;
68use namada_token::masp::{MaspFeeData, MaspTransferData, ShieldedTransfer};
69use namada_token::storage_key::balance_key;
70use namada_tx::data::pgf::UpdateStewardCommission;
71use namada_tx::data::pos::{BecomeValidator, ConsensusKeyChange};
72use namada_tx::data::{
73    BatchedTxResult, DryRunResult, ResultCode, compute_inner_tx_hash, pos,
74};
75pub use namada_tx::{Authorization, *};
76use num_traits::Zero;
77use rand_core::{OsRng, RngCore};
78
79use crate::args::{TxTransparentSource, TxTransparentTarget, Wrapper};
80use crate::borsh::BorshSerializeExt;
81use crate::control_flow::time;
82use crate::error::{EncodingError, Error, QueryError, Result, TxSubmitError};
83use crate::rpc::{
84    self, InnerTxResult, TxBroadcastData, TxResponse, get_validator_stake,
85    query_wasm_code_hash, validate_amount,
86};
87use crate::signing::{
88    self, FeeAuthorization, SigningData, SigningTxData, SigningWrapperData,
89    TxSourcePostBalance, validate_fee, validate_transparent_fee,
90};
91use crate::tendermint_rpc::endpoint::broadcast::tx_sync::Response;
92use crate::tendermint_rpc::error::Error as RpcError;
93use crate::wallet::WalletIo;
94use crate::{Namada, args, events};
95
96/// Initialize account transaction WASM
97pub const TX_INIT_ACCOUNT_WASM: &str = "tx_init_account.wasm";
98/// Become validator transaction WASM path
99pub const TX_BECOME_VALIDATOR_WASM: &str = "tx_become_validator.wasm";
100/// Unjail validator transaction WASM path
101pub const TX_UNJAIL_VALIDATOR_WASM: &str = "tx_unjail_validator.wasm";
102/// Deactivate validator transaction WASM path
103pub const TX_DEACTIVATE_VALIDATOR_WASM: &str = "tx_deactivate_validator.wasm";
104/// Reactivate validator transaction WASM path
105pub const TX_REACTIVATE_VALIDATOR_WASM: &str = "tx_reactivate_validator.wasm";
106/// Initialize proposal transaction WASM path
107pub const TX_INIT_PROPOSAL: &str = "tx_init_proposal.wasm";
108/// Vote transaction WASM path
109pub const TX_VOTE_PROPOSAL: &str = "tx_vote_proposal.wasm";
110/// Reveal public key transaction WASM path
111pub const TX_REVEAL_PK: &str = "tx_reveal_pk.wasm";
112/// Update validity predicate WASM path
113pub const TX_UPDATE_ACCOUNT_WASM: &str = "tx_update_account.wasm";
114/// Transparent transfer transaction WASM path
115pub const TX_TRANSFER_WASM: &str = "tx_transfer.wasm";
116/// IBC transaction WASM path
117pub const TX_IBC_WASM: &str = "tx_ibc.wasm";
118/// User validity predicate WASM path
119pub const VP_USER_WASM: &str = "vp_user.wasm";
120/// Bond WASM path
121pub const TX_BOND_WASM: &str = "tx_bond.wasm";
122/// Unbond WASM path
123pub const TX_UNBOND_WASM: &str = "tx_unbond.wasm";
124/// Withdraw WASM path
125pub const TX_WITHDRAW_WASM: &str = "tx_withdraw.wasm";
126/// Claim-rewards WASM path
127pub const TX_CLAIM_REWARDS_WASM: &str = "tx_claim_rewards.wasm";
128/// Bridge pool WASM path
129pub const TX_BRIDGE_POOL_WASM: &str = "tx_bridge_pool.wasm";
130/// Change commission WASM path
131pub const TX_CHANGE_COMMISSION_WASM: &str =
132    "tx_change_validator_commission.wasm";
133/// Change consensus key WASM path
134pub const TX_CHANGE_CONSENSUS_KEY_WASM: &str = "tx_change_consensus_key.wasm";
135/// Change validator metadata WASM path
136pub const TX_CHANGE_METADATA_WASM: &str = "tx_change_validator_metadata.wasm";
137/// Resign steward WASM path
138pub const TX_RESIGN_STEWARD: &str = "tx_resign_steward.wasm";
139/// Update steward commission WASM path
140pub const TX_UPDATE_STEWARD_COMMISSION: &str =
141    "tx_update_steward_commission.wasm";
142/// Redelegate transaction WASM path
143pub const TX_REDELEGATE_WASM: &str = "tx_redelegate.wasm";
144
145/// Refund target alias prefix for IBC shielded transfers
146const IBC_REFUND_ALIAS_PREFIX: &str = "ibc-refund-target";
147
148/// Default timeout in seconds for requests to the `/accepted`
149/// and `/applied` ABCI query endpoints.
150const DEFAULT_NAMADA_EVENTS_MAX_WAIT_TIME_SECONDS: u64 = 60;
151
152/// Capture the result of running a transaction
153#[derive(Debug)]
154pub enum ProcessTxResponse {
155    /// Result of submitting a transaction to the blockchain
156    Applied(TxResponse),
157    /// Result of submitting a transaction to the mempool
158    Broadcast(Response),
159    /// Result of dry running transaction
160    DryRun(DryRunResult),
161}
162
163impl ProcessTxResponse {
164    /// Returns a `BatchedTxResult` if the transaction was applied and accepted
165    /// by all VPs. Note that this always returns false for dry-run
166    /// transactions.
167    pub fn is_applied_and_valid(
168        &self,
169        wrapper_hash: Option<&Hash>,
170        cmt: &TxCommitments,
171    ) -> Option<&BatchedTxResult> {
172        match self {
173            ProcessTxResponse::Applied(resp) => {
174                if resp.code == ResultCode::Ok {
175                    if let Some(InnerTxResult::Success(result)) =
176                        resp.batch_result().get(&compute_inner_tx_hash(
177                            wrapper_hash,
178                            either::Right(cmt),
179                        ))
180                    {
181                        return Some(result);
182                    }
183                }
184                None
185            }
186            ProcessTxResponse::DryRun(_) | ProcessTxResponse::Broadcast(_) => {
187                None
188            }
189        }
190    }
191}
192
193/// Build and dump a transaction either to file or to screen
194pub fn dump_tx<IO: Io>(
195    io: &IO,
196    dump_tx: args::DumpTx,
197    output_folder: Option<PathBuf>,
198    mut tx: Tx,
199) -> Result<()> {
200    let is_wrapper_tx = tx.header.wrapper().is_some();
201    if matches!(dump_tx, args::DumpTx::Inner) && is_wrapper_tx {
202        return Err(Error::Other(
203            "Requested tx-dump on a tx which is a wrapper".to_string(),
204        ));
205    };
206
207    if matches!(dump_tx, args::DumpTx::Wrapper) && !is_wrapper_tx {
208        return Err(Error::Other(
209            "Requested wrapper-dump on a tx which is not a wrapper".to_string(),
210        ));
211    }
212
213    // Remove duplicated sections before dumping. This is useful in case the
214    // dumped tx needed to be signed offline
215    tx.prune_duplicated_sections();
216
217    match output_folder {
218        Some(path) => {
219            let tx_path = path.join(format!(
220                "{}.tx",
221                tx.header_hash().to_string().to_lowercase()
222            ));
223            let out = File::create(&tx_path)
224                .expect("Should be able to create a file to dump tx");
225            tx.to_writer_json(out)
226                .expect("Should be able to write to file.");
227            display_line!(
228                io,
229                "Transaction serialized to {}.",
230                tx_path.to_string_lossy()
231            );
232        }
233        None => {
234            let serialized_tx = serde_json::to_string_pretty(&tx)
235                .expect("Should be able to json encode the tx.");
236            display_line!(io, "Below the serialized transaction: \n");
237            display_line!(io, "{}", serialized_tx)
238        }
239    }
240
241    Ok(())
242}
243
244/// Submit transaction and wait for result. Returns a list of addresses
245/// initialized in the transaction if any. In dry run, this is always empty.
246pub async fn process_tx(
247    context: &impl Namada,
248    args: &args::Tx,
249    tx: Tx,
250) -> Result<ProcessTxResponse> {
251    // NOTE: use this to print the request JSON body:
252
253    // let request =
254    // tendermint_rpc::endpoint::broadcast::tx_commit::Request::new(
255    //     tx_bytes.clone().into(),
256    // );
257    // use tendermint_rpc::Request;
258    // let request_body = request.into_json();
259    // println!("HTTP request body: {}", request_body);
260
261    if let Some(dry_run) = &args.dry_run {
262        let is_wrapper_tx = tx.header.wrapper().is_some();
263        if matches!(dry_run, args::DryRun::Inner) && is_wrapper_tx {
264            return Err(Error::Other(
265                "Requested tx-dry-run on a tx which is a wrapper".to_string(),
266            ));
267        };
268
269        if matches!(dry_run, args::DryRun::Wrapper) && !is_wrapper_tx {
270            return Err(Error::Other(
271                "Requested wrapper-dry-run on a tx which is not a wrapper"
272                    .to_string(),
273            ));
274        }
275        expect_dry_broadcast(TxBroadcastData::DryRun(tx), context).await
276    } else {
277        // We use this to determine when the wrapper tx makes it on-chain
278        let tx_hash = tx.header_hash().to_string();
279        let cmts = tx.commitments().clone();
280        let wrapper_hash = tx.wrapper_hash();
281        if wrapper_hash.is_none() {
282            return Err(Error::Other(
283                "Can't submit a non-wrapper transaction".to_string(),
284            ));
285        }
286        // We use this to determine when the inner tx makes it
287        // on-chain
288        let to_broadcast = TxBroadcastData::Live { tx, tx_hash };
289        if args
290            .wrap_tx
291            .as_ref()
292            .is_some_and(|wrap_tx| wrap_tx.broadcast_only)
293        {
294            broadcast_tx(context, &to_broadcast)
295                .await
296                .map(ProcessTxResponse::Broadcast)
297        } else {
298            match submit_tx(context, to_broadcast).await {
299                Ok(resp) => {
300                    for cmt in cmts {
301                        if let Some(InnerTxResult::Success(result)) =
302                            resp.batch_result().get(&compute_inner_tx_hash(
303                                wrapper_hash.as_ref(),
304                                either::Right(&cmt),
305                            ))
306                        {
307                            save_initialized_accounts(
308                                context,
309                                args,
310                                result.initialized_accounts.clone(),
311                            )
312                            .await;
313                        }
314                    }
315                    Ok(ProcessTxResponse::Applied(resp))
316                }
317                Err(x) => Err(x),
318            }
319        }
320    }
321}
322
323/// Check if a reveal public key transaction is needed
324pub async fn is_reveal_pk_needed<C: Client + Sync>(
325    client: &C,
326    address: &Address,
327) -> Result<bool> {
328    // Check if PK revealed
329    Ok(!has_revealed_pk(client, address).await?)
330}
331
332/// Check if the public key for the given address has been revealed
333pub async fn has_revealed_pk<C: Client + Sync>(
334    client: &C,
335    address: &Address,
336) -> Result<bool> {
337    rpc::is_public_key_revealed(client, address).await
338}
339
340/// Submit transaction to reveal the given public key
341pub async fn build_reveal_pk(
342    context: &impl Namada,
343    args: &args::Tx,
344    public_key: &common::PublicKey,
345) -> Result<(Tx, SigningData)> {
346    let (signing_data, wrap_args, _) = derive_build_data(
347        context,
348        args.wrap_tx.as_ref().map(|wrap_args| ExtendedWrapperArgs {
349            wrap_args,
350            disposable_gas_payer: false,
351        }),
352        args.force,
353        None,
354        args.signing_keys.to_owned(),
355        vec![],
356    )
357    .await?;
358
359    build(
360        context,
361        args,
362        args.tx_reveal_code_path.clone(),
363        public_key,
364        do_nothing,
365        wrap_args,
366    )
367    .await
368    .map(|tx| (tx, signing_data))
369}
370
371/// Broadcast a transaction to be included in the blockchain and checks that
372/// the tx has been successfully included into the mempool of a node
373///
374/// In the case of errors in any of those stages, an error message is returned
375pub async fn broadcast_tx(
376    context: &impl Namada,
377    to_broadcast: &TxBroadcastData,
378) -> Result<Response> {
379    let (tx, tx_hash) = match to_broadcast {
380        TxBroadcastData::Live { tx, tx_hash } => Ok((tx, tx_hash)),
381        TxBroadcastData::DryRun(tx) => {
382            Err(TxSubmitError::ExpectLiveRun(tx.clone()))
383        }
384    }?;
385
386    tracing::debug!(
387        transaction = ?to_broadcast,
388        "Broadcasting transaction",
389    );
390
391    let response = lift_rpc_error(
392        context.client().broadcast_tx_sync(tx.to_bytes()).await,
393    )?;
394
395    if response.code == 0.into() {
396        display_line!(context.io(), "Transaction added to mempool.");
397        tracing::debug!("Transaction mempool response: {response:#?}");
398        // Print the transaction identifiers to enable the extraction of
399        // acceptance/application results later
400        {
401            display_line!(context.io(), "Transaction hash: {tx_hash}",);
402        }
403        Ok(response)
404    } else {
405        Err(Error::from(TxSubmitError::TxBroadcast(RpcError::server(
406            serde_json::to_string(&response).map_err(|err| {
407                Error::from(EncodingError::Serde(err.to_string()))
408            })?,
409        ))))
410    }
411}
412
413/// Broadcast a transaction to be included in the blockchain.
414///
415/// Checks that
416/// 1. The tx has been successfully included into the mempool of a validator
417/// 2. The tx has been included on the blockchain
418///
419/// In the case of errors in any of those stages, an error message is returned
420pub async fn submit_tx(
421    context: &impl Namada,
422    to_broadcast: TxBroadcastData,
423) -> Result<TxResponse> {
424    let (_, tx_hash) = match &to_broadcast {
425        TxBroadcastData::Live { tx, tx_hash } => Ok((tx, tx_hash)),
426        TxBroadcastData::DryRun(tx) => {
427            Err(TxSubmitError::ExpectLiveRun(tx.clone()))
428        }
429    }?;
430
431    // Broadcast the supplied transaction
432    broadcast_tx(context, &to_broadcast).await?;
433
434    #[allow(clippy::disallowed_methods)]
435    let deadline = time::Instant::now()
436        + time::Duration::from_secs(
437            DEFAULT_NAMADA_EVENTS_MAX_WAIT_TIME_SECONDS,
438        );
439
440    tracing::debug!(
441        transaction = ?to_broadcast,
442        ?deadline,
443        "Awaiting transaction approval",
444    );
445
446    // The transaction is now on chain. We wait for it to be applied
447    let tx_query = rpc::TxEventQuery::Applied(tx_hash.as_str());
448    let tx_events = rpc::query_tx_status(context, tx_query, deadline).await?;
449    let response = TxResponse::from_events(tx_events);
450    display_batch_resp(context, &response);
451    Ok(response)
452}
453
454/// Display a result of a tx batch.
455pub fn display_batch_resp(context: &impl Namada, resp: &TxResponse) {
456    // Wrapper-level logs
457    let wrapper_successful = if let ResultCode::Ok = resp.code {
458        display_line!(
459            context.io(),
460            "Transaction batch {} was applied at height {}.",
461            resp.hash,
462            resp.height,
463        );
464        true
465    } else {
466        let err = match resp.code {
467            ResultCode::Ok => unreachable!(),
468            ResultCode::WasmRuntimeError => "wasm runtime",
469            ResultCode::InvalidTx => "invalid transaction",
470            ResultCode::InvalidSig => "invalid signature",
471            ResultCode::AllocationError => "allocation",
472            ResultCode::ReplayTx => "transaction replay",
473            ResultCode::InvalidChainId => "invalid chain ID",
474            ResultCode::ExpiredTx => "transaction expired",
475            ResultCode::TxGasLimit => "gas limit",
476            ResultCode::FeeError => "fee",
477            ResultCode::InvalidVoteExtension => "invalid vote extension",
478            ResultCode::TooLarge => "transaction too large",
479            ResultCode::TxNotAllowlisted => "transaction not allowlisted",
480        };
481        let err_msg = if resp.info.is_empty() {
482            err.to_string()
483        } else {
484            format!("{err}, {}", resp.info)
485        };
486        display_line!(
487            context.io(),
488            "Transaction batch {} failed at height {} with error: {}.",
489            resp.hash,
490            resp.height,
491            err_msg
492        );
493        false
494    };
495    let batch_results = resp.batch_result();
496    // If the batch contains at least one transaction
497    if let Some((first_inner_hash, first_result)) = batch_results.first() {
498        if !wrapper_successful {
499            // Check if fees were paid via the shielded pool, in this case the
500            // first transaction of the batch was committed regardless of the
501            // batch failure (even for atomic batches)
502            let masp_fee_payment = matches!(
503                first_result,
504                InnerTxResult::Success(res)
505                    if res.events.iter().any(|event| {
506                        event.kind() == &namada_tx::event::masp_types::FEE_PAYMENT
507                    })
508            );
509            if masp_fee_payment {
510                display_line!(
511                    context.io(),
512                    "The first transaction of the batch ({}) was applied to \
513                     pay the fees via the MASP. Since the batch failed, none \
514                     of the remaining transactions listed below have been \
515                     committed. Their results are provided for completeness.",
516                    first_inner_hash
517                );
518            } else {
519                display_line!(
520                    context.io(),
521                    "Since the batch in its entirety failed, none of the \
522                     transactions listed below have been committed. Their \
523                     results are provided for completeness.",
524                );
525            }
526        }
527        display_line!(context.io(), "Batch results:");
528    }
529
530    // Batch-level logs
531    let mut all_inners_successful = true;
532    for (inner_hash, result) in batch_results {
533        match result {
534            InnerTxResult::Success(result) => {
535                display_line!(
536                    context.io(),
537                    "Transaction {} was successfully applied.",
538                    inner_hash,
539                );
540                if !result.events.is_empty() {
541                    display_line!(context.io(), "Events:");
542                    for event in result.events.clone() {
543                        display_line!(
544                            context.io(),
545                            "{:2} - {} - {}:",
546                            "",
547                            event.level(),
548                            event.kind(),
549                        );
550                        for (k, v) in
551                            event.into_attributes().iter().filter(|(k, _v)| {
552                                // Filter out data that's already displayed
553                                // above
554                                *k != events::extend::TxHash::KEY
555                                    && *k != events::extend::Height::KEY
556                                    && *k != events::extend::InnerTxHash::KEY
557                            })
558                        {
559                            display_line!(context.io(), "{:4} - {k}: {v}", "")
560                        }
561                    }
562                }
563            }
564            InnerTxResult::VpsRejected(inner) => {
565                let changed_keys: Vec<_> = inner
566                    .changed_keys
567                    .iter()
568                    .map(storage::Key::to_string)
569                    .collect();
570                edisplay_line!(
571                    context.io(),
572                    "Transaction {} was rejected by VPs: {}\nErrors: \
573                     {}\nChanged keys: {}",
574                    inner_hash,
575                    serde_json::to_string_pretty(
576                        &inner.vps_result.rejected_vps
577                    )
578                    .unwrap(),
579                    serde_json::to_string_pretty(&inner.vps_result.errors)
580                        .unwrap(),
581                    serde_json::to_string_pretty(&changed_keys).unwrap(),
582                );
583                all_inners_successful = false;
584            }
585            InnerTxResult::OtherFailure(msg) => {
586                edisplay_line!(
587                    context.io(),
588                    "Transaction {} failed.\nDetails: {}",
589                    inner_hash,
590                    msg
591                );
592                all_inners_successful = false;
593            }
594        }
595    }
596
597    // Display the gas used only if the entire batch was successful. In all the
598    // other cases the gas consumed is misleading since most likely the inner
599    // transactions did not have the chance to run until completion. This could
600    // trick the user into setting wrong gas limit values when trying to
601    // resubmit the tx
602    if wrapper_successful && all_inners_successful {
603        edisplay_line!(
604            context.io(),
605            "The batch consumed {} gas units.",
606            resp.gas_used,
607        );
608    }
609
610    tracing::debug!(
611        "Full result: {}",
612        serde_json::to_string_pretty(&resp).unwrap()
613    );
614}
615
616/// Save accounts initialized from a tx into the wallet, if any.
617pub async fn save_initialized_accounts<N: Namada>(
618    context: &N,
619    args: &args::Tx,
620    initialized_accounts: Vec<Address>,
621) {
622    let len = initialized_accounts.len();
623    if len != 0 {
624        // Store newly initialized account addresses in the wallet
625        display_line!(
626            context.io(),
627            "The transaction initialized {} new account{}",
628            len,
629            if len == 1 { "" } else { "s" }
630        );
631        // Store newly initialized account addresses in the wallet
632        for (ix, address) in initialized_accounts.iter().enumerate() {
633            let encoded = address.encode();
634            let alias: Cow<'_, str> = match &args.initialized_account_alias {
635                Some(initialized_account_alias) => {
636                    if len == 1 {
637                        // If there's only one account, use the
638                        // alias as is
639                        initialized_account_alias.into()
640                    } else {
641                        // If there're multiple accounts, use
642                        // the alias as prefix, followed by
643                        // index number
644                        format!("{}{}", initialized_account_alias, ix).into()
645                    }
646                }
647                None => N::WalletUtils::read_alias(&encoded).into(),
648            };
649            let alias = alias.into_owned();
650            let added = context.wallet_mut().await.insert_address(
651                alias.clone(),
652                address.clone(),
653                args.wallet_alias_force,
654            );
655            match added {
656                Some(new_alias) if new_alias != encoded => {
657                    display_line!(
658                        context.io(),
659                        "Added alias {} for address {}.",
660                        new_alias,
661                        encoded
662                    );
663                }
664                _ => {
665                    display_line!(
666                        context.io(),
667                        "No alias added for address {}.",
668                        encoded
669                    )
670                }
671            };
672        }
673    }
674}
675
676/// Submit validator commission rate change
677pub async fn build_change_consensus_key(
678    context: &impl Namada,
679    args::ConsensusKeyChange {
680        tx: tx_args,
681        validator,
682        consensus_key,
683        tx_code_path,
684        unsafe_dont_encrypt: _,
685    }: &args::ConsensusKeyChange,
686) -> Result<(Tx, SigningData)> {
687    let consensus_key = if let Some(consensus_key) = consensus_key {
688        consensus_key
689    } else {
690        edisplay_line!(context.io(), "Consensus key must must be present.");
691        return Err(Error::from(TxSubmitError::Other(
692            "Consensus key must must be present.".to_string(),
693        )));
694    };
695
696    // Check that the new consensus key is unique
697    let consensus_keys = rpc::get_consensus_keys(context.client()).await?;
698
699    if consensus_keys.contains(consensus_key) {
700        edisplay_line!(
701            context.io(),
702            "The consensus key is already being used."
703        );
704        return Err(Error::from(TxSubmitError::ConsensusKeyNotUnique));
705    }
706
707    let data = ConsensusKeyChange {
708        validator: validator.clone(),
709        consensus_key: consensus_key.clone(),
710    };
711
712    let signing_keys = [
713        tx_args.signing_keys.to_owned(),
714        vec![consensus_key.to_owned()],
715    ]
716    .concat();
717    let (signing_data, wrap_args, _) = derive_build_data(
718        context,
719        tx_args
720            .wrap_tx
721            .as_ref()
722            .map(|wrap_args| ExtendedWrapperArgs {
723                wrap_args,
724                disposable_gas_payer: false,
725            }),
726        tx_args.force,
727        None,
728        signing_keys,
729        vec![],
730    )
731    .await?;
732
733    build(
734        context,
735        tx_args,
736        tx_code_path.clone(),
737        data,
738        do_nothing,
739        wrap_args,
740    )
741    .await
742    .map(|tx| (tx, signing_data))
743}
744
745/// Submit validator commission rate change
746pub async fn build_validator_commission_change(
747    context: &impl Namada,
748    args::CommissionRateChange {
749        tx: tx_args,
750        validator,
751        rate,
752        tx_code_path,
753    }: &args::CommissionRateChange,
754) -> Result<(Tx, SigningData)> {
755    let (signing_data, wrap_args, _) = derive_build_data(
756        context,
757        tx_args
758            .wrap_tx
759            .as_ref()
760            .map(|wrap_args| ExtendedWrapperArgs {
761                wrap_args,
762                disposable_gas_payer: false,
763            }),
764        tx_args.force,
765        Some(validator.clone()),
766        tx_args.signing_keys.to_owned(),
767        vec![],
768    )
769    .await?;
770
771    let epoch = rpc::query_epoch(context.client()).await?;
772
773    let params: PosParams = rpc::get_pos_params(context.client()).await?;
774
775    let validator = validator.clone();
776    if rpc::is_validator(context.client(), &validator).await? {
777        if *rate < Dec::zero() || *rate > Dec::one() {
778            edisplay_line!(
779                context.io(),
780                "Invalid new commission rate, received {}",
781                rate
782            );
783            return Err(Error::from(TxSubmitError::InvalidCommissionRate(
784                *rate,
785            )));
786        }
787
788        let pipeline_epoch_minus_one =
789            epoch.unchecked_add(params.pipeline_len - 1);
790
791        let CommissionPair {
792            commission_rate,
793            max_commission_change_per_epoch,
794            epoch: _,
795        } = rpc::query_commission_rate(
796            context.client(),
797            &validator,
798            Some(pipeline_epoch_minus_one),
799        )
800        .await?;
801
802        match (commission_rate, max_commission_change_per_epoch) {
803            (Some(commission_rate), Some(max_commission_change_per_epoch)) => {
804                if rate.is_negative() || *rate > Dec::one() {
805                    edisplay_line!(
806                        context.io(),
807                        "New rate is outside of the allowed range of values \
808                         between 0.0 and 1.0."
809                    );
810                    if !tx_args.force {
811                        return Err(Error::from(
812                            TxSubmitError::InvalidCommissionRate(*rate),
813                        ));
814                    }
815                }
816                if rate.abs_diff(commission_rate)?
817                    > max_commission_change_per_epoch
818                {
819                    edisplay_line!(
820                        context.io(),
821                        "New rate is too large of a change with respect to \
822                         the predecessor epoch in which the rate will take \
823                         effect."
824                    );
825                    if !tx_args.force {
826                        return Err(Error::from(
827                            TxSubmitError::InvalidCommissionRate(*rate),
828                        ));
829                    }
830                }
831            }
832            (None, None) => {
833                edisplay_line!(
834                    context.io(),
835                    "Error retrieving commission data from validator storage. \
836                     This address may not yet be a validator."
837                );
838                if !tx_args.force {
839                    return Err(Error::from(TxSubmitError::Retrieval));
840                }
841            }
842            _ => {
843                edisplay_line!(
844                    context.io(),
845                    "Error retrieving some of the commission data from \
846                     validator storage, while other data was found. This is a \
847                     bug and should be reported."
848                );
849                if !tx_args.force {
850                    return Err(Error::from(TxSubmitError::Retrieval));
851                }
852            }
853        }
854    } else {
855        edisplay_line!(
856            context.io(),
857            "The given address {validator} is not a validator."
858        );
859        if !tx_args.force {
860            return Err(Error::from(TxSubmitError::InvalidValidatorAddress(
861                validator,
862            )));
863        }
864    }
865
866    let data = pos::CommissionChange {
867        validator: validator.clone(),
868        new_rate: *rate,
869    };
870
871    build(
872        context,
873        tx_args,
874        tx_code_path.clone(),
875        data,
876        do_nothing,
877        wrap_args,
878    )
879    .await
880    .map(|tx| (tx, signing_data))
881}
882
883/// Submit validator metadata change
884pub async fn build_validator_metadata_change(
885    context: &impl Namada,
886    args::MetaDataChange {
887        tx: tx_args,
888        validator,
889        email,
890        description,
891        website,
892        discord_handle,
893        avatar,
894        name,
895        commission_rate,
896        tx_code_path,
897    }: &args::MetaDataChange,
898) -> Result<(Tx, SigningData)> {
899    let (signing_data, wrap_args, _) = derive_build_data(
900        context,
901        tx_args
902            .wrap_tx
903            .as_ref()
904            .map(|wrap_args| ExtendedWrapperArgs {
905                wrap_args,
906                disposable_gas_payer: false,
907            }),
908        tx_args.force,
909        Some(validator.clone()),
910        tx_args.signing_keys.to_owned(),
911        vec![],
912    )
913    .await?;
914
915    let epoch = rpc::query_epoch(context.client()).await?;
916
917    let params: PosParams = rpc::get_pos_params(context.client()).await?;
918
919    // The validator must actually be a validator
920    let validator =
921        known_validator_or_err(validator.clone(), tx_args.force, context)
922            .await?;
923
924    // If there is a new email, it cannot be an empty string that indicates to
925    // remove the data (email data cannot be removed)
926    if let Some(email) = email.as_ref() {
927        if email.is_empty() {
928            edisplay_line!(
929                context.io(),
930                "Cannot remove a validator's email, which was implied by the \
931                 empty string"
932            );
933            return Err(Error::from(TxSubmitError::InvalidEmail));
934        }
935        // Check that the email is within MAX_VALIDATOR_METADATA_LEN characters
936        if email.len() as u64 > MAX_VALIDATOR_METADATA_LEN {
937            edisplay_line!(
938                context.io(),
939                "Email provided is too long, must be within \
940                 {MAX_VALIDATOR_METADATA_LEN} characters"
941            );
942            if !tx_args.force {
943                return Err(Error::from(TxSubmitError::MetadataTooLong));
944            }
945        }
946    }
947
948    // Check that any new metadata provided is within MAX_VALIDATOR_METADATA_LEN
949    // characters
950    if let Some(description) = description.as_ref() {
951        if description.len() as u64 > MAX_VALIDATOR_METADATA_LEN {
952            edisplay_line!(
953                context.io(),
954                "Description provided is too long, must be within \
955                 {MAX_VALIDATOR_METADATA_LEN} characters"
956            );
957            if !tx_args.force {
958                return Err(Error::from(TxSubmitError::MetadataTooLong));
959            }
960        }
961    }
962    if let Some(website) = website.as_ref() {
963        if website.len() as u64 > MAX_VALIDATOR_METADATA_LEN {
964            edisplay_line!(
965                context.io(),
966                "Website provided is too long, must be within \
967                 {MAX_VALIDATOR_METADATA_LEN} characters"
968            );
969            if !tx_args.force {
970                return Err(Error::from(TxSubmitError::MetadataTooLong));
971            }
972        }
973    }
974    if let Some(discord_handle) = discord_handle.as_ref() {
975        if discord_handle.len() as u64 > MAX_VALIDATOR_METADATA_LEN {
976            edisplay_line!(
977                context.io(),
978                "Discord handle provided is too long, must be within \
979                 {MAX_VALIDATOR_METADATA_LEN} characters"
980            );
981            if !tx_args.force {
982                return Err(Error::from(TxSubmitError::MetadataTooLong));
983            }
984        }
985    }
986    if let Some(avatar) = avatar.as_ref() {
987        if avatar.len() as u64 > MAX_VALIDATOR_METADATA_LEN {
988            edisplay_line!(
989                context.io(),
990                "Avatar provided is too long, must be within \
991                 {MAX_VALIDATOR_METADATA_LEN} characters"
992            );
993            if !tx_args.force {
994                return Err(Error::from(TxSubmitError::MetadataTooLong));
995            }
996        }
997    }
998    if let Some(name) = name.as_ref() {
999        if name.len() as u64 > MAX_VALIDATOR_METADATA_LEN {
1000            edisplay_line!(
1001                context.io(),
1002                "Name provided is too long, must be within \
1003                 {MAX_VALIDATOR_METADATA_LEN} characters"
1004            );
1005            if !tx_args.force {
1006                return Err(Error::from(TxSubmitError::MetadataTooLong));
1007            }
1008        }
1009    }
1010
1011    // If there's a new commission rate, it must be valid
1012    if let Some(rate) = commission_rate.as_ref() {
1013        if *rate < Dec::zero() || *rate > Dec::one() {
1014            edisplay_line!(
1015                context.io(),
1016                "Invalid new commission rate, received {}",
1017                rate
1018            );
1019            if !tx_args.force {
1020                return Err(Error::from(TxSubmitError::InvalidCommissionRate(
1021                    *rate,
1022                )));
1023            }
1024        }
1025        let pipeline_epoch_minus_one =
1026            epoch.unchecked_add(params.pipeline_len - 1);
1027
1028        let CommissionPair {
1029            commission_rate,
1030            max_commission_change_per_epoch,
1031            epoch: _,
1032        } = rpc::query_commission_rate(
1033            context.client(),
1034            &validator,
1035            Some(pipeline_epoch_minus_one),
1036        )
1037        .await?;
1038
1039        match (commission_rate, max_commission_change_per_epoch) {
1040            (Some(commission_rate), Some(max_commission_change_per_epoch)) => {
1041                if rate.is_negative() || *rate > Dec::one() {
1042                    edisplay_line!(
1043                        context.io(),
1044                        "New rate is outside of the allowed range of values \
1045                         between 0.0 and 1.0."
1046                    );
1047                    if !tx_args.force {
1048                        return Err(Error::from(
1049                            TxSubmitError::InvalidCommissionRate(*rate),
1050                        ));
1051                    }
1052                }
1053                if rate.abs_diff(commission_rate)?
1054                    > max_commission_change_per_epoch
1055                {
1056                    edisplay_line!(
1057                        context.io(),
1058                        "New rate is too large of a change with respect to \
1059                         the predecessor epoch in which the rate will take \
1060                         effect."
1061                    );
1062                    if !tx_args.force {
1063                        return Err(Error::from(
1064                            TxSubmitError::InvalidCommissionRate(*rate),
1065                        ));
1066                    }
1067                }
1068            }
1069            (None, None) => {
1070                edisplay_line!(
1071                    context.io(),
1072                    "Error retrieving commission data from validator storage. \
1073                     This address may not yet be a validator."
1074                );
1075                if !tx_args.force {
1076                    return Err(Error::from(TxSubmitError::Retrieval));
1077                }
1078            }
1079            _ => {
1080                edisplay_line!(
1081                    context.io(),
1082                    "Error retrieving some of the commission data from \
1083                     validator storage, while other data was found. This is a \
1084                     bug and should be reported."
1085                );
1086                if !tx_args.force {
1087                    return Err(Error::from(TxSubmitError::Retrieval));
1088                }
1089            }
1090        }
1091    }
1092
1093    let data = pos::MetaDataChange {
1094        validator: validator.clone(),
1095        email: email.clone(),
1096        website: website.clone(),
1097        description: description.clone(),
1098        discord_handle: discord_handle.clone(),
1099        avatar: avatar.clone(),
1100        name: name.clone(),
1101        commission_rate: *commission_rate,
1102    };
1103
1104    build(
1105        context,
1106        tx_args,
1107        tx_code_path.clone(),
1108        data,
1109        do_nothing,
1110        wrap_args,
1111    )
1112    .await
1113    .map(|tx| (tx, signing_data))
1114}
1115
1116/// Craft transaction to update a steward commission
1117pub async fn build_update_steward_commission(
1118    context: &impl Namada,
1119    args::UpdateStewardCommission {
1120        tx: tx_args,
1121        steward,
1122        commission,
1123        tx_code_path,
1124    }: &args::UpdateStewardCommission,
1125) -> Result<(Tx, SigningData)> {
1126    let (signing_data, wrap_args, _) = derive_build_data(
1127        context,
1128        tx_args
1129            .wrap_tx
1130            .as_ref()
1131            .map(|wrap_args| ExtendedWrapperArgs {
1132                wrap_args,
1133                disposable_gas_payer: false,
1134            }),
1135        tx_args.force,
1136        Some(steward.clone()),
1137        tx_args.signing_keys.to_owned(),
1138        vec![],
1139    )
1140    .await?;
1141
1142    if !rpc::is_steward(context.client(), steward).await {
1143        edisplay_line!(
1144            context.io(),
1145            "The given address {} is not a steward.",
1146            &steward
1147        );
1148        if !tx_args.force {
1149            return Err(Error::from(TxSubmitError::InvalidSteward(
1150                steward.clone(),
1151            )));
1152        }
1153    };
1154
1155    let commission = Commission::try_from(commission.as_ref())
1156        .map_err(|e| TxSubmitError::InvalidStewardCommission(e.to_string()))?;
1157
1158    if !commission.is_valid() {
1159        edisplay_line!(
1160            context.io(),
1161            "The sum of all percentage must not be greater than 1."
1162        );
1163        if !tx_args.force {
1164            return Err(Error::from(TxSubmitError::InvalidStewardCommission(
1165                "Commission sum is greater than 1.".to_string(),
1166            )));
1167        }
1168    }
1169
1170    let data = UpdateStewardCommission {
1171        steward: steward.clone(),
1172        commission: commission.reward_distribution,
1173    };
1174
1175    build(
1176        context,
1177        tx_args,
1178        tx_code_path.clone(),
1179        data,
1180        do_nothing,
1181        wrap_args,
1182    )
1183    .await
1184    .map(|tx| (tx, signing_data))
1185}
1186
1187/// Craft transaction to resign as a steward
1188pub async fn build_resign_steward(
1189    context: &impl Namada,
1190    args::ResignSteward {
1191        tx: tx_args,
1192        steward,
1193        tx_code_path,
1194    }: &args::ResignSteward,
1195) -> Result<(Tx, SigningData)> {
1196    let (signing_data, wrap_args, _) = derive_build_data(
1197        context,
1198        tx_args
1199            .wrap_tx
1200            .as_ref()
1201            .map(|wrap_args| ExtendedWrapperArgs {
1202                wrap_args,
1203                disposable_gas_payer: false,
1204            }),
1205        tx_args.force,
1206        Some(steward.clone()),
1207        tx_args.signing_keys.to_owned(),
1208        vec![],
1209    )
1210    .await?;
1211
1212    if !rpc::is_steward(context.client(), steward).await {
1213        edisplay_line!(
1214            context.io(),
1215            "The given address {} is not a steward.",
1216            &steward
1217        );
1218        if !tx_args.force {
1219            return Err(Error::from(TxSubmitError::InvalidSteward(
1220                steward.clone(),
1221            )));
1222        }
1223    };
1224
1225    build(
1226        context,
1227        tx_args,
1228        tx_code_path.clone(),
1229        steward.clone(),
1230        do_nothing,
1231        wrap_args,
1232    )
1233    .await
1234    .map(|tx| (tx, signing_data))
1235}
1236
1237/// Submit transaction to unjail a jailed validator
1238pub async fn build_unjail_validator(
1239    context: &impl Namada,
1240    args::TxUnjailValidator {
1241        tx: tx_args,
1242        validator,
1243        tx_code_path,
1244    }: &args::TxUnjailValidator,
1245) -> Result<(Tx, SigningData)> {
1246    let (signing_data, wrap_args, _) = derive_build_data(
1247        context,
1248        tx_args
1249            .wrap_tx
1250            .as_ref()
1251            .map(|wrap_args| ExtendedWrapperArgs {
1252                wrap_args,
1253                disposable_gas_payer: false,
1254            }),
1255        tx_args.force,
1256        Some(validator.clone()),
1257        tx_args.signing_keys.to_owned(),
1258        vec![],
1259    )
1260    .await?;
1261
1262    if !rpc::is_validator(context.client(), validator).await? {
1263        edisplay_line!(
1264            context.io(),
1265            "The given address {} is not a validator.",
1266            &validator
1267        );
1268        if !tx_args.force {
1269            return Err(Error::from(TxSubmitError::InvalidValidatorAddress(
1270                validator.clone(),
1271            )));
1272        }
1273    }
1274
1275    let params: PosParams = rpc::get_pos_params(context.client()).await?;
1276    let current_epoch = rpc::query_epoch(context.client()).await?;
1277    let pipeline_epoch = current_epoch.unchecked_add(params.pipeline_len);
1278
1279    let (validator_state_at_pipeline, _) = rpc::get_validator_state(
1280        context.client(),
1281        validator,
1282        Some(pipeline_epoch),
1283    )
1284    .await?;
1285    if validator_state_at_pipeline != Some(ValidatorState::Jailed) {
1286        edisplay_line!(
1287            context.io(),
1288            "The given validator address {} is not jailed at the pipeline \
1289             epoch when it would be restored to one of the validator sets.",
1290            &validator
1291        );
1292        if !tx_args.force {
1293            return Err(Error::from(
1294                TxSubmitError::ValidatorNotCurrentlyJailed(validator.clone()),
1295            ));
1296        }
1297    }
1298
1299    let last_slash_epoch =
1300        rpc::query_last_infraction_epoch(context.client(), validator).await;
1301    match last_slash_epoch {
1302        Ok(Some(last_slash_epoch)) => {
1303            // Jailed due to slashing
1304            let eligible_epoch = last_slash_epoch
1305                .unchecked_add(params.slash_processing_epoch_offset());
1306            if current_epoch < eligible_epoch {
1307                edisplay_line!(
1308                    context.io(),
1309                    "The given validator address {} is currently frozen and \
1310                     will be eligible to be unjailed starting at epoch {}.",
1311                    &validator,
1312                    eligible_epoch
1313                );
1314                if !tx_args.force {
1315                    return Err(Error::from(TxSubmitError::ValidatorFrozen(
1316                        validator.clone(),
1317                    )));
1318                }
1319            }
1320        }
1321        Ok(None) => {
1322            // Jailed due to liveness only. No checks needed.
1323        }
1324        Err(err) => {
1325            if !tx_args.force {
1326                return Err(err);
1327            }
1328        }
1329    }
1330
1331    build(
1332        context,
1333        tx_args,
1334        tx_code_path.clone(),
1335        validator.clone(),
1336        do_nothing,
1337        wrap_args,
1338    )
1339    .await
1340    .map(|tx| (tx, signing_data))
1341}
1342
1343/// Submit transaction to deactivate a validator
1344pub async fn build_deactivate_validator(
1345    context: &impl Namada,
1346    args::TxDeactivateValidator {
1347        tx: tx_args,
1348        validator,
1349        tx_code_path,
1350    }: &args::TxDeactivateValidator,
1351) -> Result<(Tx, SigningData)> {
1352    let (signing_data, wrap_args, _) = derive_build_data(
1353        context,
1354        tx_args
1355            .wrap_tx
1356            .as_ref()
1357            .map(|wrap_args| ExtendedWrapperArgs {
1358                wrap_args,
1359                disposable_gas_payer: false,
1360            }),
1361        tx_args.force,
1362        Some(validator.clone()),
1363        tx_args.signing_keys.to_owned(),
1364        vec![],
1365    )
1366    .await?;
1367
1368    // Check if the validator address is actually a validator
1369    if !rpc::is_validator(context.client(), validator).await? {
1370        edisplay_line!(
1371            context.io(),
1372            "The given address {} is not a validator.",
1373            &validator
1374        );
1375        if !tx_args.force {
1376            return Err(Error::from(TxSubmitError::InvalidValidatorAddress(
1377                validator.clone(),
1378            )));
1379        }
1380    }
1381
1382    let params: PosParams = rpc::get_pos_params(context.client()).await?;
1383    let current_epoch = rpc::query_epoch(context.client()).await?;
1384    let pipeline_epoch = current_epoch.unchecked_add(params.pipeline_len);
1385
1386    let (validator_state_at_pipeline, _) = rpc::get_validator_state(
1387        context.client(),
1388        validator,
1389        Some(pipeline_epoch),
1390    )
1391    .await?;
1392    if validator_state_at_pipeline == Some(ValidatorState::Inactive) {
1393        edisplay_line!(
1394            context.io(),
1395            "The given validator address {} is already inactive at the \
1396             pipeline epoch {}.",
1397            &validator,
1398            &pipeline_epoch
1399        );
1400        if !tx_args.force {
1401            return Err(Error::from(TxSubmitError::ValidatorInactive(
1402                validator.clone(),
1403                pipeline_epoch,
1404            )));
1405        }
1406    }
1407
1408    build(
1409        context,
1410        tx_args,
1411        tx_code_path.clone(),
1412        validator.clone(),
1413        do_nothing,
1414        wrap_args,
1415    )
1416    .await
1417    .map(|tx| (tx, signing_data))
1418}
1419
1420/// Submit transaction to deactivate a validator
1421pub async fn build_reactivate_validator(
1422    context: &impl Namada,
1423    args::TxReactivateValidator {
1424        tx: tx_args,
1425        validator,
1426        tx_code_path,
1427    }: &args::TxReactivateValidator,
1428) -> Result<(Tx, SigningData)> {
1429    let (signing_data, wrap_args, _) = derive_build_data(
1430        context,
1431        tx_args
1432            .wrap_tx
1433            .as_ref()
1434            .map(|wrap_args| ExtendedWrapperArgs {
1435                wrap_args,
1436                disposable_gas_payer: false,
1437            }),
1438        tx_args.force,
1439        Some(validator.clone()),
1440        tx_args.signing_keys.to_owned(),
1441        vec![],
1442    )
1443    .await?;
1444
1445    // Check if the validator address is actually a validator
1446    if !rpc::is_validator(context.client(), validator).await? {
1447        edisplay_line!(
1448            context.io(),
1449            "The given address {} is not a validator.",
1450            &validator
1451        );
1452        if !tx_args.force {
1453            return Err(Error::from(TxSubmitError::InvalidValidatorAddress(
1454                validator.clone(),
1455            )));
1456        }
1457    }
1458
1459    let params: PosParams = rpc::get_pos_params(context.client()).await?;
1460    let current_epoch = rpc::query_epoch(context.client()).await?;
1461    let pipeline_epoch = current_epoch.unchecked_add(params.pipeline_len);
1462
1463    for epoch in Epoch::iter_bounds_inclusive(current_epoch, pipeline_epoch) {
1464        let (validator_state, _) =
1465            rpc::get_validator_state(context.client(), validator, Some(epoch))
1466                .await?;
1467
1468        if validator_state != Some(ValidatorState::Inactive) {
1469            edisplay_line!(
1470                context.io(),
1471                "The given validator address {} is not inactive at epoch {}.",
1472                &validator,
1473                &epoch
1474            );
1475            if !tx_args.force {
1476                return Err(Error::from(TxSubmitError::ValidatorNotInactive(
1477                    validator.clone(),
1478                    epoch,
1479                )));
1480            }
1481        }
1482    }
1483
1484    build(
1485        context,
1486        tx_args,
1487        tx_code_path.clone(),
1488        validator.clone(),
1489        do_nothing,
1490        wrap_args,
1491    )
1492    .await
1493    .map(|tx| (tx, signing_data))
1494}
1495
1496/// Redelegate bonded tokens from one validator to another
1497pub async fn build_redelegation(
1498    context: &impl Namada,
1499    args::Redelegate {
1500        tx: tx_args,
1501        src_validator,
1502        dest_validator,
1503        owner,
1504        amount: redel_amount,
1505        tx_code_path,
1506    }: &args::Redelegate,
1507) -> Result<(Tx, SigningData)> {
1508    // Require a positive amount of tokens to be redelegated
1509    if redel_amount.is_zero() {
1510        edisplay_line!(
1511            context.io(),
1512            "The requested redelegation amount is 0. A positive amount must \
1513             be requested."
1514        );
1515        if !tx_args.force {
1516            return Err(Error::from(TxSubmitError::RedelegationIsZero));
1517        }
1518    }
1519
1520    // The src and dest validators must actually be validators
1521    let src_validator =
1522        known_validator_or_err(src_validator.clone(), tx_args.force, context)
1523            .await?;
1524    let dest_validator =
1525        known_validator_or_err(dest_validator.clone(), tx_args.force, context)
1526            .await?;
1527
1528    // The delegator (owner) must exist on-chain and must not be a validator
1529    let owner =
1530        source_exists_or_err(owner.clone(), tx_args.force, context).await?;
1531    if rpc::is_validator(context.client(), &owner).await? {
1532        edisplay_line!(
1533            context.io(),
1534            "The given address {} is a validator. A validator is prohibited \
1535             from redelegating its own bonds.",
1536            &owner
1537        );
1538        if !tx_args.force {
1539            return Err(Error::from(TxSubmitError::RedelegatorIsValidator(
1540                owner.clone(),
1541            )));
1542        }
1543    }
1544
1545    // Prohibit redelegation to the same validator
1546    if src_validator == dest_validator {
1547        edisplay_line!(
1548            context.io(),
1549            "The provided source and destination validators are the same. \
1550             Redelegation is not allowed to the same validator."
1551        );
1552        if !tx_args.force {
1553            return Err(Error::from(TxSubmitError::RedelegationSrcEqDest));
1554        }
1555    }
1556
1557    // Prohibit chained redelegations
1558    let params = rpc::get_pos_params(context.client()).await?;
1559    let incoming_redel_epoch = rpc::query_incoming_redelegations(
1560        context.client(),
1561        &src_validator,
1562        &owner,
1563    )
1564    .await?;
1565    let current_epoch = rpc::query_epoch(context.client()).await?;
1566    let earliest_redeleg_epoch =
1567        if let Some(redel_end_epoch) = incoming_redel_epoch {
1568            let last_contrib_epoch =
1569                redel_end_epoch.prev().expect("End epoch must have a prev");
1570            let earliest_redeleg_epoch = last_contrib_epoch
1571                .unchecked_add(params.slash_processing_epoch_offset());
1572            (earliest_redeleg_epoch > current_epoch)
1573                .then_some(earliest_redeleg_epoch)
1574        } else {
1575            None
1576        };
1577    if let Some(earliest_redeleg_epoch) = earliest_redeleg_epoch {
1578        edisplay_line!(
1579            context.io(),
1580            "The source validator {} has an incoming redelegation from the \
1581             delegator {} that may still be subject to future slashing. \
1582             Redelegation is not allowed until epoch {} when this is no \
1583             longer the case.",
1584            &src_validator,
1585            &owner,
1586            earliest_redeleg_epoch
1587        );
1588        if !tx_args.force {
1589            return Err(Error::from(
1590                TxSubmitError::IncomingRedelIsStillSlashable(
1591                    owner.clone(),
1592                    src_validator.clone(),
1593                    earliest_redeleg_epoch,
1594                ),
1595            ));
1596        }
1597    }
1598
1599    // Give a redelegation warning based on the pipeline state of the dest
1600    // validator
1601    let pipeline_epoch = current_epoch.unchecked_add(params.pipeline_len);
1602    let (dest_validator_state_at_pipeline, _) = rpc::get_validator_state(
1603        context.client(),
1604        &dest_validator,
1605        Some(pipeline_epoch),
1606    )
1607    .await?;
1608    if dest_validator_state_at_pipeline == Some(ValidatorState::Inactive) {
1609        edisplay_line!(
1610            context.io(),
1611            "WARNING: the given destination validator address {} is inactive \
1612             at the pipeline epoch {}. If you would still like to redelegate \
1613             to the inactive validator, use the --force option.",
1614            &dest_validator,
1615            &pipeline_epoch
1616        );
1617        if !tx_args.force {
1618            return Err(Error::from(TxSubmitError::ValidatorInactive(
1619                dest_validator.clone(),
1620                pipeline_epoch,
1621            )));
1622        }
1623    }
1624
1625    // There must be at least as many tokens in the bond as the requested
1626    // redelegation amount
1627    let bond_amount =
1628        rpc::query_bond(context.client(), &owner, &src_validator, None).await?;
1629    if *redel_amount > bond_amount {
1630        edisplay_line!(
1631            context.io(),
1632            "There are not enough tokens available for the desired \
1633             redelegation at the current epoch {}. Requested to redelegate {} \
1634             tokens but only {} tokens are available.",
1635            current_epoch,
1636            redel_amount.to_string_native(),
1637            bond_amount.to_string_native()
1638        );
1639        if !tx_args.force {
1640            return Err(Error::from(
1641                TxSubmitError::RedelegationAmountTooLarge(
1642                    redel_amount.to_string_native(),
1643                    bond_amount.to_string_native(),
1644                ),
1645            ));
1646        }
1647    } else {
1648        display_line!(
1649            context.io(),
1650            "{} NAM tokens available for redelegation. Submitting \
1651             redelegation transaction for {} tokens...",
1652            bond_amount.to_string_native(),
1653            redel_amount.to_string_native()
1654        );
1655    }
1656
1657    let (signing_data, wrap_args, _) = derive_build_data(
1658        context,
1659        tx_args
1660            .wrap_tx
1661            .as_ref()
1662            .map(|wrap_args| ExtendedWrapperArgs {
1663                wrap_args,
1664                disposable_gas_payer: false,
1665            }),
1666        tx_args.force,
1667        Some(owner.clone()),
1668        tx_args.signing_keys.to_owned(),
1669        vec![],
1670    )
1671    .await?;
1672
1673    let data = pos::Redelegation {
1674        src_validator,
1675        dest_validator,
1676        owner,
1677        amount: *redel_amount,
1678    };
1679
1680    build(
1681        context,
1682        tx_args,
1683        tx_code_path.clone(),
1684        data,
1685        do_nothing,
1686        wrap_args,
1687    )
1688    .await
1689    .map(|tx| (tx, signing_data))
1690}
1691
1692/// Submit transaction to withdraw an unbond
1693pub async fn build_withdraw(
1694    context: &impl Namada,
1695    args::Withdraw {
1696        tx: tx_args,
1697        validator,
1698        source,
1699        tx_code_path,
1700    }: &args::Withdraw,
1701) -> Result<(Tx, SigningData)> {
1702    let default_signer = Some(source.clone().unwrap_or(validator.clone()));
1703    let (signing_data, wrap_args, _) = derive_build_data(
1704        context,
1705        tx_args
1706            .wrap_tx
1707            .as_ref()
1708            .map(|wrap_args| ExtendedWrapperArgs {
1709                wrap_args,
1710                disposable_gas_payer: false,
1711            }),
1712        tx_args.force,
1713        default_signer,
1714        tx_args.signing_keys.to_owned(),
1715        vec![],
1716    )
1717    .await?;
1718
1719    let epoch = rpc::query_epoch(context.client()).await?;
1720
1721    // Check that the validator address is actually a validator
1722    let validator =
1723        known_validator_or_err(validator.clone(), tx_args.force, context)
1724            .await?;
1725
1726    // Check that the source address exists on chain
1727    let source = match source.clone() {
1728        Some(source) => source_exists_or_err(source, tx_args.force, context)
1729            .await
1730            .map(Some),
1731        None => Ok(source.clone()),
1732    }?;
1733
1734    // Check the source's current unbond amount
1735    let bond_source = source.clone().unwrap_or_else(|| validator.clone());
1736    let tokens = rpc::query_withdrawable_tokens(
1737        context.client(),
1738        &bond_source,
1739        &validator,
1740        Some(epoch),
1741    )
1742    .await?;
1743
1744    if tokens.is_zero() {
1745        edisplay_line!(
1746            context.io(),
1747            "There are no unbonded bonds ready to withdraw in the current \
1748             epoch {}.",
1749            epoch
1750        );
1751        rpc::query_and_print_unbonds(context, &bond_source, &validator).await?;
1752        if !tx_args.force {
1753            return Err(Error::from(TxSubmitError::NoUnbondReady(epoch)));
1754        }
1755    } else {
1756        display_line!(
1757            context.io(),
1758            "Found {} tokens that can be withdrawn.",
1759            tokens.to_string_native()
1760        );
1761        display_line!(
1762            context.io(),
1763            "Submitting transaction to withdraw them..."
1764        );
1765    }
1766
1767    let data = pos::Withdraw { validator, source };
1768
1769    build(
1770        context,
1771        tx_args,
1772        tx_code_path.clone(),
1773        data,
1774        do_nothing,
1775        wrap_args,
1776    )
1777    .await
1778    .map(|tx| (tx, signing_data))
1779}
1780
1781/// Submit transaction to withdraw an unbond
1782pub async fn build_claim_rewards(
1783    context: &impl Namada,
1784    args::ClaimRewards {
1785        tx: tx_args,
1786        validator,
1787        source,
1788        tx_code_path,
1789    }: &args::ClaimRewards,
1790) -> Result<(Tx, SigningData)> {
1791    let default_signer = Some(source.clone().unwrap_or(validator.clone()));
1792    let (signing_data, wrap_args, _) = derive_build_data(
1793        context,
1794        tx_args
1795            .wrap_tx
1796            .as_ref()
1797            .map(|wrap_args| ExtendedWrapperArgs {
1798                wrap_args,
1799                disposable_gas_payer: false,
1800            }),
1801        tx_args.force,
1802        default_signer,
1803        tx_args.signing_keys.to_owned(),
1804        vec![],
1805    )
1806    .await?;
1807
1808    // Check that the validator address is actually a validator
1809    let validator =
1810        known_validator_or_err(validator.clone(), tx_args.force, context)
1811            .await?;
1812
1813    // Check that the source address exists on chain
1814    let source = match source.clone() {
1815        Some(source) => source_exists_or_err(source, tx_args.force, context)
1816            .await
1817            .map(Some),
1818        None => Ok(source.clone()),
1819    }?;
1820
1821    let data = pos::ClaimRewards { validator, source };
1822
1823    build(
1824        context,
1825        tx_args,
1826        tx_code_path.clone(),
1827        data,
1828        do_nothing,
1829        wrap_args,
1830    )
1831    .await
1832    .map(|tx| (tx, signing_data))
1833}
1834
1835/// Submit a transaction to unbond
1836pub async fn build_unbond(
1837    context: &impl Namada,
1838    args::Unbond {
1839        tx: tx_args,
1840        validator,
1841        amount,
1842        source,
1843        tx_code_path,
1844    }: &args::Unbond,
1845) -> Result<(Tx, SigningData, Option<(Epoch, token::Amount)>)> {
1846    // Require a positive amount of tokens to be bonded
1847    if amount.is_zero() {
1848        edisplay_line!(
1849            context.io(),
1850            "The requested bond amount is 0. A positive amount must be \
1851             requested."
1852        );
1853        if !tx_args.force {
1854            return Err(Error::from(TxSubmitError::BondIsZero));
1855        }
1856    }
1857
1858    // The validator must actually be a validator
1859    let validator =
1860        known_validator_or_err(validator.clone(), tx_args.force, context)
1861            .await?;
1862
1863    // Check that the source address exists on chain
1864    let source = match source.clone() {
1865        Some(source) => source_exists_or_err(source, tx_args.force, context)
1866            .await
1867            .map(Some),
1868        None => Ok(source.clone()),
1869    }?;
1870
1871    // Check that the validator is not frozen due to slashes
1872    let last_slash_epoch =
1873        rpc::query_last_infraction_epoch(context.client(), &validator).await?;
1874    if let Some(infraction_epoch) = last_slash_epoch {
1875        let params = rpc::get_pos_params(context.client()).await?;
1876        let current_epoch = rpc::query_epoch(context.client()).await?;
1877
1878        let eligible_epoch = infraction_epoch
1879            .unchecked_add(params.slash_processing_epoch_offset());
1880        if current_epoch < eligible_epoch {
1881            edisplay_line!(
1882                context.io(),
1883                "The validator {} is currently frozen due to an infraction in \
1884                 epoch {}. Unbonds can be processed starting at epoch {}.",
1885                &validator,
1886                infraction_epoch,
1887                eligible_epoch
1888            );
1889            if !tx_args.force {
1890                return Err(Error::from(TxSubmitError::ValidatorFrozen(
1891                    validator.clone(),
1892                )));
1893            }
1894        }
1895    }
1896
1897    let default_signer = Some(source.clone().unwrap_or(validator.clone()));
1898    let (signing_data, wrap_args, _) = derive_build_data(
1899        context,
1900        tx_args
1901            .wrap_tx
1902            .as_ref()
1903            .map(|wrap_args| ExtendedWrapperArgs {
1904                wrap_args,
1905                disposable_gas_payer: false,
1906            }),
1907        tx_args.force,
1908        default_signer,
1909        tx_args.signing_keys.to_owned(),
1910        vec![],
1911    )
1912    .await?;
1913
1914    // Check the source's current bond amount
1915    let bond_source = source.clone().unwrap_or_else(|| validator.clone());
1916
1917    let bond_amount =
1918        rpc::query_bond(context.client(), &bond_source, &validator, None)
1919            .await?;
1920    display_line!(
1921        context.io(),
1922        "Bond amount available for unbonding: {} NAM",
1923        bond_amount.to_string_native()
1924    );
1925
1926    if *amount > bond_amount {
1927        edisplay_line!(
1928            context.io(),
1929            "The total bonds of the source {} is lower than the amount to be \
1930             unbonded. Amount to unbond is {} and the total bonds is {}.",
1931            bond_source,
1932            amount.to_string_native(),
1933            bond_amount.to_string_native(),
1934        );
1935        if !tx_args.force {
1936            return Err(Error::from(TxSubmitError::LowerBondThanUnbond(
1937                bond_source,
1938                amount.to_string_native(),
1939                bond_amount.to_string_native(),
1940            )));
1941        }
1942    }
1943
1944    // Query the unbonds before submitting the tx
1945    let unbonds = rpc::query_unbond_with_slashing(
1946        context.client(),
1947        &bond_source,
1948        &validator,
1949    )
1950    .await?;
1951    let mut withdrawable = BTreeMap::<Epoch, token::Amount>::new();
1952    for ((_start_epoch, withdraw_epoch), amount) in unbonds.into_iter() {
1953        let to_withdraw = withdrawable.entry(withdraw_epoch).or_default();
1954        *to_withdraw = checked!(to_withdraw + amount)?;
1955    }
1956    let latest_withdrawal_pre = withdrawable.into_iter().next_back();
1957
1958    let data = pos::Unbond {
1959        validator: validator.clone(),
1960        amount: *amount,
1961        source: source.clone(),
1962    };
1963
1964    let tx = build(
1965        context,
1966        tx_args,
1967        tx_code_path.clone(),
1968        data,
1969        do_nothing,
1970        wrap_args,
1971    )
1972    .await?;
1973    Ok((tx, signing_data, latest_withdrawal_pre))
1974}
1975
1976/// Query the unbonds post-tx
1977pub async fn query_unbonds(
1978    context: &impl Namada,
1979    args: args::Unbond,
1980    latest_withdrawal_pre: Option<(Epoch, token::Amount)>,
1981) -> Result<()> {
1982    let source = args.source.clone();
1983    // Check the source's current bond amount
1984    let bond_source = source.clone().unwrap_or_else(|| args.validator.clone());
1985
1986    // Query the unbonds post-tx
1987    let unbonds = rpc::query_unbond_with_slashing(
1988        context.client(),
1989        &bond_source,
1990        &args.validator,
1991    )
1992    .await?;
1993    let mut withdrawable = BTreeMap::<Epoch, token::Amount>::new();
1994    for ((_start_epoch, withdraw_epoch), amount) in unbonds.into_iter() {
1995        let to_withdraw = withdrawable.entry(withdraw_epoch).or_default();
1996        *to_withdraw = checked!(to_withdraw + amount)?;
1997    }
1998    let (latest_withdraw_epoch_post, latest_withdraw_amount_post) =
1999        withdrawable.into_iter().next_back().ok_or_else(|| {
2000            Error::Other("No withdrawable amount".to_string())
2001        })?;
2002
2003    if let Some((latest_withdraw_epoch_pre, latest_withdraw_amount_pre)) =
2004        latest_withdrawal_pre
2005    {
2006        match latest_withdraw_epoch_post.cmp(&latest_withdraw_epoch_pre) {
2007            std::cmp::Ordering::Less => {
2008                if args.tx.force {
2009                    edisplay_line!(
2010                        context.io(),
2011                        "Unexpected behavior reading the unbonds data has \
2012                         occurred"
2013                    );
2014                } else {
2015                    return Err(Error::from(TxSubmitError::UnbondError));
2016                }
2017            }
2018            std::cmp::Ordering::Equal => {
2019                display_line!(
2020                    context.io(),
2021                    "Amount {} withdrawable starting from epoch {}",
2022                    checked!(
2023                        latest_withdraw_amount_post
2024                            - latest_withdraw_amount_pre
2025                    )?
2026                    .to_string_native(),
2027                    latest_withdraw_epoch_post
2028                );
2029            }
2030            std::cmp::Ordering::Greater => {
2031                display_line!(
2032                    context.io(),
2033                    "Amount {} withdrawable starting from epoch {}",
2034                    latest_withdraw_amount_post.to_string_native(),
2035                    latest_withdraw_epoch_post,
2036                );
2037            }
2038        }
2039    } else {
2040        display_line!(
2041            context.io(),
2042            "Amount {} withdrawable starting from epoch {}",
2043            latest_withdraw_amount_post.to_string_native(),
2044            latest_withdraw_epoch_post,
2045        );
2046    }
2047    Ok(())
2048}
2049
2050pub(crate) struct ExtendedWrapperArgs<'args> {
2051    pub(crate) wrap_args: &'args Wrapper,
2052    pub(crate) disposable_gas_payer: bool,
2053}
2054
2055// Given the SDK arguments, extracts the necessary data to properly build the
2056// transaction, which are: the [`SigningData`] and the optional [`WrapArgs`] and
2057// updated balance
2058pub(crate) async fn derive_build_data<'args>(
2059    context: &impl Namada,
2060    wrap_args: Option<ExtendedWrapperArgs<'args>>,
2061    force: bool,
2062    default_signer: Option<Address>,
2063    signing_keys: Vec<common::CommonPublicKey>,
2064    signatures: Vec<Vec<u8>>,
2065) -> Result<(SigningData, Option<WrapArgs>, Option<TxSourcePostBalance>)> {
2066    match wrap_args {
2067        Some(ExtendedWrapperArgs {
2068            wrap_args,
2069            disposable_gas_payer,
2070        }) => {
2071            let signing_data = signing::aux_signing_data(
2072                context,
2073                wrap_args,
2074                default_signer,
2075                signing_keys,
2076                disposable_gas_payer,
2077                signatures,
2078            )
2079            .await?;
2080            let fee_payer = signing_data.fee_payer_or_err()?.to_owned();
2081            let (fee_amount, updated_balance) = if disposable_gas_payer {
2082                // MASP fee payment
2083                (validate_fee(context, wrap_args, force).await?, None)
2084            } else {
2085                // Transparent fee payment
2086                validate_transparent_fee(context, wrap_args, force, &fee_payer)
2087                    .await
2088                    .map(|(fee_amount, updated_balance)| {
2089                        (fee_amount, Some(updated_balance))
2090                    })?
2091            };
2092
2093            Ok((
2094                SigningData::Wrapper(signing_data),
2095                Some(WrapArgs {
2096                    fee_amount,
2097                    fee_payer,
2098                    fee_token: wrap_args.fee_token.to_owned(),
2099                    gas_limit: wrap_args.gas_limit,
2100                }),
2101                updated_balance,
2102            ))
2103        }
2104        None => {
2105            let signing_data = signing::aux_inner_signing_data(
2106                context,
2107                signing_keys,
2108                default_signer,
2109                signatures,
2110            )
2111            .await?;
2112
2113            Ok((SigningData::Inner(signing_data), None, None))
2114        }
2115    }
2116}
2117
2118/// Submit a transaction to bond
2119pub async fn build_bond(
2120    context: &impl Namada,
2121    args::Bond {
2122        tx: tx_args,
2123        validator,
2124        amount,
2125        source,
2126        tx_code_path,
2127    }: &args::Bond,
2128) -> Result<(Tx, SigningData)> {
2129    // Require a positive amount of tokens to be bonded
2130    if amount.is_zero() {
2131        edisplay_line!(
2132            context.io(),
2133            "The requested bond amount is 0. A positive amount must be \
2134             requested."
2135        );
2136        if !tx_args.force {
2137            return Err(Error::from(TxSubmitError::BondIsZero));
2138        }
2139    }
2140
2141    // The validator must actually be a validator
2142    let validator =
2143        known_validator_or_err(validator.clone(), tx_args.force, context)
2144            .await?;
2145
2146    // Check that the source address exists on chain
2147    let mut is_src_also_val = false;
2148    let source = match source.clone() {
2149        Some(source) => {
2150            is_src_also_val =
2151                rpc::is_validator(context.client(), &source).await?;
2152            source_exists_or_err(source, tx_args.force, context)
2153                .await
2154                .map(Some)
2155        }
2156        None => Ok(source.clone()),
2157    }?;
2158
2159    // Check that the source is not a different validator bonding to validator
2160    if is_src_also_val && source != Some(validator.clone()) {
2161        edisplay_line!(
2162            context.io(),
2163            "The given source address {} is a validator. A validator is \
2164             prohibited from bonding to another validator.",
2165            &source.clone().unwrap()
2166        );
2167        if !tx_args.force {
2168            return Err(Error::from(TxSubmitError::InvalidBondPair(
2169                source.clone().unwrap(),
2170                validator.clone(),
2171            )));
2172        }
2173    }
2174
2175    // Give a bonding warning based on the pipeline state
2176    let params: PosParams = rpc::get_pos_params(context.client()).await?;
2177    let current_epoch = rpc::query_epoch(context.client()).await?;
2178    let pipeline_epoch = current_epoch.unchecked_add(params.pipeline_len);
2179    let (validator_state_at_pipeline, _) = rpc::get_validator_state(
2180        context.client(),
2181        &validator,
2182        Some(pipeline_epoch),
2183    )
2184    .await?;
2185    if validator_state_at_pipeline == Some(ValidatorState::Inactive) {
2186        edisplay_line!(
2187            context.io(),
2188            "WARNING: the given validator address {} is inactive at the \
2189             pipeline epoch {}. If you would still like to bond to the \
2190             inactive validator, use the --force option.",
2191            &validator,
2192            &pipeline_epoch
2193        );
2194        if !tx_args.force {
2195            return Err(Error::from(TxSubmitError::ValidatorInactive(
2196                validator.clone(),
2197                pipeline_epoch,
2198            )));
2199        }
2200    }
2201
2202    let default_signer = Some(source.clone().unwrap_or(validator.clone()));
2203    let (signing_data, wrap_args, updated_balance) = derive_build_data(
2204        context,
2205        tx_args
2206            .wrap_tx
2207            .as_ref()
2208            .map(|wrap_args| ExtendedWrapperArgs {
2209                wrap_args,
2210                disposable_gas_payer: false,
2211            }),
2212        tx_args.force,
2213        default_signer,
2214        tx_args.signing_keys.to_owned(),
2215        vec![],
2216    )
2217    .await?;
2218
2219    // Check bond's source (source for delegation or validator for self-bonds)
2220    // balance
2221    let bond_source = source.as_ref().unwrap_or(&validator);
2222    let native_token = context.native_token();
2223    let check_balance = match updated_balance {
2224        Some(updated_balance)
2225            if &updated_balance.source == bond_source
2226                && updated_balance.token == native_token =>
2227        {
2228            CheckBalance::Balance(updated_balance.post_balance)
2229        }
2230        _ => CheckBalance::Query(balance_key(&native_token, bond_source)),
2231    };
2232    check_balance_too_low_err(
2233        &native_token,
2234        bond_source,
2235        *amount,
2236        check_balance,
2237        tx_args.force,
2238        context,
2239    )
2240    .await?;
2241
2242    let data = pos::Bond {
2243        validator,
2244        amount: *amount,
2245        source,
2246    };
2247
2248    build(
2249        context,
2250        tx_args,
2251        tx_code_path.clone(),
2252        data,
2253        do_nothing,
2254        wrap_args,
2255    )
2256    .await
2257    .map(|tx| (tx, signing_data))
2258}
2259
2260/// Build a default proposal governance
2261pub async fn build_default_proposal(
2262    context: &impl Namada,
2263    args::InitProposal {
2264        tx,
2265        proposal_data: _,
2266        is_pgf_stewards: _,
2267        is_pgf_funding: _,
2268        tx_code_path,
2269    }: &args::InitProposal,
2270    proposal: DefaultProposal,
2271) -> Result<(Tx, SigningData)> {
2272    let (signing_data, wrap_args, _) = derive_build_data(
2273        context,
2274        tx.wrap_tx.as_ref().map(|wrap_args| ExtendedWrapperArgs {
2275            wrap_args,
2276            disposable_gas_payer: false,
2277        }),
2278        tx.force,
2279        Some(proposal.proposal.author.clone()),
2280        tx.signing_keys.to_owned(),
2281        vec![],
2282    )
2283    .await?;
2284
2285    let init_proposal_data = InitProposalData::try_from(proposal.clone())
2286        .map_err(|e| TxSubmitError::InvalidProposal(e.to_string()))?;
2287
2288    let push_data =
2289        |tx_builder: &mut Tx, init_proposal_data: &mut InitProposalData| {
2290            let (_, extra_section_hash) = tx_builder
2291                .add_extra_section(proposal_to_vec(proposal.proposal)?, None);
2292            init_proposal_data.content = extra_section_hash;
2293
2294            if matches!(
2295                init_proposal_data.r#type,
2296                ProposalType::DefaultWithWasm(_)
2297            ) {
2298                if let Some(init_proposal_code) = proposal.data {
2299                    let (_, extra_section_hash) =
2300                        tx_builder.add_extra_section(init_proposal_code, None);
2301                    init_proposal_data.r#type =
2302                        ProposalType::DefaultWithWasm(extra_section_hash);
2303                };
2304            }
2305            Ok(())
2306        };
2307
2308    build(
2309        context,
2310        tx,
2311        tx_code_path.clone(),
2312        init_proposal_data,
2313        push_data,
2314        wrap_args,
2315    )
2316    .await
2317    .map(|tx| (tx, signing_data))
2318}
2319
2320/// Build a proposal vote
2321pub async fn build_vote_proposal(
2322    context: &impl Namada,
2323    args::VoteProposal {
2324        tx,
2325        proposal_id,
2326        vote,
2327        voter_address,
2328        tx_code_path,
2329    }: &args::VoteProposal,
2330    current_epoch: Epoch,
2331) -> Result<(Tx, SigningData)> {
2332    let (signing_data, wrap_args, _) = derive_build_data(
2333        context,
2334        tx.wrap_tx.as_ref().map(|wrap_args| ExtendedWrapperArgs {
2335            wrap_args,
2336            disposable_gas_payer: false,
2337        }),
2338        tx.force,
2339        Some(voter_address.clone()),
2340        tx.signing_keys.to_owned(),
2341        vec![],
2342    )
2343    .await?;
2344
2345    let proposal_vote = ProposalVote::try_from(vote.clone())
2346        .map_err(|_| TxSubmitError::InvalidProposalVote)?;
2347
2348    let proposal = if let Some(proposal) =
2349        rpc::query_proposal_by_id(context.client(), *proposal_id).await?
2350    {
2351        proposal
2352    } else {
2353        return Err(Error::from(TxSubmitError::ProposalDoesNotExist(
2354            *proposal_id,
2355        )));
2356    };
2357
2358    let is_validator =
2359        rpc::is_validator(context.client(), voter_address).await?;
2360
2361    // Check if the voting period is still valid for the voter
2362    if !proposal.can_be_voted(current_epoch, is_validator) {
2363        edisplay_line!(
2364            context.io(),
2365            "Proposal {} cannot be voted on, either the voting period ended \
2366             or the proposal is still pending.",
2367            proposal_id
2368        );
2369        if is_validator {
2370            edisplay_line!(
2371                context.io(),
2372                "NB: voter address {} is a validator, and validators can only \
2373                 vote on proposals within the first 2/3 of the voting period. \
2374                 Either the voting period has not started, or the voting \
2375                 period specifically for validators has ended.",
2376                voter_address
2377            );
2378        }
2379        if !tx.force {
2380            return Err(Error::from(
2381                TxSubmitError::InvalidProposalVotingPeriod(*proposal_id),
2382            ));
2383        }
2384    }
2385
2386    if is_validator {
2387        // Prevent a validator voter from voting if they are jailed or inactive
2388        // right now
2389        let state = rpc::get_validator_state(
2390            context.client(),
2391            voter_address,
2392            Some(current_epoch),
2393        )
2394        .await?
2395        .0
2396        .expect("Expected to find the state of the validator");
2397
2398        if matches!(state, ValidatorState::Jailed | ValidatorState::Inactive) {
2399            edisplay_line!(
2400                context.io(),
2401                "The voter {} is a validator who is currently jailed or \
2402                 inactive. Thus, this address is prohibited from voting in \
2403                 governance right now. Please try again when not jailed or \
2404                 inactive.",
2405                voter_address
2406            );
2407            if !tx.force {
2408                return Err(Error::from(
2409                    TxSubmitError::CannotVoteInGovernance(
2410                        voter_address.clone(),
2411                        current_epoch,
2412                    ),
2413                ));
2414            }
2415        }
2416
2417        let stake =
2418            get_validator_stake(context.client(), current_epoch, voter_address)
2419                .await?;
2420
2421        if stake.is_zero() {
2422            edisplay_line!(
2423                context.io(),
2424                "Voter address {voter_address} is a validator but has no \
2425                 stake, so it has no votes.",
2426            );
2427            if !tx.force {
2428                return Err(Error::Other(
2429                    "Voter address must have delegations".to_string(),
2430                ));
2431            }
2432        }
2433    } else {
2434        // Check that there are delegations to vote with
2435        let delegation_validators = rpc::get_delegation_validators(
2436            context.client(),
2437            voter_address,
2438            current_epoch,
2439        )
2440        .await?;
2441
2442        if delegation_validators.is_empty() {
2443            edisplay_line!(
2444                context.io(),
2445                "Voter address {voter_address} does not have any delegations.",
2446            );
2447            if !tx.force {
2448                return Err(Error::from(TxSubmitError::NoDelegationsFound(
2449                    voter_address.clone(),
2450                    current_epoch,
2451                )));
2452            }
2453        }
2454    };
2455
2456    let data = VoteProposalData {
2457        id: *proposal_id,
2458        vote: proposal_vote,
2459        voter: voter_address.clone(),
2460    };
2461
2462    build(
2463        context,
2464        tx,
2465        tx_code_path.clone(),
2466        data,
2467        do_nothing,
2468        wrap_args,
2469    )
2470    .await
2471    .map(|tx| (tx, signing_data))
2472}
2473
2474/// Build a pgf funding proposal governance
2475pub async fn build_become_validator(
2476    context: &impl Namada,
2477    args::TxBecomeValidator {
2478        tx: tx_args,
2479        address,
2480        scheme: _,
2481        consensus_key,
2482        eth_cold_key,
2483        eth_hot_key,
2484        protocol_key,
2485        commission_rate,
2486        max_commission_rate_change,
2487        email,
2488        website,
2489        description,
2490        discord_handle,
2491        avatar,
2492        name,
2493        unsafe_dont_encrypt: _,
2494        tx_code_path,
2495    }: &args::TxBecomeValidator,
2496) -> Result<(Tx, SigningData)> {
2497    // Check that the address is established
2498    if !address.is_established() {
2499        edisplay_line!(
2500            context.io(),
2501            "The given address {address} is not established. Only an \
2502             established address can become a validator.",
2503        );
2504        if !tx_args.force {
2505            return Err(Error::Other(
2506                "The given address must be established".to_string(),
2507            ));
2508        }
2509    };
2510
2511    // Check that the address is not already a validator
2512    if rpc::is_validator(context.client(), address).await? {
2513        edisplay_line!(
2514            context.io(),
2515            "The given address {address} is already a validator",
2516        );
2517        if !tx_args.force {
2518            return Err(Error::Other(
2519                "The given address must not be a validator already".to_string(),
2520            ));
2521        }
2522    };
2523
2524    // If the address is not yet a validator, it cannot have self-bonds, but it
2525    // may have delegations. It has to unbond those before it can become a
2526    // validator.
2527    if rpc::has_bonds(context.client(), address).await? {
2528        edisplay_line!(
2529            context.io(),
2530            "The given address {address} has delegations and therefore cannot \
2531             become a validator. To become a validator, you have to unbond \
2532             your delegations first.",
2533        );
2534        if !tx_args.force {
2535            return Err(Error::Other(
2536                "The given address must not have delegations".to_string(),
2537            ));
2538        }
2539    }
2540
2541    // Validate the commission rate data
2542    if *commission_rate > Dec::one() || *commission_rate < Dec::zero() {
2543        edisplay_line!(
2544            context.io(),
2545            "The validator commission rate must not exceed 1.0 or 100%, and \
2546             it must be 0 or positive."
2547        );
2548        if !tx_args.force {
2549            return Err(Error::Other(
2550                "Invalid validator commission rate".to_string(),
2551            ));
2552        }
2553    }
2554
2555    if *max_commission_rate_change > Dec::one()
2556        || *max_commission_rate_change < Dec::zero()
2557    {
2558        edisplay_line!(
2559            context.io(),
2560            "The validator maximum change in commission rate per epoch must \
2561             not exceed 1.0 or 100%, and it must be 0 or positive."
2562        );
2563        if !tx_args.force {
2564            return Err(Error::Other(
2565                "Invalid validator maximum change".to_string(),
2566            ));
2567        }
2568    }
2569
2570    // Validate the email
2571    if email.is_empty() {
2572        edisplay_line!(
2573            context.io(),
2574            "The validator email must not be an empty string."
2575        );
2576        if !tx_args.force {
2577            return Err(Error::Other(
2578                "Validator email must not be empty".to_string(),
2579            ));
2580        }
2581    }
2582
2583    // check that all keys have been supplied correctly
2584    if [
2585        consensus_key.clone(),
2586        eth_cold_key.clone(),
2587        eth_hot_key.clone(),
2588        protocol_key.clone(),
2589    ]
2590    .iter()
2591    .any(|key| key.is_none())
2592    {
2593        edisplay_line!(
2594            context.io(),
2595            "All validator keys must be supplied to create a validator."
2596        );
2597        return Err(Error::Other("Validator key must be present".to_string()));
2598    }
2599
2600    let data = BecomeValidator {
2601        address: address.clone(),
2602        consensus_key: consensus_key.clone().unwrap(),
2603        eth_cold_key: key::secp256k1::PublicKey::try_from_pk(
2604            &eth_cold_key.clone().unwrap(),
2605        )
2606        .unwrap(),
2607        eth_hot_key: key::secp256k1::PublicKey::try_from_pk(
2608            &eth_hot_key.clone().unwrap(),
2609        )
2610        .unwrap(),
2611        protocol_key: protocol_key.clone().unwrap(),
2612        commission_rate: *commission_rate,
2613        max_commission_rate_change: *max_commission_rate_change,
2614        email: email.to_owned(),
2615        description: description.clone(),
2616        website: website.clone(),
2617        discord_handle: discord_handle.clone(),
2618        avatar: avatar.clone(),
2619        name: name.clone(),
2620    };
2621
2622    // Put together all the PKs that we have to sign with to verify ownership
2623    let account = if let Some(account) =
2624        rpc::get_account_info(context.client(), address).await?
2625    {
2626        account
2627    } else {
2628        edisplay_line!(
2629            context.io(),
2630            "Unable to query account keys for address {address}."
2631        );
2632        return Err(Error::Other("Invalid address".to_string()));
2633    };
2634
2635    let mut all_pks = [
2636        tx_args.signing_keys.to_owned(),
2637        account.get_all_public_keys(),
2638    ]
2639    .concat();
2640    all_pks.push(consensus_key.clone().unwrap().clone());
2641    all_pks.push(eth_cold_key.clone().unwrap());
2642    all_pks.push(eth_hot_key.clone().unwrap());
2643    all_pks.push(protocol_key.clone().unwrap().clone());
2644
2645    let (signing_data, wrap_args) = if let Some(wrap_tx) = &tx_args.wrap_tx {
2646        let signing_data = signing::aux_signing_data(
2647            context,
2648            wrap_tx,
2649            None,
2650            all_pks,
2651            false,
2652            vec![],
2653        )
2654        .await?;
2655
2656        let fee_payer = signing_data.fee_payer_or_err()?.to_owned();
2657        let (fee_amount, _updated_balance) = validate_transparent_fee(
2658            context,
2659            wrap_tx,
2660            tx_args.force,
2661            &fee_payer,
2662        )
2663        .await?;
2664
2665        (
2666            SigningData::Wrapper(signing_data),
2667            Some(WrapArgs {
2668                fee_amount,
2669                fee_payer,
2670                fee_token: wrap_tx.fee_token.to_owned(),
2671                gas_limit: wrap_tx.gas_limit,
2672            }),
2673        )
2674    } else {
2675        let signing_data =
2676            signing::aux_inner_signing_data(context, all_pks, None, vec![])
2677                .await?;
2678
2679        (SigningData::Inner(signing_data), None)
2680    };
2681
2682    build(
2683        context,
2684        tx_args,
2685        tx_code_path.clone(),
2686        data,
2687        do_nothing,
2688        wrap_args,
2689    )
2690    .await
2691    .map(|tx| (tx, signing_data))
2692}
2693
2694/// Build a pgf funding proposal governance
2695pub async fn build_pgf_funding_proposal(
2696    context: &impl Namada,
2697    args::InitProposal {
2698        tx,
2699        proposal_data: _,
2700        is_pgf_stewards: _,
2701        is_pgf_funding: _,
2702        tx_code_path,
2703    }: &args::InitProposal,
2704    proposal: PgfFundingProposal,
2705) -> Result<(Tx, SigningData)> {
2706    let (signing_data, wrap_args, _) = derive_build_data(
2707        context,
2708        tx.wrap_tx.as_ref().map(|wrap_args| ExtendedWrapperArgs {
2709            wrap_args,
2710            disposable_gas_payer: false,
2711        }),
2712        tx.force,
2713        Some(proposal.proposal.author.clone()),
2714        tx.signing_keys.to_owned(),
2715        vec![],
2716    )
2717    .await?;
2718
2719    let init_proposal_data = InitProposalData::try_from(proposal.clone())
2720        .map_err(|e| TxSubmitError::InvalidProposal(e.to_string()))?;
2721
2722    let add_section = |tx: &mut Tx, data: &mut InitProposalData| {
2723        let (_, extra_section_hash) =
2724            tx.add_extra_section(proposal_to_vec(proposal.proposal)?, None);
2725        data.content = extra_section_hash;
2726        Ok(())
2727    };
2728    build(
2729        context,
2730        tx,
2731        tx_code_path.clone(),
2732        init_proposal_data,
2733        add_section,
2734        wrap_args,
2735    )
2736    .await
2737    .map(|tx| (tx, signing_data))
2738}
2739
2740/// Build a pgf funding proposal governance
2741pub async fn build_pgf_stewards_proposal(
2742    context: &impl Namada,
2743    args::InitProposal {
2744        tx,
2745        proposal_data: _,
2746        is_pgf_stewards: _,
2747        is_pgf_funding: _,
2748        tx_code_path,
2749    }: &args::InitProposal,
2750    proposal: PgfStewardProposal,
2751) -> Result<(Tx, SigningData)> {
2752    let (signing_data, wrap_args, _) = derive_build_data(
2753        context,
2754        tx.wrap_tx.as_ref().map(|wrap_args| ExtendedWrapperArgs {
2755            wrap_args,
2756            disposable_gas_payer: false,
2757        }),
2758        tx.force,
2759        Some(proposal.proposal.author.clone()),
2760        tx.signing_keys.to_owned(),
2761        vec![],
2762    )
2763    .await?;
2764
2765    let init_proposal_data = InitProposalData::try_from(proposal.clone())
2766        .map_err(|e| TxSubmitError::InvalidProposal(e.to_string()))?;
2767
2768    let add_section = |tx: &mut Tx, data: &mut InitProposalData| {
2769        let (_, extra_section_hash) =
2770            tx.add_extra_section(proposal_to_vec(proposal.proposal)?, None);
2771        data.content = extra_section_hash;
2772        Ok(())
2773    };
2774
2775    build(
2776        context,
2777        tx,
2778        tx_code_path.clone(),
2779        init_proposal_data,
2780        add_section,
2781        wrap_args,
2782    )
2783    .await
2784    .map(|tx| (tx, signing_data))
2785}
2786
2787/// Submit an IBC transfer
2788pub async fn build_ibc_transfer(
2789    context: &impl Namada,
2790    args: &args::TxIbcTransfer,
2791    bparams: &mut impl BuildParams,
2792) -> Result<(Tx, SigningData, Option<MaspEpoch>)> {
2793    if args.ibc_shielding_data.is_some() && args.ibc_memo.is_some() {
2794        return Err(Error::Other(
2795            "The memo field of the IBC packet can't be used for both \
2796             shielding transfer and another purpose at the same time"
2797                .to_string(),
2798        ));
2799    }
2800
2801    let refund_target =
2802        get_refund_target(context, &args.source, &args.refund_target).await?;
2803
2804    let (mut signing_data, wrap_args, updated_balance) = derive_build_data(
2805        context,
2806        args.tx
2807            .wrap_tx
2808            .as_ref()
2809            .map(|wrap_args| ExtendedWrapperArgs {
2810                wrap_args,
2811                disposable_gas_payer: args.source.spending_key().is_some(),
2812            }),
2813        args.tx.force,
2814        args.source.address(),
2815        args.tx.signing_keys.to_owned(),
2816        vec![],
2817    )
2818    .await?;
2819
2820    // Check that the source address exists on chain
2821    let source = args.source.effective_address();
2822    let source =
2823        source_exists_or_err(source.clone(), args.tx.force, context).await?;
2824    // We cannot check the receiver
2825
2826    // validate the amount given
2827    let validated_amount =
2828        validate_amount(context, args.amount, &args.token, args.tx.force)
2829            .await
2830            .expect("expected to validate amount");
2831
2832    // If source is transparent check the balance (MASP balance is checked when
2833    // constructing the shielded part)
2834    if source != MASP {
2835        let check_balance = match updated_balance {
2836            Some(updated_balance)
2837                if updated_balance.source == source
2838                    && updated_balance.token == args.token =>
2839            {
2840                CheckBalance::Balance(updated_balance.post_balance)
2841            }
2842            _ => CheckBalance::Query(balance_key(&args.token, &source)),
2843        };
2844
2845        check_balance_too_low_err(
2846            &args.token,
2847            &source,
2848            validated_amount.amount(),
2849            check_balance,
2850            args.tx.force,
2851            context,
2852        )
2853        .await?;
2854    }
2855
2856    let tx_code_hash =
2857        query_wasm_code_hash(context, args.tx_code_path.to_str().unwrap())
2858            .await
2859            .map_err(|e| Error::from(QueryError::Wasm(e.to_string())))?;
2860    let mut masp_transfer_data = MaspTransferData {
2861        sources: vec![(
2862            args.source.clone(),
2863            args.token.clone(),
2864            validated_amount,
2865        )],
2866        // The token will be escrowed to IBC address
2867        targets: vec![(
2868            TransferTarget::Ibc(args.receiver.clone()),
2869            args.token.clone(),
2870            validated_amount,
2871        )],
2872    };
2873
2874    let mut transfer = token::Transfer::default();
2875
2876    // Add masp fee payment if necessary
2877    let masp_fee_data = if let Some(wrap_tx) = &wrap_args {
2878        let masp_fee_data = get_masp_fee_payment_amount(
2879            context,
2880            wrap_tx,
2881            // If no custom gas spending key is provided default to the source
2882            args.gas_spending_key.or(args.source.spending_key()),
2883        )
2884        .await?;
2885        if let Some(fee_data) = &masp_fee_data {
2886            transfer = transfer
2887                .transfer(
2888                    MASP,
2889                    fee_data.target.to_owned(),
2890                    fee_data.token.to_owned(),
2891                    fee_data.amount,
2892                )
2893                .ok_or(Error::Other(
2894                    "Combined transfer overflows".to_string(),
2895                ))?;
2896        }
2897
2898        masp_fee_data
2899    } else {
2900        None
2901    };
2902
2903    if let Some((target, percentage)) = &args.frontend_sus_fee {
2904        match (&source, &args.ibc_shielding_data) {
2905            (&MASP, None) => {
2906                // NOTE: The frontend fee should NOT account for the masp fee
2907                // payment amount
2908                let validated_fee_amount = compute_masp_frontend_sus_fee(
2909                    context,
2910                    &validated_amount,
2911                    percentage,
2912                    &args.token,
2913                    args.tx.force,
2914                )
2915                .await?;
2916
2917                masp_transfer_data.sources.push((
2918                    args.source.clone(),
2919                    args.token.clone(),
2920                    validated_fee_amount,
2921                ));
2922                masp_transfer_data.targets.push((
2923                    target.clone(),
2924                    args.token.to_owned(),
2925                    validated_fee_amount,
2926                ));
2927
2928                transfer = transfer
2929                    .transfer(
2930                        source.to_owned(),
2931                        target.effective_address(),
2932                        args.token.to_owned(),
2933                        validated_fee_amount,
2934                    )
2935                    .ok_or(Error::Other(
2936                        "Combined transfer overflows".to_string(),
2937                    ))?;
2938            }
2939            (&MASP, Some(_)) => {
2940                return Err(Error::Other(
2941                    "A frontend sustainability fee was requested but the ibc \
2942                     roundtrip is shielded"
2943                        .to_string(),
2944                ));
2945            }
2946            (_, _) => {
2947                return Err(Error::Other(
2948                    "A frontend sustainability fee was requested but the ibc \
2949                     source is transparent. If the transaction is a roundtrip \
2950                     (e.g. swap), and the return target is shielded, the fee \
2951                     should be taken from the shielding transaction instead."
2952                        .to_string(),
2953                ));
2954            }
2955        }
2956    }
2957
2958    // For transfer from a spending key
2959    let shielded_parts = construct_shielded_parts(
2960        context,
2961        masp_transfer_data,
2962        masp_fee_data,
2963        args.tx.expiration.to_datetime(),
2964        bparams,
2965    )
2966    .await?;
2967    let shielded_tx_epoch = shielded_parts.as_ref().map(|trans| trans.0.epoch);
2968
2969    // this height should be that of the destination chain, not this chain
2970    let timeout_height = match args.timeout_height {
2971        Some(h) => {
2972            TimeoutHeight::At(IbcHeight::new(0, h).map_err(|err| {
2973                Error::Other(format!("Invalid height: {err}"))
2974            })?)
2975        }
2976        None => TimeoutHeight::Never,
2977    };
2978
2979    let now: std::result::Result<
2980        crate::tendermint::Time,
2981        namada_core::tendermint::Error,
2982    > = {
2983        #[allow(clippy::disallowed_methods)]
2984        DateTimeUtc::now()
2985    }
2986    .try_into();
2987    let now = now.map_err(|e| Error::Other(e.to_string()))?;
2988    let now: IbcTimestamp = now.into_timestamp().map_err(|e| {
2989        Error::Other(format!("Timestamp conversion failed: {e}"))
2990    })?;
2991    let timeout_timestamp = if let Some(offset) = args.timeout_sec_offset {
2992        let timestamp = (now + Duration::new(offset, 0))
2993            .map_err(|e| Error::Other(e.to_string()))?;
2994        TimeoutTimestamp::At(timestamp)
2995    } else if timeout_height == TimeoutHeight::Never {
2996        // we cannot set 0 to both the height and the timestamp
2997        let timestamp = (now + Duration::new(3600, 0))
2998            .map_err(|e| Error::Other(e.to_string()))?;
2999        TimeoutTimestamp::At(timestamp)
3000    } else {
3001        TimeoutTimestamp::Never
3002    };
3003
3004    let chain_id = args.tx.chain_id.clone().unwrap();
3005    let mut tx = Tx::new(chain_id, args.tx.expiration.to_datetime());
3006    if let Some(memo) = &args.tx.memo {
3007        tx.add_memo(memo);
3008    }
3009
3010    let transfer = shielded_parts
3011        .map(|(shielded_transfer, asset_types)| {
3012            let masp_tx_hash =
3013                tx.add_masp_tx_section(shielded_transfer.masp_tx.clone()).1;
3014            transfer.shielded_section_hash = Some(masp_tx_hash);
3015            match signing_data {
3016                SigningData::Inner(ref mut signing_tx_data) => {
3017                    signing_tx_data.shielded_hash = Some(masp_tx_hash);
3018                }
3019                SigningData::Wrapper(ref mut signing_wrapper_data) => {
3020                    signing_wrapper_data
3021                        .signing_data
3022                        .first_mut()
3023                        .expect("Missing expected inner IBC transaction")
3024                        .shielded_hash = Some(masp_tx_hash);
3025                }
3026            };
3027            tx.add_masp_builder(MaspBuilder {
3028                asset_types,
3029                metadata: shielded_transfer.metadata,
3030                builder: shielded_transfer.builder,
3031                target: masp_tx_hash,
3032            });
3033            Result::Ok(transfer)
3034        })
3035        .transpose()?;
3036
3037    // Check the token and make the tx data
3038    let ibc_denom =
3039        rpc::query_ibc_denom(context, &args.token.to_string(), Some(&source))
3040            .await;
3041    // The refund target should be given or created if the source is shielded.
3042    // Otherwise, the refund target should be None.
3043    assert!(
3044        (args.source.spending_key().is_some() && refund_target.is_some())
3045            || (args.source.address().is_some() && refund_target.is_none())
3046    );
3047    // The memo is either IbcShieldingData or just a memo
3048    let memo = args
3049        .ibc_shielding_data
3050        .as_ref()
3051        .map_or(args.ibc_memo.clone(), |shielding_data| {
3052            Some(shielding_data.clone().into())
3053        });
3054    // If the refund address is given, set the refund address. It is used only
3055    // when refunding and won't affect the actual transfer because the actual
3056    // source will be the MASP address and the MASP transaction is generated by
3057    // the shielded source address.
3058    let sender = refund_target
3059        .map(|t| t.to_string())
3060        .unwrap_or(source.to_string())
3061        .into();
3062    let data = if args.port_id == PortId::transfer() {
3063        let token = PrefixedCoin {
3064            denom: ibc_denom
3065                .parse()
3066                .map_err(|e| Error::Other(format!("Invalid IBC denom: {e}")))?,
3067            // Set the IBC amount as an integer
3068            amount: validated_amount.into(),
3069        };
3070        let packet_data = PacketData {
3071            token,
3072            sender,
3073            receiver: args.receiver.clone().into(),
3074            memo: memo.unwrap_or_default().into(),
3075        };
3076        let message = IbcMsgTransfer {
3077            port_id_on_a: args.port_id.clone(),
3078            chan_id_on_a: args.channel_id.clone(),
3079            packet_data,
3080            timeout_height_on_b: timeout_height,
3081            timeout_timestamp_on_b: timeout_timestamp,
3082        };
3083        MsgTransfer { message, transfer }.serialize_to_vec()
3084    } else if let Some((trace_path, base_class_id, token_id)) =
3085        is_nft_trace(&ibc_denom)
3086    {
3087        let class_id = PrefixedClassId {
3088            trace_path,
3089            base_class_id: base_class_id.parse().map_err(|_| {
3090                Error::Other(format!("Invalid class ID: {base_class_id}"))
3091            })?,
3092        };
3093        let token_ids = vec![token_id.clone()].try_into().map_err(|_| {
3094            Error::Other(format!("Invalid token ID: {token_id}"))
3095        })?;
3096        let packet_data = NftPacketData {
3097            class_id,
3098            class_uri: None,
3099            class_data: None,
3100            token_ids,
3101            token_uris: None,
3102            token_data: None,
3103            sender,
3104            receiver: args.receiver.clone().into(),
3105            memo: memo.map(|s| s.into()),
3106        };
3107        let message = IbcMsgNftTransfer {
3108            port_id_on_a: args.port_id.clone(),
3109            chan_id_on_a: args.channel_id.clone(),
3110            packet_data,
3111            timeout_height_on_b: timeout_height,
3112            timeout_timestamp_on_b: timeout_timestamp,
3113        };
3114        MsgNftTransfer { message, transfer }.serialize_to_vec()
3115    } else {
3116        return Err(Error::Other(format!("Invalid IBC denom: {ibc_denom}")));
3117    };
3118
3119    tx.add_code_from_hash(
3120        tx_code_hash,
3121        Some(args.tx_code_path.to_string_lossy().into_owned()),
3122    )
3123    .add_serialized_data(data);
3124    if let Some(WrapArgs {
3125        fee_amount,
3126        fee_payer,
3127        fee_token,
3128        gas_limit,
3129    }) = wrap_args
3130    {
3131        tx.add_wrapper(
3132            Fee {
3133                amount_per_gas_unit: fee_amount,
3134                token: fee_token,
3135            },
3136            fee_payer,
3137            gas_limit,
3138        );
3139    }
3140
3141    Ok((tx, signing_data, shielded_tx_epoch))
3142}
3143
3144pub(crate) struct WrapArgs {
3145    pub(crate) fee_amount: DenominatedAmount,
3146    pub(crate) fee_payer: common::PublicKey,
3147    pub(crate) fee_token: Address,
3148    pub(crate) gas_limit: GasLimit,
3149}
3150
3151/// Abstraction for helping build transactions. This function will build either
3152/// a Raw or a Wrapper transaction depending on the presence of the [`WrapArgs`]
3153/// argument.
3154#[allow(clippy::too_many_arguments)]
3155async fn build<F, D>(
3156    context: &impl Namada,
3157    tx_args: &crate::args::Tx,
3158    path: PathBuf,
3159    mut data: D,
3160    on_tx: F,
3161    wrap_args: Option<WrapArgs>,
3162) -> Result<Tx>
3163where
3164    F: FnOnce(&mut Tx, &mut D) -> Result<()>,
3165    D: BorshSerialize,
3166{
3167    let chain_id = tx_args.chain_id.clone().unwrap();
3168
3169    let mut tx = Tx::new(chain_id, tx_args.expiration.to_datetime());
3170    if let Some(memo) = &tx_args.memo {
3171        tx.add_memo(memo);
3172    }
3173
3174    let tx_code_hash = query_wasm_code_hash(context, path.to_string_lossy())
3175        .await
3176        .map_err(|e| Error::from(QueryError::Wasm(e.to_string())))?;
3177
3178    on_tx(&mut tx, &mut data)?;
3179
3180    tx.add_code_from_hash(
3181        tx_code_hash,
3182        Some(path.to_string_lossy().into_owned()),
3183    )
3184    .add_data(data);
3185
3186    // Wrap the transaction if requested
3187    if let Some(WrapArgs {
3188        fee_amount,
3189        fee_payer,
3190        fee_token,
3191        gas_limit,
3192    }) = wrap_args
3193    {
3194        tx.add_wrapper(
3195            Fee {
3196                amount_per_gas_unit: fee_amount,
3197                token: fee_token,
3198            },
3199            fee_payer,
3200            gas_limit,
3201        );
3202    }
3203
3204    Ok(tx)
3205}
3206
3207/// Try to decode the given asset type and add its decoding to the supplied set.
3208/// Returns true only if a new decoding has been added to the given set.
3209async fn add_asset_type(
3210    asset_types: &mut HashSet<AssetData>,
3211    context: &impl Namada,
3212    asset_type: AssetType,
3213) -> bool {
3214    if let Some(asset_type) = context
3215        .shielded_mut()
3216        .await
3217        .decode_asset_type(context.client(), asset_type)
3218        .await
3219    {
3220        asset_types.insert(asset_type)
3221    } else {
3222        false
3223    }
3224}
3225
3226/// Collect the asset types used in the given Builder and decode them. This
3227/// function provides the data necessary for offline wallets to present asset
3228/// type information.
3229async fn used_asset_types<P, K, N>(
3230    context: &impl Namada,
3231    builder: &Builder<P, K, N>,
3232) -> std::result::Result<HashSet<AssetData>, RpcError> {
3233    let mut asset_types = HashSet::new();
3234    // Collect all the asset types used in the Sapling inputs
3235    for input in builder.sapling_inputs() {
3236        add_asset_type(&mut asset_types, context, input.asset_type()).await;
3237    }
3238    // Collect all the asset types used in the transparent inputs
3239    for input in builder.transparent_inputs() {
3240        add_asset_type(&mut asset_types, context, input.coin().asset_type())
3241            .await;
3242    }
3243    // Collect all the asset types used in the Sapling outputs
3244    for output in builder.sapling_outputs() {
3245        add_asset_type(&mut asset_types, context, output.asset_type()).await;
3246    }
3247    // Collect all the asset types used in the transparent outputs
3248    for output in builder.transparent_outputs() {
3249        add_asset_type(&mut asset_types, context, output.asset_type()).await;
3250    }
3251    // Collect all the asset types used in the Sapling converts
3252    for output in builder.sapling_converts() {
3253        for (asset_type, _) in
3254            I128Sum::from(output.conversion().clone()).components()
3255        {
3256            add_asset_type(&mut asset_types, context, *asset_type).await;
3257        }
3258    }
3259    Ok(asset_types)
3260}
3261
3262/// Constructs the batched tx from the provided list. Returns the batch and the
3263/// data for signing.
3264///
3265/// # Arguments
3266///
3267/// * `txs` - The list of transactions for the batch. The first transaction in
3268///   the list can be either a wrapper transaction or a raw one. In the former
3269///   case, its `Header` will be used as the whole batche's header. If MASP fee
3270///   payment is required that should be included in this first transaction. The
3271///   remaining transactions are supposed to be raw transactions. They can also
3272///   be wrapped but in this case their wrapper data will be silently discarded
3273pub fn build_batch(
3274    mut txs: Vec<(Tx, SigningData)>,
3275) -> Result<(Tx, either::Either<SigningWrapperData, Vec<SigningTxData>>)> {
3276    if txs.is_empty() {
3277        return Err(Error::Other(
3278            "No transactions provided for the batch".to_string(),
3279        ));
3280    }
3281    let (mut batched_tx, signing_data) = txs.remove(0);
3282    let mut signing_batch_data = match signing_data {
3283        SigningData::Wrapper(signing_wrapper_data) => {
3284            either::Left(signing_wrapper_data)
3285        }
3286        SigningData::Inner(signing_tx_data) => {
3287            either::Right(vec![signing_tx_data])
3288        }
3289    };
3290
3291    for (tx, sig_data) in txs {
3292        batched_tx = Tx::merge_transactions(batched_tx, tx).map_err(|_| {
3293            Error::Other(
3294                "Found duplicated tx commitments when building the batch"
3295                    .to_string(),
3296            )
3297        })?;
3298        // Avoid redundant signing data
3299        for signing_tx_data in sig_data.signing_tx_data() {
3300            match &mut signing_batch_data {
3301                either::Either::Left(wrapper_data) => {
3302                    if !wrapper_data.signing_data.contains(signing_tx_data) {
3303                        wrapper_data
3304                            .signing_data
3305                            .push(signing_tx_data.to_owned());
3306                    }
3307                }
3308                either::Either::Right(sig_data) => {
3309                    if !sig_data.contains(signing_tx_data) {
3310                        sig_data.push(signing_tx_data.to_owned());
3311                    }
3312                }
3313            }
3314        }
3315    }
3316
3317    Ok((batched_tx, signing_batch_data))
3318}
3319
3320/// Build a transparent transfer
3321pub async fn build_transparent_transfer<N: Namada>(
3322    context: &N,
3323    args: &mut args::TxTransparentTransfer,
3324) -> Result<(Tx, SigningData)> {
3325    let mut transfers = token::Transfer::default();
3326
3327    // Evaluate signer and fees
3328    let source = if args.sources.len() == 1 {
3329        // If only one transfer take its source as the signer
3330        args.sources
3331            .first()
3332            .map(|transfer_data| transfer_data.source.clone())
3333    } else {
3334        // Otherwise the caller is required to pass the public keys in the
3335        // argument
3336        None
3337    };
3338    let (signing_data, wrap_args, updated_balance) = derive_build_data(
3339        context,
3340        args.tx
3341            .wrap_tx
3342            .as_ref()
3343            .map(|wrap_args| ExtendedWrapperArgs {
3344                wrap_args,
3345                disposable_gas_payer: false,
3346            }),
3347        args.tx.force,
3348        source,
3349        args.tx.signing_keys.to_owned(),
3350        vec![],
3351    )
3352    .await?;
3353
3354    for TxTransparentSource {
3355        source,
3356        token,
3357        amount,
3358    } in &args.sources
3359    {
3360        // Check that the source address exists on chain
3361        source_exists_or_err(source.clone(), args.tx.force, context).await?;
3362
3363        // Validate the amount given
3364        let validated_amount =
3365            validate_amount(context, amount.to_owned(), token, args.tx.force)
3366                .await?;
3367
3368        // Check the balance of the source
3369        if let Some(updated_balance) = &updated_balance {
3370            let check_balance = if &updated_balance.source == source
3371                && &updated_balance.token == token
3372            {
3373                CheckBalance::Balance(updated_balance.post_balance)
3374            } else {
3375                CheckBalance::Query(balance_key(token, source))
3376            };
3377
3378            check_balance_too_low_err(
3379                token,
3380                source,
3381                validated_amount.amount(),
3382                check_balance,
3383                args.tx.force,
3384                context,
3385            )
3386            .await?;
3387        }
3388
3389        // Construct the corresponding transparent Transfer object
3390        transfers = transfers
3391            .debit(source.to_owned(), token.to_owned(), validated_amount)
3392            .ok_or(Error::Other("Combined transfer overflows".to_string()))?;
3393    }
3394
3395    for TxTransparentTarget {
3396        target,
3397        token,
3398        amount,
3399    } in &args.targets
3400    {
3401        // Check that the target address exists on chain
3402        target_exists_or_err(target.clone(), args.tx.force, context).await?;
3403
3404        // Validate the amount given
3405        let validated_amount =
3406            validate_amount(context, amount.to_owned(), token, args.tx.force)
3407                .await?;
3408
3409        // Construct the corresponding transparent Transfer object
3410        transfers = transfers
3411            .credit(target.to_owned(), token.to_owned(), validated_amount)
3412            .ok_or(Error::Other("Combined transfer overflows".to_string()))?;
3413    }
3414
3415    let tx = build(
3416        context,
3417        &args.tx,
3418        args.tx_code_path.clone(),
3419        transfers,
3420        do_nothing,
3421        wrap_args,
3422    )
3423    .await?;
3424
3425    Ok((tx, signing_data))
3426}
3427
3428/// Build a shielded transfer
3429pub async fn build_shielded_transfer<N: Namada>(
3430    context: &N,
3431    args: &mut args::TxShieldedTransfer,
3432    bparams: &mut impl BuildParams,
3433) -> Result<(Tx, SigningData)> {
3434    let (mut signing_data, wrap_args, _) = derive_build_data(
3435        context,
3436        args.tx
3437            .wrap_tx
3438            .as_ref()
3439            .map(|wrap_args| ExtendedWrapperArgs {
3440                wrap_args,
3441                disposable_gas_payer: true,
3442            }),
3443        args.tx.force,
3444        None,
3445        args.tx.signing_keys.to_owned(),
3446        vec![],
3447    )
3448    .await?;
3449
3450    let mut transfer_data = MaspTransferData::default();
3451    for args::TxShieldedSource {
3452        source,
3453        token,
3454        amount,
3455    } in &args.sources
3456    {
3457        // Validate the amount given
3458        let validated_amount =
3459            validate_amount(context, amount.to_owned(), token, args.tx.force)
3460                .await?;
3461
3462        transfer_data.sources.push((
3463            TransferSource::ExtendedKey(source.to_owned()),
3464            token.to_owned(),
3465            validated_amount,
3466        ));
3467    }
3468    for args::TxShieldedTarget {
3469        target,
3470        token,
3471        amount,
3472    } in &args.targets
3473    {
3474        // Validate the amount given
3475        let validated_amount =
3476            validate_amount(context, amount.to_owned(), token, args.tx.force)
3477                .await?;
3478
3479        transfer_data.targets.push((
3480            TransferTarget::PaymentAddress(target.to_owned()),
3481            token.to_owned(),
3482            validated_amount,
3483        ));
3484    }
3485
3486    // Construct the tx data with a placeholder shielded section hash
3487    let mut data = token::Transfer::default();
3488
3489    // Add masp fee payment if necessary
3490    let masp_fee_data = if let Some(wrap_tx) = &wrap_args {
3491        let masp_fee_data = get_masp_fee_payment_amount(
3492            context,
3493            wrap_tx,
3494            // If no custom gas spending key is provided default to the first
3495            // source
3496            args.gas_spending_key
3497                .or(args.sources.first().map(|data| data.source)),
3498        )
3499        .await?;
3500        if let Some(fee_data) = &masp_fee_data {
3501            data = data
3502                .transfer(
3503                    MASP,
3504                    fee_data.target.to_owned(),
3505                    fee_data.token.to_owned(),
3506                    fee_data.amount,
3507                )
3508                .ok_or(Error::Other(
3509                    "Combined transfer overflows".to_string(),
3510                ))?;
3511        }
3512
3513        masp_fee_data
3514    } else {
3515        None
3516    };
3517
3518    let shielded_parts = construct_shielded_parts(
3519        context,
3520        transfer_data,
3521        masp_fee_data,
3522        args.tx.expiration.to_datetime(),
3523        bparams,
3524    )
3525    .await?
3526    .expect("Shielded transfer must have shielded parts");
3527
3528    let add_shielded_parts = |tx: &mut Tx, data: &mut token::Transfer| {
3529        // Add the MASP Transaction and its Builder to facilitate validation
3530        let (
3531            ShieldedTransfer {
3532                builder,
3533                masp_tx,
3534                metadata,
3535                epoch: _,
3536            },
3537            asset_types,
3538        ) = shielded_parts;
3539        // Add a MASP Transaction section to the Tx and get the tx hash
3540        let section_hash = tx.add_masp_tx_section(masp_tx).1;
3541
3542        tx.add_masp_builder(MaspBuilder {
3543            asset_types,
3544            // Store how the Info objects map to Descriptors/Outputs
3545            metadata,
3546            // Store the data that was used to construct the Transaction
3547            builder,
3548            // Link the Builder to the Transaction by hash code
3549            target: section_hash,
3550        });
3551
3552        data.shielded_section_hash = Some(section_hash);
3553        match signing_data {
3554            SigningData::Inner(ref mut signing_tx_data) => {
3555                signing_tx_data.shielded_hash = Some(section_hash);
3556            }
3557            SigningData::Wrapper(ref mut signing_wrapper_data) => {
3558                signing_wrapper_data
3559                    .signing_data
3560                    .first_mut()
3561                    .expect("Missing expected inner shielded transaction")
3562                    .shielded_hash = Some(section_hash);
3563            }
3564        };
3565        tracing::debug!("Transfer data {data:?}");
3566        Ok(())
3567    };
3568
3569    let tx = build(
3570        context,
3571        &args.tx,
3572        args.tx_code_path.clone(),
3573        data,
3574        add_shielded_parts,
3575        wrap_args,
3576    )
3577    .await?;
3578    Ok((tx, signing_data))
3579}
3580
3581// Check if the transaction will need to pay fees via the masp and extract the
3582// right masp data
3583async fn get_masp_fee_payment_amount<N: Namada>(
3584    context: &N,
3585    WrapArgs {
3586        fee_amount,
3587        fee_payer,
3588        fee_token,
3589        gas_limit,
3590    }: &WrapArgs,
3591    gas_spending_key: Option<PseudoExtendedKey>,
3592) -> Result<Option<MaspFeeData>> {
3593    let fee_payer_address = Address::from(fee_payer);
3594    let balance_key = balance_key(fee_token, &fee_payer_address);
3595    #[allow(clippy::disallowed_methods)]
3596    let balance = rpc::query_storage_value::<_, token::Amount>(
3597        context.client(),
3598        &balance_key,
3599    )
3600    .await
3601    .unwrap_or_default();
3602    let total_fee = checked!(fee_amount.amount() * u64::from(*gas_limit))?;
3603
3604    Ok(match total_fee.checked_sub(balance) {
3605        Some(diff) if !diff.is_zero() => Some(MaspFeeData {
3606            source: gas_spending_key.ok_or(Error::Other(
3607                "MASP fee payment is required for this transaction but a \
3608                 spending key was not provided"
3609                    .to_string(),
3610            ))?,
3611            target: fee_payer_address,
3612            token: fee_token.to_owned(),
3613            amount: DenominatedAmount::new(diff, fee_amount.denom()),
3614        }),
3615        _ => None,
3616    })
3617}
3618
3619// Extract the validate amount for the masp frontend sustainability fee
3620async fn compute_masp_frontend_sus_fee(
3621    context: &impl Namada,
3622    input_amount: &namada_token::DenominatedAmount,
3623    percentage: &namada_core::dec::Dec,
3624    token: &Address,
3625    force: bool,
3626) -> Result<namada_token::DenominatedAmount> {
3627    let sus_fee_amt = namada_token::Amount::from_uint(
3628        input_amount
3629            .amount()
3630            .raw_amount()
3631            .checked_mul_div(
3632                percentage.abs(),
3633                namada_core::uint::Uint::exp10(POS_DECIMAL_PRECISION as _),
3634            )
3635            .ok_or_else(|| {
3636                Error::Other(
3637                    "Overflow in masp frontend fee computation".to_string(),
3638                )
3639            })?
3640            .0,
3641        0,
3642    )
3643    .map_err(|e| Error::Other(e.to_string()))?;
3644
3645    // Validate the amount given
3646    validate_amount(
3647        context,
3648        args::InputAmount::Unvalidated(DenominatedAmount::new(
3649            sus_fee_amt,
3650            input_amount.denom(),
3651        )),
3652        token,
3653        force,
3654    )
3655    .await
3656}
3657
3658/// Build a shielding transfer
3659pub async fn build_shielding_transfer<N: Namada>(
3660    context: &N,
3661    args: &args::TxShieldingTransfer,
3662    bparams: &mut impl BuildParams,
3663) -> Result<(Tx, SigningData, MaspEpoch)> {
3664    let source = if args.sources.len() == 1 {
3665        // If only one transfer take its source as the signer
3666        args.sources
3667            .first()
3668            .map(|transfer_data| transfer_data.source.clone())
3669    } else {
3670        // Otherwise the caller is required to pass the public keys in the
3671        // argument
3672        None
3673    };
3674    let (mut signing_data, wrap_args, updated_balance) = derive_build_data(
3675        context,
3676        args.tx
3677            .wrap_tx
3678            .as_ref()
3679            .map(|wrap_args| ExtendedWrapperArgs {
3680                wrap_args,
3681                disposable_gas_payer: false,
3682            }),
3683        args.tx.force,
3684        source,
3685        args.tx.signing_keys.to_owned(),
3686        vec![],
3687    )
3688    .await?;
3689
3690    let mut transfer_data = MaspTransferData::default();
3691    let mut data = token::Transfer::default();
3692    for TxTransparentSource {
3693        source,
3694        token,
3695        amount,
3696    } in &args.sources
3697    {
3698        // Validate the amount given
3699        let validated_amount =
3700            validate_amount(context, amount.to_owned(), token, args.tx.force)
3701                .await?;
3702
3703        // Compute the frontend fee (if required), take the fee percentage from
3704        // every source
3705        let validated_frontend_fee_amt = if let Some((
3706            sus_fee_target,
3707            percentage,
3708        )) = &args.frontend_sus_fee
3709        {
3710            let validated_fee_amount = compute_masp_frontend_sus_fee(
3711                context,
3712                &validated_amount,
3713                percentage,
3714                token,
3715                args.tx.force,
3716            )
3717            .await?;
3718            Some((sus_fee_target, validated_fee_amount))
3719        } else {
3720            None
3721        };
3722        let total_input_amt = checked!(
3723            validated_amount
3724                + validated_frontend_fee_amt
3725                    .map(|(_, amt)| amt)
3726                    .unwrap_or_else(|| DenominatedAmount::new(
3727                        namada_token::Amount::zero(),
3728                        validated_amount.denom()
3729                    ))
3730        )?;
3731
3732        // Check the balance of the source
3733        if let Some(updated_balance) = &updated_balance {
3734            let check_balance = if &updated_balance.source == source
3735                && &updated_balance.token == token
3736            {
3737                CheckBalance::Balance(updated_balance.post_balance)
3738            } else {
3739                CheckBalance::Query(balance_key(token, source))
3740            };
3741
3742            check_balance_too_low_err(
3743                token,
3744                source,
3745                total_input_amt.amount(),
3746                check_balance,
3747                args.tx.force,
3748                context,
3749            )
3750            .await?;
3751        }
3752
3753        transfer_data.sources.push((
3754            TransferSource::Address(source.to_owned()),
3755            token.to_owned(),
3756            validated_amount,
3757        ));
3758
3759        data = data
3760            .debit(source.to_owned(), token.to_owned(), total_input_amt)
3761            .ok_or(Error::Other("Combined transfer overflows".to_string()))?;
3762
3763        if let Some((sus_fee_target, validated_fee_amount)) =
3764            validated_frontend_fee_amt
3765        {
3766            if sus_fee_target.payment_address().is_some() {
3767                // Add the extra shielding source and target
3768                transfer_data.sources.push((
3769                    TransferSource::Address(source.to_owned()),
3770                    token.to_owned(),
3771                    validated_fee_amount,
3772                ));
3773                transfer_data.targets.push((
3774                    sus_fee_target.to_owned(),
3775                    token.to_owned(),
3776                    validated_fee_amount,
3777                ));
3778            }
3779            data = data
3780                .credit(
3781                    sus_fee_target.effective_address(),
3782                    token.to_owned(),
3783                    validated_fee_amount,
3784                )
3785                .ok_or(Error::Other(
3786                    "Combined transfer overflows".to_string(),
3787                ))?;
3788        }
3789    }
3790
3791    for args::TxShieldedTarget {
3792        target,
3793        token,
3794        amount,
3795    } in &args.targets
3796    {
3797        // Validate the amount given
3798        let validated_amount =
3799            validate_amount(context, amount.to_owned(), token, args.tx.force)
3800                .await?;
3801        transfer_data.targets.push((
3802            TransferTarget::PaymentAddress(target.to_owned()),
3803            token.to_owned(),
3804            validated_amount,
3805        ));
3806
3807        data = data
3808            .credit(MASP, token.to_owned(), validated_amount)
3809            .ok_or(Error::Other("Combined transfer overflows".to_string()))?;
3810    }
3811
3812    let shielded_parts = construct_shielded_parts(
3813        context,
3814        transfer_data,
3815        None,
3816        args.tx.expiration.to_datetime(),
3817        bparams,
3818    )
3819    .await?
3820    .expect("Shielding transfer must have shielded parts");
3821    let shielded_tx_epoch = shielded_parts.0.epoch;
3822
3823    let add_shielded_parts = |tx: &mut Tx, data: &mut token::Transfer| {
3824        // Add the MASP Transaction and its Builder to facilitate validation
3825        let (
3826            ShieldedTransfer {
3827                builder,
3828                masp_tx,
3829                metadata,
3830                epoch: _,
3831            },
3832            asset_types,
3833        ) = shielded_parts;
3834        // Add a MASP Transaction section to the Tx and get the tx hash
3835        let shielded_section_hash = tx.add_masp_tx_section(masp_tx).1;
3836
3837        tx.add_masp_builder(MaspBuilder {
3838            asset_types,
3839            // Store how the Info objects map to Descriptors/Outputs
3840            metadata,
3841            // Store the data that was used to construct the Transaction
3842            builder,
3843            // Link the Builder to the Transaction by hash code
3844            target: shielded_section_hash,
3845        });
3846
3847        data.shielded_section_hash = Some(shielded_section_hash);
3848        match signing_data {
3849            SigningData::Inner(ref mut signing_tx_data) => {
3850                signing_tx_data.shielded_hash = Some(shielded_section_hash);
3851            }
3852            SigningData::Wrapper(ref mut signing_wrapper_data) => {
3853                signing_wrapper_data
3854                    .signing_data
3855                    .first_mut()
3856                    .expect("Missing expected inner shielding transaction")
3857                    .shielded_hash = Some(shielded_section_hash);
3858            }
3859        };
3860        tracing::debug!("Transfer data {data:?}");
3861        Ok(())
3862    };
3863
3864    let tx = build(
3865        context,
3866        &args.tx,
3867        args.tx_code_path.clone(),
3868        data,
3869        add_shielded_parts,
3870        wrap_args,
3871    )
3872    .await?;
3873    Ok((tx, signing_data, shielded_tx_epoch))
3874}
3875
3876/// Build an unshielding transfer
3877pub async fn build_unshielding_transfer<N: Namada>(
3878    context: &N,
3879    args: &mut args::TxUnshieldingTransfer,
3880    bparams: &mut impl BuildParams,
3881) -> Result<(Tx, SigningData)> {
3882    let (mut signing_data, wrap_args, _) = derive_build_data(
3883        context,
3884        args.tx
3885            .wrap_tx
3886            .as_ref()
3887            .map(|wrap_args| ExtendedWrapperArgs {
3888                wrap_args,
3889                disposable_gas_payer: true,
3890            }),
3891        args.tx.force,
3892        None,
3893        args.tx.signing_keys.to_owned(),
3894        vec![],
3895    )
3896    .await?;
3897
3898    let mut transfer_data = MaspTransferData::default();
3899    let mut data = token::Transfer::default();
3900    for TxTransparentTarget {
3901        target,
3902        token,
3903        amount,
3904    } in &args.targets
3905    {
3906        // Validate the amount given
3907        let validated_amount =
3908            validate_amount(context, amount.to_owned(), token, args.tx.force)
3909                .await?;
3910
3911        transfer_data.targets.push((
3912            TransferTarget::Address(target.to_owned()),
3913            token.to_owned(),
3914            validated_amount,
3915        ));
3916
3917        data = data
3918            .credit(target.to_owned(), token.to_owned(), validated_amount)
3919            .ok_or(Error::Other("Combined transfer overflows".to_string()))?;
3920    }
3921    for args::TxShieldedSource {
3922        source,
3923        token,
3924        amount,
3925    } in &args.sources
3926    {
3927        // Validate the amount given
3928        let validated_amount =
3929            validate_amount(context, amount.to_owned(), token, args.tx.force)
3930                .await?;
3931
3932        transfer_data.sources.push((
3933            TransferSource::ExtendedKey(source.to_owned()),
3934            token.to_owned(),
3935            validated_amount,
3936        ));
3937
3938        data = data
3939            .debit(MASP, token.to_owned(), validated_amount)
3940            .ok_or(Error::Other("Combined transfer overflows".to_string()))?;
3941
3942        // Transfer the frontend fee (if required), take the fee percentage from
3943        // every source
3944        if let Some((sus_fee_target, percentage)) = &args.frontend_sus_fee {
3945            // NOTE: The frontend fee should NOT account for the masp fee
3946            // payment amount
3947            let validated_fee_amount = compute_masp_frontend_sus_fee(
3948                context,
3949                &validated_amount,
3950                percentage,
3951                token,
3952                args.tx.force,
3953            )
3954            .await?;
3955            data = data
3956                .transfer(
3957                    MASP,
3958                    sus_fee_target.effective_address(),
3959                    token.to_owned(),
3960                    validated_fee_amount,
3961                )
3962                .ok_or(Error::Other(
3963                    "Combined transfer overflows".to_string(),
3964                ))?;
3965
3966            // Add the extra unshielding source and target
3967            transfer_data.sources.push((
3968                TransferSource::ExtendedKey(source.to_owned()),
3969                token.to_owned(),
3970                validated_fee_amount,
3971            ));
3972            transfer_data.targets.push((
3973                sus_fee_target.to_owned(),
3974                token.to_owned(),
3975                validated_fee_amount,
3976            ));
3977        }
3978    }
3979
3980    // Add masp fee payment if necessary
3981    let masp_fee_data = if let Some(wrap_tx) = &wrap_args {
3982        let masp_fee_data = get_masp_fee_payment_amount(
3983            context,
3984            wrap_tx,
3985            // If no custom gas spending key is provided default to the source
3986            args.gas_spending_key
3987                .or(args.sources.first().map(|x| x.source)),
3988        )
3989        .await?;
3990        if let Some(fee_data) = &masp_fee_data {
3991            // Add another unshield to the list
3992            data = data
3993                .transfer(
3994                    MASP,
3995                    fee_data.target.to_owned(),
3996                    fee_data.token.to_owned(),
3997                    fee_data.amount,
3998                )
3999                .ok_or(Error::Other(
4000                    "Combined transfer overflows".to_string(),
4001                ))?;
4002        }
4003
4004        masp_fee_data
4005    } else {
4006        None
4007    };
4008
4009    let shielded_parts = construct_shielded_parts(
4010        context,
4011        transfer_data,
4012        masp_fee_data,
4013        args.tx.expiration.to_datetime(),
4014        bparams,
4015    )
4016    .await?
4017    .expect("Shielding transfer must have shielded parts");
4018
4019    let add_shielded_parts = |tx: &mut Tx, data: &mut token::Transfer| {
4020        // Add the MASP Transaction and its Builder to facilitate validation
4021        let (
4022            ShieldedTransfer {
4023                builder,
4024                masp_tx,
4025                metadata,
4026                epoch: _,
4027            },
4028            asset_types,
4029        ) = shielded_parts;
4030        // Add a MASP Transaction section to the Tx and get the tx hash
4031        let shielded_section_hash = tx.add_masp_tx_section(masp_tx).1;
4032
4033        tx.add_masp_builder(MaspBuilder {
4034            asset_types,
4035            // Store how the Info objects map to Descriptors/Outputs
4036            metadata,
4037            // Store the data that was used to construct the Transaction
4038            builder,
4039            // Link the Builder to the Transaction by hash code
4040            target: shielded_section_hash,
4041        });
4042
4043        data.shielded_section_hash = Some(shielded_section_hash);
4044        match signing_data {
4045            SigningData::Inner(ref mut signing_tx_data) => {
4046                signing_tx_data.shielded_hash = Some(shielded_section_hash);
4047            }
4048            SigningData::Wrapper(ref mut signing_wrapper_data) => {
4049                signing_wrapper_data
4050                    .signing_data
4051                    .first_mut()
4052                    .expect("Missing expected inner unshielding transaction")
4053                    .shielded_hash = Some(shielded_section_hash);
4054            }
4055        };
4056        tracing::debug!("Transfer data {data:?}");
4057        Ok(())
4058    };
4059
4060    let tx = build(
4061        context,
4062        &args.tx,
4063        args.tx_code_path.clone(),
4064        data,
4065        add_shielded_parts,
4066        wrap_args,
4067    )
4068    .await?;
4069    Ok((tx, signing_data))
4070}
4071
4072// Construct the shielded part of the transaction, if any
4073async fn construct_shielded_parts<N: Namada>(
4074    context: &N,
4075    data: MaspTransferData,
4076    fee_data: Option<MaspFeeData>,
4077    expiration: Option<DateTimeUtc>,
4078    bparams: &mut impl BuildParams,
4079) -> Result<Option<(ShieldedTransfer, HashSet<AssetData>)>> {
4080    // Precompute asset types to increase chances of success in decoding
4081    let token_map = context.wallet().await.get_addresses();
4082    let tokens = token_map.values().collect();
4083
4084    let stx_result = {
4085        let mut shielded = context.shielded_mut().await;
4086        _ = shielded
4087            .precompute_asset_types(context.client(), tokens)
4088            .await;
4089
4090        shielded
4091            .gen_shielded_transfer(context, data, fee_data, expiration, bparams)
4092            .await
4093    };
4094
4095    let shielded_parts = match stx_result {
4096        Ok(Some(stx)) => stx,
4097        Ok(None) => return Ok(None),
4098        Err(err) => {
4099            return Err(TxSubmitError::MaspError(format!(
4100                "Failed to construct MASP transaction shielded parts: {err}"
4101            ))
4102            .into());
4103        }
4104    };
4105
4106    // Get the decoded asset types used in the transaction to give offline
4107    // wallet users more information
4108    #[allow(clippy::disallowed_methods)]
4109    let asset_types = used_asset_types(context, &shielded_parts.builder)
4110        .await
4111        .unwrap_or_default();
4112
4113    Ok(Some((shielded_parts, asset_types)))
4114}
4115
4116/// Submit a transaction to initialize an account
4117pub async fn build_init_account(
4118    context: &impl Namada,
4119    args::TxInitAccount {
4120        tx: tx_args,
4121        vp_code_path,
4122        tx_code_path,
4123        public_keys,
4124        threshold,
4125    }: &args::TxInitAccount,
4126) -> Result<(Tx, SigningData)> {
4127    let (signing_data, wrap_args, _) = derive_build_data(
4128        context,
4129        tx_args
4130            .wrap_tx
4131            .as_ref()
4132            .map(|wrap_args| ExtendedWrapperArgs {
4133                wrap_args,
4134                disposable_gas_payer: false,
4135            }),
4136        tx_args.force,
4137        None,
4138        tx_args.signing_keys.to_owned(),
4139        vec![],
4140    )
4141    .await?;
4142
4143    let vp_code_hash = query_wasm_code_hash_buf(context, vp_code_path).await?;
4144
4145    let threshold = match threshold {
4146        Some(threshold) => {
4147            let threshold = *threshold;
4148            if (threshold > 0 && public_keys.len() as u8 >= threshold)
4149                || tx_args.force
4150            {
4151                threshold
4152            } else {
4153                edisplay_line!(
4154                    context.io(),
4155                    "Invalid account threshold: either the provided threshold \
4156                     is zero or the number of public keys is less than the \
4157                     threshold."
4158                );
4159                if !tx_args.force {
4160                    return Err(Error::from(
4161                        TxSubmitError::InvalidAccountThreshold,
4162                    ));
4163                }
4164                threshold
4165            }
4166        }
4167        None => {
4168            if public_keys.len() == 1 {
4169                1u8
4170            } else {
4171                return Err(Error::from(
4172                    TxSubmitError::MissingAccountThreshold,
4173                ));
4174            }
4175        }
4176    };
4177
4178    let data = InitAccount {
4179        public_keys: public_keys.clone(),
4180        // We will add the hash inside the add_code_hash function
4181        vp_code_hash: Hash::zero(),
4182        threshold,
4183    };
4184
4185    let add_code_hash = |tx: &mut Tx, data: &mut InitAccount| {
4186        let extra_section_hash = tx.add_extra_section_from_hash(
4187            vp_code_hash,
4188            Some(vp_code_path.to_string_lossy().into_owned()),
4189        );
4190        data.vp_code_hash = extra_section_hash;
4191        Ok(())
4192    };
4193    build(
4194        context,
4195        tx_args,
4196        tx_code_path.clone(),
4197        data,
4198        add_code_hash,
4199        wrap_args,
4200    )
4201    .await
4202    .map(|tx| (tx, signing_data))
4203}
4204
4205/// Submit a transaction to update a VP
4206pub async fn build_update_account(
4207    context: &impl Namada,
4208    args::TxUpdateAccount {
4209        tx: tx_args,
4210        vp_code_path,
4211        tx_code_path,
4212        addr,
4213        public_keys,
4214        threshold,
4215    }: &args::TxUpdateAccount,
4216) -> Result<(Tx, SigningData)> {
4217    let (signing_data, wrap_args, _) = derive_build_data(
4218        context,
4219        tx_args
4220            .wrap_tx
4221            .as_ref()
4222            .map(|wrap_args| ExtendedWrapperArgs {
4223                wrap_args,
4224                disposable_gas_payer: false,
4225            }),
4226        tx_args.force,
4227        Some(addr.clone()),
4228        tx_args.signing_keys.to_owned(),
4229        vec![],
4230    )
4231    .await?;
4232
4233    let account = if let Some(account) =
4234        rpc::get_account_info(context.client(), addr).await?
4235    {
4236        account
4237    } else {
4238        return Err(Error::from(TxSubmitError::LocationDoesNotExist(
4239            addr.clone(),
4240        )));
4241    };
4242
4243    let threshold = if let Some(threshold) = threshold {
4244        let threshold = *threshold;
4245
4246        let invalid_threshold = threshold.is_zero();
4247        let invalid_threshold_updated =
4248            !public_keys.is_empty() && public_keys.len() < threshold as usize;
4249        let invalid_threshold_current = public_keys.is_empty()
4250            && account.get_all_public_keys().len() < threshold as usize;
4251
4252        if invalid_threshold
4253            || invalid_threshold_updated
4254            || invalid_threshold_current
4255        {
4256            edisplay_line!(
4257                context.io(),
4258                "Invalid account threshold: either the provided threshold is \
4259                 zero or the number of public keys is less than the threshold."
4260            );
4261            if !tx_args.force {
4262                return Err(Error::from(
4263                    TxSubmitError::InvalidAccountThreshold,
4264                ));
4265            }
4266        }
4267
4268        Some(threshold)
4269    } else {
4270        let invalid_too_few_pks = !public_keys.is_empty()
4271            && public_keys.len() < account.threshold as usize;
4272
4273        if invalid_too_few_pks {
4274            return Err(Error::from(TxSubmitError::InvalidAccountThreshold));
4275        }
4276
4277        None
4278    };
4279
4280    let vp_code_hash = match vp_code_path {
4281        Some(code_path) => {
4282            let vp_hash = query_wasm_code_hash_buf(context, code_path).await?;
4283            Some(vp_hash)
4284        }
4285        None => None,
4286    };
4287
4288    let chain_id = tx_args.chain_id.clone().unwrap();
4289    let mut tx = Tx::new(chain_id, tx_args.expiration.to_datetime());
4290    if let Some(memo) = &tx_args.memo {
4291        tx.add_memo(memo);
4292    }
4293    let extra_section_hash = vp_code_path.as_ref().zip(vp_code_hash).map(
4294        |(code_path, vp_code_hash)| {
4295            tx.add_extra_section_from_hash(
4296                vp_code_hash,
4297                Some(code_path.to_string_lossy().into_owned()),
4298            )
4299        },
4300    );
4301
4302    let data = UpdateAccount {
4303        addr: account.address,
4304        vp_code_hash: extra_section_hash,
4305        public_keys: public_keys.clone(),
4306        threshold,
4307    };
4308
4309    let add_code_hash = |tx: &mut Tx, data: &mut UpdateAccount| {
4310        let extra_section_hash = vp_code_path.as_ref().zip(vp_code_hash).map(
4311            |(code_path, vp_code_hash)| {
4312                tx.add_extra_section_from_hash(
4313                    vp_code_hash,
4314                    Some(code_path.to_string_lossy().into_owned()),
4315                )
4316            },
4317        );
4318        data.vp_code_hash = extra_section_hash;
4319        Ok(())
4320    };
4321    build(
4322        context,
4323        tx_args,
4324        tx_code_path.clone(),
4325        data,
4326        add_code_hash,
4327        wrap_args,
4328    )
4329    .await
4330    .map(|tx| (tx, signing_data))
4331}
4332
4333/// Submit a custom transaction
4334pub async fn build_custom(
4335    context: &impl Namada,
4336    args::TxCustom {
4337        tx: tx_args,
4338        code_path,
4339        data_path,
4340        serialized_tx,
4341        owner,
4342        signatures,
4343        wrapper_signature,
4344    }: &args::TxCustom,
4345) -> Result<(Tx, SigningData)> {
4346    let mut tx = if let Some(serialized_tx) = serialized_tx {
4347        Tx::try_from_json_bytes(serialized_tx.as_ref()).map_err(|_| {
4348            Error::Other(
4349                "Invalid tx deserialization. Please make sure you are passing \
4350                 a file in .tx format, typically produced from using the \
4351                 `--dump-tx` or `--dump-wrapper-tx` flag."
4352                    .to_string(),
4353            )
4354        })?
4355    } else {
4356        let code_path = code_path
4357            .as_ref()
4358            .ok_or(Error::Other("No code path supplied".to_string()))?;
4359        let tx_code_hash = query_wasm_code_hash_buf(context, code_path).await?;
4360        let chain_id = tx_args.chain_id.clone().unwrap();
4361        let mut tx = Tx::new(chain_id, tx_args.expiration.to_datetime());
4362        if let Some(memo) = &tx_args.memo {
4363            tx.add_memo(memo);
4364        }
4365        tx.add_code_from_hash(
4366            tx_code_hash,
4367            Some(code_path.to_string_lossy().into_owned()),
4368        );
4369        data_path.clone().map(|data| tx.add_serialized_data(data));
4370        tx
4371    };
4372
4373    // Wrap the tx only if it's not already. If the user passed the argument for
4374    // the wrapper signatures we also assume the followings:
4375    //    1. The tx loaded is of type Wrapper
4376    //    2. The user also provided the offline signatures for the inner
4377    //       transaction(s)
4378    // The workflow is the following:
4379    //    1. If no signatures were provided we generate a SigningData to sign
4380    //       the tx
4381    //    2. If only the inner sigs were provided we generate a SigningData that
4382    //       will attach them and then sign the wrapper online
4383    //    3. If the wrapper signature was provided then we also expect the inner
4384    //       signature(s) to have been provided, in this case we generate a
4385    //       SigningData to attach all these signatures
4386    let (signing_data, wrap_tx) = if tx.header.wrapper().is_some() {
4387        match (wrapper_signature, &tx_args.wrap_tx) {
4388            (None, None) => {
4389                return Err(Error::Other(
4390                    "A wrapper signature or a wrapper signer must be provided \
4391                     when loading a wrapped custom transaction"
4392                        .to_string(),
4393                ));
4394            }
4395            (None, Some(wrap_args)) => {
4396                let (signing_data, wrap_tx, _) = derive_build_data(
4397                    context,
4398                    Some(ExtendedWrapperArgs {
4399                        wrap_args,
4400                        // The optional masp fee paying transaction has
4401                        // already been
4402                        // produced so no need to generate a disposable
4403                        // address anyway
4404                        disposable_gas_payer: false,
4405                    }),
4406                    tx_args.force,
4407                    owner.to_owned(),
4408                    tx_args.signing_keys.to_owned(),
4409                    signatures.to_owned(),
4410                )
4411                .await?;
4412
4413                (signing_data, wrap_tx)
4414            }
4415            // If both a serialized wrapper signature and a request to
4416            // wrap the tx are passed, the serialized signature takes
4417            // precedence
4418            (Some(wrapper_sig), _) => (
4419                SigningData::Wrapper(SigningWrapperData {
4420                    signing_data: vec![SigningTxData {
4421                        owner: None,
4422                        public_keys: Default::default(),
4423                        threshold: 0,
4424                        account_public_keys_map: Default::default(),
4425                        shielded_hash: None,
4426                        signatures: signatures.to_owned(),
4427                    }],
4428                    fee_auth: FeeAuthorization::Signature(
4429                        wrapper_sig.to_owned(),
4430                    ),
4431                }),
4432                None,
4433            ),
4434        }
4435    } else {
4436        if wrapper_signature.is_some() {
4437            return Err(Error::Other(
4438                "A wrapper signature was provided but the transaction is not \
4439                 a wrapper"
4440                    .to_string(),
4441            ));
4442        }
4443
4444        let (signing_data, wrap_tx, _) = derive_build_data(
4445            context,
4446            tx_args
4447                .wrap_tx
4448                .as_ref()
4449                .map(|wrap_args| ExtendedWrapperArgs {
4450                    wrap_args,
4451                    disposable_gas_payer: false,
4452                }),
4453            tx_args.force,
4454            owner.clone(),
4455            tx_args.signing_keys.to_owned(),
4456            signatures.to_owned(),
4457        )
4458        .await?;
4459
4460        (signing_data, wrap_tx)
4461    };
4462
4463    if let Some(WrapArgs {
4464        fee_amount,
4465        fee_payer,
4466        fee_token,
4467        gas_limit,
4468    }) = wrap_tx
4469    {
4470        tx.add_wrapper(
4471            Fee {
4472                amount_per_gas_unit: fee_amount,
4473                token: fee_token,
4474            },
4475            fee_payer,
4476            gas_limit,
4477        );
4478    }
4479
4480    Ok((tx, signing_data))
4481}
4482
4483/// Generate IBC shielded transfer
4484pub async fn gen_ibc_shielding_transfer<N: Namada>(
4485    context: &N,
4486    args: args::GenIbcShieldingTransfer,
4487) -> Result<Option<MaspTransaction>> {
4488    let source = IBC;
4489
4490    let token = match args.asset {
4491        args::IbcShieldingTransferAsset::Address(addr) => addr,
4492        args::IbcShieldingTransferAsset::LookupNamadaAddress {
4493            token,
4494            port_id,
4495            channel_id,
4496        } => {
4497            let (src_port_id, src_channel_id) =
4498                get_ibc_src_port_channel(context, &port_id, &channel_id)
4499                    .await?;
4500            let ibc_denom =
4501                rpc::query_ibc_denom(context, &token, Some(&source)).await;
4502
4503            namada_ibc::received_ibc_token(
4504                &ibc_denom,
4505                &src_port_id,
4506                &src_channel_id,
4507                &port_id,
4508                &channel_id,
4509            )
4510            .map_err(|e| {
4511                Error::Other(format!("Getting IBC Token failed: error {e}"))
4512            })?
4513        }
4514    };
4515
4516    let validated_amount =
4517        validate_amount(context, args.amount, &token, false).await?;
4518
4519    // Precompute asset types to increase chances of success in decoding
4520    let token_map = context.wallet().await.get_addresses();
4521    let tokens = token_map.values().collect();
4522    let _ = context
4523        .shielded_mut()
4524        .await
4525        .precompute_asset_types(context.client(), tokens)
4526        .await;
4527
4528    let (extra_target, source_amount) = match &args.frontend_sus_fee {
4529        Some((target, percentage)) => {
4530            let validated_fee_amount = compute_masp_frontend_sus_fee(
4531                context,
4532                &validated_amount,
4533                percentage,
4534                &token,
4535                false,
4536            )
4537            .await?;
4538            let source_amount =
4539                checked!(validated_amount + validated_fee_amount)?;
4540
4541            (
4542                vec![(
4543                    TransferTarget::PaymentAddress(target.to_owned()),
4544                    token.to_owned(),
4545                    validated_fee_amount,
4546                )],
4547                source_amount,
4548            )
4549        }
4550        None => (vec![], validated_amount),
4551    };
4552
4553    let masp_transfer_data = MaspTransferData {
4554        sources: vec![(
4555            TransferSource::Address(source.clone()),
4556            token.clone(),
4557            source_amount,
4558        )],
4559        targets: [
4560            extra_target,
4561            vec![(
4562                TransferTarget::PaymentAddress(args.target),
4563                token.clone(),
4564                validated_amount,
4565            )],
4566        ]
4567        .concat(),
4568    };
4569
4570    let shielded_transfer = {
4571        let mut shielded = context.shielded_mut().await;
4572        shielded
4573            .gen_shielded_transfer(
4574                context,
4575                masp_transfer_data,
4576                // Fees are paid from the transparent balance of the relayer
4577                None,
4578                args.expiration.to_datetime(),
4579                &mut RngBuildParams::new(OsRng),
4580            )
4581            .await
4582            .map_err(|err| TxSubmitError::MaspError(err.to_string()))?
4583    };
4584
4585    Ok(shielded_transfer.map(|st| st.masp_tx))
4586}
4587
4588pub(crate) async fn get_ibc_src_port_channel(
4589    context: &impl Namada,
4590    dest_port_id: &PortId,
4591    dest_channel_id: &ChannelId,
4592) -> Result<(PortId, ChannelId)> {
4593    use crate::ibc::core::channel::types::channel::ChannelEnd;
4594    use crate::ibc::primitives::proto::Protobuf;
4595
4596    let channel_key = channel_key(dest_port_id, dest_channel_id);
4597    let bytes = rpc::query_storage_value_bytes(
4598        context.client(),
4599        &channel_key,
4600        None,
4601        false,
4602    )
4603    .await?
4604    .0
4605    .ok_or_else(|| {
4606        Error::Other(format!(
4607            "No channel end: port {dest_port_id}, channel {dest_channel_id}"
4608        ))
4609    })?;
4610    let channel = ChannelEnd::decode_vec(&bytes).map_err(|_| {
4611        Error::Other(format!(
4612            "Decoding channel end failed: port {dest_port_id}, channel \
4613             {dest_channel_id}",
4614        ))
4615    })?;
4616    channel
4617        .remote
4618        .channel_id()
4619        .map(|src_channel| {
4620            (channel.remote.port_id.clone(), src_channel.clone())
4621        })
4622        .ok_or_else(|| {
4623            Error::Other(format!(
4624                "The source channel doesn't exist: port {dest_port_id}, \
4625                 channel {dest_channel_id}"
4626            ))
4627        })
4628}
4629
4630async fn expect_dry_broadcast(
4631    to_broadcast: TxBroadcastData,
4632    context: &impl Namada,
4633) -> Result<ProcessTxResponse> {
4634    match to_broadcast {
4635        TxBroadcastData::DryRun(tx) => {
4636            let result = rpc::dry_run_tx(context, tx.to_bytes()).await?;
4637            Ok(ProcessTxResponse::DryRun(result))
4638        }
4639        TxBroadcastData::Live { tx, tx_hash: _ } => {
4640            Err(Error::from(TxSubmitError::ExpectDryRun(tx)))
4641        }
4642    }
4643}
4644
4645fn lift_rpc_error<T>(res: std::result::Result<T, RpcError>) -> Result<T> {
4646    res.map_err(|err| Error::from(TxSubmitError::TxBroadcast(err)))
4647}
4648
4649/// Returns the given validator if the given address is a validator,
4650/// otherwise returns an error, force forces the address through even
4651/// if it isn't a validator
4652async fn known_validator_or_err(
4653    validator: Address,
4654    force: bool,
4655    context: &impl Namada,
4656) -> Result<Address> {
4657    // Check that the validator address exists on chain
4658    let is_validator = rpc::is_validator(context.client(), &validator).await?;
4659    if !is_validator {
4660        if force {
4661            edisplay_line!(
4662                context.io(),
4663                "The address {} doesn't belong to any known validator account.",
4664                validator
4665            );
4666            Ok(validator)
4667        } else {
4668            Err(Error::from(TxSubmitError::InvalidValidatorAddress(
4669                validator,
4670            )))
4671        }
4672    } else {
4673        Ok(validator)
4674    }
4675}
4676
4677/// general pattern for checking if an address exists on the chain, or
4678/// throwing an error if it's not forced. Takes a generic error
4679/// message and the error type.
4680async fn address_exists_or_err<F>(
4681    addr: Address,
4682    force: bool,
4683    context: &impl Namada,
4684    message: String,
4685    err: F,
4686) -> Result<Address>
4687where
4688    F: FnOnce(Address) -> Error,
4689{
4690    let addr_exists = rpc::known_address(context.client(), &addr).await?;
4691    if !addr_exists {
4692        if force {
4693            edisplay_line!(context.io(), "{}", message);
4694            Ok(addr)
4695        } else {
4696            Err(err(addr))
4697        }
4698    } else {
4699        Ok(addr)
4700    }
4701}
4702
4703/// Returns the given source address if the given address exists on chain
4704/// otherwise returns an error, force forces the address through even
4705/// if it isn't on chain
4706async fn source_exists_or_err(
4707    token: Address,
4708    force: bool,
4709    context: &impl Namada,
4710) -> Result<Address> {
4711    let message =
4712        format!("The source address {} doesn't exist on chain.", token);
4713    address_exists_or_err(token, force, context, message, |err| {
4714        Error::from(TxSubmitError::SourceDoesNotExist(err))
4715    })
4716    .await
4717}
4718
4719/// Returns the given target address if the given address exists on chain
4720/// otherwise returns an error, force forces the address through even
4721/// if it isn't on chain
4722async fn target_exists_or_err(
4723    token: Address,
4724    force: bool,
4725    context: &impl Namada,
4726) -> Result<Address> {
4727    let message =
4728        format!("The target address {} doesn't exist on chain.", token);
4729    address_exists_or_err(token, force, context, message, |err| {
4730        Error::from(TxSubmitError::TargetLocationDoesNotExist(err))
4731    })
4732    .await
4733}
4734
4735/// Returns the given refund target address if the given address is valid for
4736/// the IBC shielded transfer. Returns an error if the address is a payment
4737/// address or given for non-shielded transfer.
4738async fn get_refund_target(
4739    context: &impl Namada,
4740    source: &TransferSource,
4741    refund_target: &Option<TransferTarget>,
4742) -> Result<Option<Address>> {
4743    match (source, refund_target) {
4744        (_, Some(TransferTarget::PaymentAddress(pa))) => {
4745            Err(Error::Other(format!(
4746                "Supporting only a transparent address as a refund target: {}",
4747                pa,
4748            )))
4749        }
4750        (
4751            TransferSource::ExtendedKey(_),
4752            Some(TransferTarget::Address(addr)),
4753        ) => Ok(Some(addr.clone())),
4754        (TransferSource::ExtendedKey(_), None) => {
4755            // Generate a new transparent address if it doesn't exist
4756            let mut rng = OsRng;
4757            let mut wallet = context.wallet_mut().await;
4758            let mut alias =
4759                format!("{IBC_REFUND_ALIAS_PREFIX}-{}", rng.next_u64());
4760            while wallet.find_address(&alias).is_some() {
4761                alias = format!("{IBC_REFUND_ALIAS_PREFIX}-{}", rng.next_u64());
4762            }
4763            wallet
4764                .gen_store_secret_key(
4765                    SchemeType::Ed25519,
4766                    Some(alias.clone()),
4767                    false,
4768                    None,
4769                    &mut rng,
4770                )
4771                .ok_or_else(|| {
4772                    Error::Other(
4773                        "Adding a new refund address failed".to_string(),
4774                    )
4775                })?;
4776            wallet.save().map_err(|e| {
4777                Error::Other(format!("Saving wallet error: {e}"))
4778            })?;
4779            let addr = wallet.find_address(alias).ok_or_else(|| {
4780                Error::Other("Finding the reund address failed".to_string())
4781            })?;
4782            Ok(Some(addr.into_owned()))
4783        }
4784        (_, Some(_)) => Err(Error::Other(
4785            "Refund target can't be specified for non-shielded transfer"
4786                .to_string(),
4787        )),
4788        (_, None) => Ok(None),
4789    }
4790}
4791
4792enum CheckBalance {
4793    Balance(token::Amount),
4794    Query(storage::Key),
4795}
4796
4797/// Checks the balance at the given address is enough to transfer the
4798/// given amount, along with the balance even existing. Force
4799/// overrides this.
4800async fn check_balance_too_low_err<N: Namada>(
4801    token: &Address,
4802    source: &Address,
4803    amount: token::Amount,
4804    balance: CheckBalance,
4805    force: bool,
4806    context: &N,
4807) -> Result<()> {
4808    let balance = match balance {
4809        CheckBalance::Balance(amt) => amt,
4810        CheckBalance::Query(balance_key) => {
4811            match rpc::query_storage_value::<N::Client, token::Amount>(
4812                context.client(),
4813                &balance_key,
4814            )
4815            .await
4816            {
4817                Ok(amt) => amt,
4818                Err(Error::Query(
4819                    QueryError::General(_) | QueryError::NoSuchKey(_),
4820                )) => {
4821                    if force {
4822                        edisplay_line!(
4823                            context.io(),
4824                            "No balance found for the source {} of token {}",
4825                            source,
4826                            token
4827                        );
4828                        return Ok(());
4829                    } else {
4830                        return Err(Error::from(
4831                            TxSubmitError::NoBalanceForToken(
4832                                source.clone(),
4833                                token.clone(),
4834                            ),
4835                        ));
4836                    }
4837                }
4838                // We're either facing a no response or a conversion error
4839                // either way propagate it up
4840                Err(err) => return Err(err),
4841            }
4842        }
4843    };
4844
4845    match balance.checked_sub(amount) {
4846        Some(_) => Ok(()),
4847        None => {
4848            if force {
4849                edisplay_line!(
4850                    context.io(),
4851                    "The balance of the source {} of token {} is lower than \
4852                     the amount to be transferred. Amount to transfer is {} \
4853                     and the balance is {}.",
4854                    source,
4855                    token,
4856                    context.format_amount(token, amount).await,
4857                    context.format_amount(token, balance).await,
4858                );
4859                Ok(())
4860            } else {
4861                Err(Error::from(TxSubmitError::BalanceTooLow(
4862                    source.clone(),
4863                    token.clone(),
4864                    amount.to_string_native(),
4865                    balance.to_string_native(),
4866                )))
4867            }
4868        }
4869    }
4870}
4871
4872async fn query_wasm_code_hash_buf(
4873    context: &impl Namada,
4874    path: &Path,
4875) -> Result<Hash> {
4876    query_wasm_code_hash(context, path.to_string_lossy()).await
4877}
4878
4879/// A helper for [`fn build`] that can be used for `on_tx` arg that does nothing
4880fn do_nothing<D>(_tx: &mut Tx, _data: &mut D) -> Result<()>
4881where
4882    D: BorshSerialize,
4883{
4884    Ok(())
4885}
4886
4887fn proposal_to_vec(proposal: OnChainProposal) -> Result<Vec<u8>> {
4888    borsh::to_vec(&proposal.content)
4889        .map_err(|e| Error::from(EncodingError::Conversion(e.to_string())))
4890}