1use 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
96pub const TX_INIT_ACCOUNT_WASM: &str = "tx_init_account.wasm";
98pub const TX_BECOME_VALIDATOR_WASM: &str = "tx_become_validator.wasm";
100pub const TX_UNJAIL_VALIDATOR_WASM: &str = "tx_unjail_validator.wasm";
102pub const TX_DEACTIVATE_VALIDATOR_WASM: &str = "tx_deactivate_validator.wasm";
104pub const TX_REACTIVATE_VALIDATOR_WASM: &str = "tx_reactivate_validator.wasm";
106pub const TX_INIT_PROPOSAL: &str = "tx_init_proposal.wasm";
108pub const TX_VOTE_PROPOSAL: &str = "tx_vote_proposal.wasm";
110pub const TX_REVEAL_PK: &str = "tx_reveal_pk.wasm";
112pub const TX_UPDATE_ACCOUNT_WASM: &str = "tx_update_account.wasm";
114pub const TX_TRANSFER_WASM: &str = "tx_transfer.wasm";
116pub const TX_IBC_WASM: &str = "tx_ibc.wasm";
118pub const VP_USER_WASM: &str = "vp_user.wasm";
120pub const TX_BOND_WASM: &str = "tx_bond.wasm";
122pub const TX_UNBOND_WASM: &str = "tx_unbond.wasm";
124pub const TX_WITHDRAW_WASM: &str = "tx_withdraw.wasm";
126pub const TX_CLAIM_REWARDS_WASM: &str = "tx_claim_rewards.wasm";
128pub const TX_BRIDGE_POOL_WASM: &str = "tx_bridge_pool.wasm";
130pub const TX_CHANGE_COMMISSION_WASM: &str =
132 "tx_change_validator_commission.wasm";
133pub const TX_CHANGE_CONSENSUS_KEY_WASM: &str = "tx_change_consensus_key.wasm";
135pub const TX_CHANGE_METADATA_WASM: &str = "tx_change_validator_metadata.wasm";
137pub const TX_RESIGN_STEWARD: &str = "tx_resign_steward.wasm";
139pub const TX_UPDATE_STEWARD_COMMISSION: &str =
141 "tx_update_steward_commission.wasm";
142pub const TX_REDELEGATE_WASM: &str = "tx_redelegate.wasm";
144
145const IBC_REFUND_ALIAS_PREFIX: &str = "ibc-refund-target";
147
148const DEFAULT_NAMADA_EVENTS_MAX_WAIT_TIME_SECONDS: u64 = 60;
151
152#[derive(Debug)]
154pub enum ProcessTxResponse {
155 Applied(TxResponse),
157 Broadcast(Response),
159 DryRun(DryRunResult),
161}
162
163impl ProcessTxResponse {
164 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
193pub 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 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
244pub async fn process_tx(
247 context: &impl Namada,
248 args: &args::Tx,
249 tx: Tx,
250) -> Result<ProcessTxResponse> {
251 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 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 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
323pub async fn is_reveal_pk_needed<C: Client + Sync>(
325 client: &C,
326 address: &Address,
327) -> Result<bool> {
328 Ok(!has_revealed_pk(client, address).await?)
330}
331
332pub 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
340pub 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
371pub 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 {
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
413pub 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_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 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
454pub fn display_batch_resp(context: &impl Namada, resp: &TxResponse) {
456 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 let Some((first_inner_hash, first_result)) = batch_results.first() {
498 if !wrapper_successful {
499 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 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 *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 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
616pub 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 display_line!(
626 context.io(),
627 "The transaction initialized {} new account{}",
628 len,
629 if len == 1 { "" } else { "s" }
630 );
631 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 initialized_account_alias.into()
640 } else {
641 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
676pub 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 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
745pub 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
883pub 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 let validator =
921 known_validator_or_err(validator.clone(), tx_args.force, context)
922 .await?;
923
924 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 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 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 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
1116pub 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
1187pub 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
1237pub 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 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 }
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
1343pub 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 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
1420pub 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 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
1496pub 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 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 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 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 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 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 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 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
1692pub 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 let validator =
1723 known_validator_or_err(validator.clone(), tx_args.force, context)
1724 .await?;
1725
1726 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 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
1781pub 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 let validator =
1810 known_validator_or_err(validator.clone(), tx_args.force, context)
1811 .await?;
1812
1813 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
1835pub 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 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 let validator =
1860 known_validator_or_err(validator.clone(), tx_args.force, context)
1861 .await?;
1862
1863 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 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 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 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
1976pub 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 let bond_source = source.clone().unwrap_or_else(|| args.validator.clone());
1985
1986 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
2055pub(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 (validate_fee(context, wrap_args, force).await?, None)
2084 } else {
2085 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
2118pub 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 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 let validator =
2143 known_validator_or_err(validator.clone(), tx_args.force, context)
2144 .await?;
2145
2146 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 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 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 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
2260pub 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
2320pub 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 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 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 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
2474pub 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 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 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 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 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 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 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 ð_cold_key.clone().unwrap(),
2605 )
2606 .unwrap(),
2607 eth_hot_key: key::secp256k1::PublicKey::try_from_pk(
2608 ð_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 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
2694pub 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
2740pub 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
2787pub 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 let source = args.source.effective_address();
2822 let source =
2823 source_exists_or_err(source.clone(), args.tx.force, context).await?;
2824 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 != 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 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 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 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 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 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 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 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 let ibc_denom =
3039 rpc::query_ibc_denom(context, &args.token.to_string(), Some(&source))
3040 .await;
3041 assert!(
3044 (args.source.spending_key().is_some() && refund_target.is_some())
3045 || (args.source.address().is_some() && refund_target.is_none())
3046 );
3047 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 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 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#[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 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
3207async 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
3226async 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 for input in builder.sapling_inputs() {
3236 add_asset_type(&mut asset_types, context, input.asset_type()).await;
3237 }
3238 for input in builder.transparent_inputs() {
3240 add_asset_type(&mut asset_types, context, input.coin().asset_type())
3241 .await;
3242 }
3243 for output in builder.sapling_outputs() {
3245 add_asset_type(&mut asset_types, context, output.asset_type()).await;
3246 }
3247 for output in builder.transparent_outputs() {
3249 add_asset_type(&mut asset_types, context, output.asset_type()).await;
3250 }
3251 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
3262pub 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 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
3320pub 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 let source = if args.sources.len() == 1 {
3329 args.sources
3331 .first()
3332 .map(|transfer_data| transfer_data.source.clone())
3333 } else {
3334 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 source_exists_or_err(source.clone(), args.tx.force, context).await?;
3362
3363 let validated_amount =
3365 validate_amount(context, amount.to_owned(), token, args.tx.force)
3366 .await?;
3367
3368 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 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 target_exists_or_err(target.clone(), args.tx.force, context).await?;
3403
3404 let validated_amount =
3406 validate_amount(context, amount.to_owned(), token, args.tx.force)
3407 .await?;
3408
3409 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
3428pub 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 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 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 let mut data = token::Transfer::default();
3488
3489 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 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 let (
3531 ShieldedTransfer {
3532 builder,
3533 masp_tx,
3534 metadata,
3535 epoch: _,
3536 },
3537 asset_types,
3538 ) = shielded_parts;
3539 let section_hash = tx.add_masp_tx_section(masp_tx).1;
3541
3542 tx.add_masp_builder(MaspBuilder {
3543 asset_types,
3544 metadata,
3546 builder,
3548 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
3581async 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
3619async 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_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
3658pub 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 args.sources
3667 .first()
3668 .map(|transfer_data| transfer_data.source.clone())
3669 } else {
3670 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 let validated_amount =
3700 validate_amount(context, amount.to_owned(), token, args.tx.force)
3701 .await?;
3702
3703 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 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 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 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 let (
3826 ShieldedTransfer {
3827 builder,
3828 masp_tx,
3829 metadata,
3830 epoch: _,
3831 },
3832 asset_types,
3833 ) = shielded_parts;
3834 let shielded_section_hash = tx.add_masp_tx_section(masp_tx).1;
3836
3837 tx.add_masp_builder(MaspBuilder {
3838 asset_types,
3839 metadata,
3841 builder,
3843 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
3876pub 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 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 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 if let Some((sus_fee_target, percentage)) = &args.frontend_sus_fee {
3945 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 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 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 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 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 let (
4022 ShieldedTransfer {
4023 builder,
4024 masp_tx,
4025 metadata,
4026 epoch: _,
4027 },
4028 asset_types,
4029 ) = shielded_parts;
4030 let shielded_section_hash = tx.add_masp_tx_section(masp_tx).1;
4032
4033 tx.add_masp_builder(MaspBuilder {
4034 asset_types,
4035 metadata,
4037 builder,
4039 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
4072async 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 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 #[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
4116pub 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 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
4205pub 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
4333pub 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 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 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 (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
4483pub 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 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 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
4649async fn known_validator_or_err(
4653 validator: Address,
4654 force: bool,
4655 context: &impl Namada,
4656) -> Result<Address> {
4657 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
4677async 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
4703async 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
4719async 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
4735async 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 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
4797async 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 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
4879fn 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}