1use std::env;
4use std::marker::PhantomData;
5use std::path::{Path, PathBuf};
6use std::str::FromStr;
7
8use color_eyre::eyre::Result;
9use masp_primitives::zip32::sapling::PseudoExtendedKey;
10use masp_primitives::zip32::{
11 ExtendedFullViewingKey as MaspExtendedViewingKey,
12 ExtendedSpendingKey as MaspExtendedSpendingKey,
13};
14use namada_core::masp::{
15 BalanceOwner, ExtendedSpendingKey, ExtendedViewingKey, PaymentAddress,
16 TransferSource, TransferTarget,
17};
18use namada_sdk::address::{Address, InternalAddress};
19use namada_sdk::chain::ChainId;
20use namada_sdk::ethereum_events::EthAddress;
21use namada_sdk::ibc::trace::{ibc_token, is_ibc_denom, is_nft_trace};
22use namada_sdk::io::Io;
23use namada_sdk::key::*;
24use namada_sdk::masp::ShieldedWallet;
25use namada_sdk::masp::fs::FsShieldedUtils;
26use namada_sdk::wallet::{DatedSpendingKey, DatedViewingKey, Wallet};
27use namada_sdk::{Namada, NamadaImpl};
28
29use super::args;
30use crate::cli::utils;
31use crate::config::global::GlobalConfig;
32use crate::config::{Config, genesis};
33use crate::wallet::CliWalletUtils;
34use crate::{wallet, wasm_loader};
35
36struct SkipErr;
38
39pub const ENV_VAR_WASM_DIR: &str = "NAMADA_WASM_DIR";
41
42pub const ENV_VAR_CHAIN_ID: &str = "NAMADA_CHAIN_ID";
44
45pub type WalletAddress = FromContext<Address>;
48
49pub type WalletAddrOrNativeToken = FromContext<AddrOrNativeToken>;
52
53pub type WalletSpendingKey = FromContext<PseudoExtendedKey>;
56
57pub type WalletDatedSpendingKey = FromContext<DatedSpendingKey>;
60
61pub type WalletPaymentAddr = FromContext<PaymentAddress>;
64
65pub type WalletViewingKey = FromContext<ExtendedViewingKey>;
68
69pub type WalletDatedViewingKey = FromContext<DatedViewingKey>;
72
73pub type WalletTransferSource = FromContext<TransferSource>;
76
77pub type WalletTransferTarget = FromContext<TransferTarget>;
80
81pub type WalletKeypair = FromContext<common::SecretKey>;
84
85pub type WalletPublicKey = FromContext<common::PublicKey>;
88
89pub type WalletBalanceOwner = FromContext<BalanceOwner>;
92
93pub type ConfigRpcAddress = FromContext<tendermint_rpc::Url>;
95
96#[derive(Clone, Debug)]
98pub struct AddrOrNativeToken(Address);
99
100impl From<AddrOrNativeToken> for Address {
101 fn from(AddrOrNativeToken(addr): AddrOrNativeToken) -> Self {
102 addr
103 }
104}
105
106impl FromStr for AddrOrNativeToken {
107 type Err = <Address as FromStr>::Err;
108
109 fn from_str(s: &str) -> Result<Self, Self::Err> {
110 let addr = Address::from_str(s)?;
111 Ok(Self(addr))
112 }
113}
114
115impl From<tendermint_rpc::Url> for ConfigRpcAddress {
116 fn from(value: tendermint_rpc::Url) -> Self {
117 FromContext::new(value.to_string())
118 }
119}
120
121#[derive(Debug)]
123pub struct Context {
124 pub global_args: args::Global,
126 pub global_config: GlobalConfig,
128 pub chain: Option<ChainContext>,
130}
131
132#[derive(Debug)]
134pub struct ChainContext {
135 pub wallet: Wallet<CliWalletUtils>,
137 pub config: Config,
139 pub shielded: ShieldedWallet<FsShieldedUtils>,
141 pub native_token: Address,
143}
144
145pub fn wasm_dir_from_env_or_args(
147 global_args: &args::Global,
148) -> Option<PathBuf> {
149 wasm_dir_from_env_or(global_args.wasm_dir.as_ref())
150}
151
152pub fn wasm_dir_from_env_or<P: AsRef<Path>>(
154 wasm_dir: Option<&P>,
155) -> Option<PathBuf> {
156 wasm_dir
157 .map(|wasm_dir| wasm_dir.as_ref().to_owned())
158 .or_else(|| {
159 env::var(ENV_VAR_WASM_DIR)
160 .ok()
161 .map(|wasm_dir| wasm_dir.into())
162 })
163}
164
165impl Context {
166 pub fn new<IO: Io>(global_args: args::Global) -> Result<Self> {
167 let global_config = read_or_try_new_global_config(&global_args);
168
169 let env_var_chain_id = std::env::var(ENV_VAR_CHAIN_ID)
170 .ok()
171 .and_then(|chain_id| ChainId::from_str(&chain_id).ok());
172 let chain_id = env_var_chain_id
173 .as_ref()
174 .or(global_args.chain_id.as_ref())
175 .or(global_config.default_chain_id.as_ref());
176
177 let chain = match chain_id {
178 Some(chain_id) if !global_args.is_pre_genesis => {
179 let mut config =
180 Config::load(&global_args.base_dir, chain_id, None);
181 let chain_dir = global_args.base_dir.join(chain_id.as_str());
182 let native_token =
183 genesis::chain::Finalized::read_native_token(&chain_dir)
184 .expect("Missing genesis files");
185 let wallet = if wallet::exists(&chain_dir) {
186 wallet::load(&chain_dir).unwrap()
187 } else {
188 panic!(
189 "Could not find wallet at {}.",
190 chain_dir.to_string_lossy()
191 );
192 };
193
194 if let Some(wasm_dir) = wasm_dir_from_env_or_args(&global_args)
196 {
197 config.wasm_dir = wasm_dir;
198 }
199
200 Some(ChainContext {
201 wallet,
202 config,
203 shielded: FsShieldedUtils::new(chain_dir),
204 native_token,
205 })
206 }
207 _ => None,
208 };
209
210 Ok(Self {
211 global_args,
212 global_config,
213 chain,
214 })
215 }
216
217 pub fn take_chain_or_exit(self) -> ChainContext {
220 self.chain
221 .unwrap_or_else(|| safe_exit_on_missing_chain_context())
222 }
223
224 pub fn borrow_chain_or_exit(&self) -> &ChainContext {
227 self.chain
228 .as_ref()
229 .unwrap_or_else(|| safe_exit_on_missing_chain_context())
230 }
231
232 pub fn borrow_mut_chain_or_exit(&mut self) -> &mut ChainContext {
235 self.chain
236 .as_mut()
237 .unwrap_or_else(|| safe_exit_on_missing_chain_context())
238 }
239
240 pub fn to_sdk<C, IO>(self, client: C, io: IO) -> impl Namada
242 where
243 C: namada_sdk::io::Client + Sync,
244 IO: Io,
245 {
246 let chain_ctx = self.take_chain_or_exit();
247 NamadaImpl::native_new(
248 client,
249 chain_ctx.wallet,
250 chain_ctx.shielded,
251 io,
252 chain_ctx.native_token,
253 )
254 }
255}
256
257fn safe_exit_on_missing_chain_context() -> ! {
258 eprintln!(
259 "Failed to construct Namada chain context. If no chain is configured, \
260 you may need to run `namada client utils join-network`. If the chain \
261 is configured, you may need to set the chain id with `--chain-id \
262 <chainid>`, via the env var `{ENV_VAR_CHAIN_ID}`, or configure the \
263 default chain id in the `global-config.toml` file. If you do intend \
264 to run pre-genesis operations, pass the `--pre-genesis` flag as the \
265 first argument to the command."
266 );
267 utils::safe_exit(1)
268}
269
270impl ChainContext {
271 pub fn get<T>(&self, from_context: &FromContext<T>) -> T
273 where
274 T: ArgFromContext,
275 {
276 from_context.arg_from_ctx(self).unwrap()
277 }
278
279 pub fn get_opt<T>(&self, from_context: &Option<FromContext<T>>) -> Option<T>
281 where
282 T: ArgFromContext,
283 {
284 from_context
285 .as_ref()
286 .map(|from_context| from_context.arg_from_ctx(self).unwrap())
287 }
288
289 pub fn get_cached<T>(&mut self, from_context: &FromContext<T>) -> T
291 where
292 T: ArgFromMutContext,
293 {
294 from_context.arg_from_mut_ctx(self).unwrap()
295 }
296
297 pub fn get_opt_cached<T>(
300 &mut self,
301 from_context: &Option<FromContext<T>>,
302 ) -> Option<T>
303 where
304 T: ArgFromMutContext,
305 {
306 from_context
307 .as_ref()
308 .map(|from_context| from_context.arg_from_mut_ctx(self).unwrap())
309 }
310
311 pub fn wasm_dir(&self) -> PathBuf {
315 self.config.ledger.chain_dir().join(&self.config.wasm_dir)
316 }
317
318 pub fn read_wasm(&self, file_name: impl AsRef<Path>) -> Vec<u8> {
320 wasm_loader::read_wasm_or_exit(self.wasm_dir(), file_name)
321 }
322}
323
324pub fn read_or_try_new_global_config(
327 global_args: &args::Global,
328) -> GlobalConfig {
329 GlobalConfig::read(&global_args.base_dir).unwrap_or_else(|err| {
330 eprintln!("Error reading global config: {}", err);
331 super::safe_exit(1)
332 })
333}
334
335#[derive(Debug, Clone)]
337pub struct FromContext<T> {
338 pub(crate) raw: String,
339 phantom: PhantomData<T>,
340}
341
342impl<T> FromContext<T> {
343 pub fn new(raw: String) -> FromContext<T> {
344 Self {
345 raw,
346 phantom: PhantomData,
347 }
348 }
349}
350
351impl From<FromContext<Address>> for FromContext<AddrOrNativeToken> {
352 fn from(value: FromContext<Address>) -> Self {
353 Self {
354 raw: value.raw,
355 phantom: PhantomData,
356 }
357 }
358}
359
360impl FromContext<TransferSource> {
361 pub fn to_address(&self) -> FromContext<Address> {
364 FromContext::<Address> {
365 raw: self.raw.clone(),
366 phantom: PhantomData,
367 }
368 }
369
370 pub fn to_spending_key(&self) -> FromContext<ExtendedSpendingKey> {
374 FromContext::<ExtendedSpendingKey> {
375 raw: self.raw.clone(),
376 phantom: PhantomData,
377 }
378 }
379}
380
381impl FromContext<TransferTarget> {
382 pub fn to_address(&self) -> FromContext<Address> {
385 FromContext::<Address> {
386 raw: self.raw.clone(),
387 phantom: PhantomData,
388 }
389 }
390
391 pub fn to_payment_address(&self) -> FromContext<PaymentAddress> {
394 FromContext::<PaymentAddress> {
395 raw: self.raw.clone(),
396 phantom: PhantomData,
397 }
398 }
399}
400
401impl<T> FromContext<T>
402where
403 T: ArgFromContext,
404{
405 fn arg_from_ctx(&self, ctx: &ChainContext) -> Result<T, String> {
407 T::arg_from_ctx(ctx, &self.raw)
408 }
409}
410
411impl<T> FromContext<T>
412where
413 T: ArgFromMutContext,
414{
415 fn arg_from_mut_ctx(&self, ctx: &mut ChainContext) -> Result<T, String> {
417 T::arg_from_mut_ctx(ctx, &self.raw)
418 }
419}
420
421pub trait ArgFromContext: Sized {
423 fn arg_from_ctx(
424 ctx: &ChainContext,
425 raw: impl AsRef<str>,
426 ) -> Result<Self, String>;
427}
428
429pub trait ArgFromMutContext: Sized {
432 fn arg_from_mut_ctx(
433 ctx: &mut ChainContext,
434 raw: impl AsRef<str>,
435 ) -> Result<Self, String>;
436}
437
438impl ArgFromContext for Address {
439 fn arg_from_ctx(
440 ctx: &ChainContext,
441 raw: impl AsRef<str>,
442 ) -> Result<Self, String> {
443 let raw = raw.as_ref();
444 FromStr::from_str(raw)
446 .or_else(|_| {
448 if raw.len() == 42 && raw.starts_with("0x") {
449 {
450 raw.parse::<EthAddress>()
451 .map(|addr| {
452 Address::Internal(InternalAddress::Erc20(addr))
453 })
454 .map_err(|_| SkipErr)
455 }
456 } else {
457 Err(SkipErr)
458 }
459 })
460 .or_else(|_| {
462 is_ibc_denom(raw)
463 .map(|(trace_path, base_denom)| {
464 let base_token = ctx
465 .wallet
466 .find_address(&base_denom)
467 .filter(|addr| **addr == ctx.native_token)
468 .map(|addr| addr.to_string())
469 .unwrap_or(base_denom);
470 let ibc_denom = format!("{trace_path}/{base_token}");
471 ibc_token(ibc_denom)
472 })
473 .ok_or(SkipErr)
474 })
475 .or_else(|_| {
476 is_nft_trace(raw)
477 .map(|(_, _, _)| ibc_token(raw))
478 .ok_or(SkipErr)
479 })
480 .or_else(|_| {
482 ctx.wallet
483 .find_address(raw)
484 .map(|x| x.into_owned())
485 .ok_or(SkipErr)
486 })
487 .map_err(|_| format!("Unknown address {raw}"))
488 }
489}
490
491impl ArgFromContext for AddrOrNativeToken {
492 fn arg_from_ctx(
493 ctx: &ChainContext,
494 raw: impl AsRef<str>,
495 ) -> Result<Self, String> {
496 if let Ok(addr) = Address::arg_from_ctx(ctx, raw) {
497 Ok(Self(addr))
498 } else {
499 Ok(Self(ctx.native_token.clone()))
500 }
501 }
502}
503
504impl ArgFromContext for tendermint_rpc::Url {
505 fn arg_from_ctx(
506 ctx: &ChainContext,
507 raw: impl AsRef<str>,
508 ) -> Result<Self, String> {
509 if raw.as_ref().is_empty() {
510 return Self::from_str(
511 &ctx.config
512 .ledger
513 .cometbft
514 .rpc
515 .laddr
516 .to_string()
517 .replace("tcp", "http"),
518 )
519 .map_err(|err| format!("Invalid Tendermint address: {err}"));
520 }
521 Self::from_str(raw.as_ref())
522 .map_err(|err| format!("Invalid Tendermint address: {err}"))
523 }
524}
525
526impl ArgFromMutContext for common::SecretKey {
527 fn arg_from_mut_ctx(
528 ctx: &mut ChainContext,
529 raw: impl AsRef<str>,
530 ) -> Result<Self, String> {
531 let raw = raw.as_ref();
532 FromStr::from_str(raw).or_else(|_parse_err| {
534 ctx.wallet
536 .find_secret_key(raw, None)
537 .map_err(|_find_err| format!("Unknown key {}", raw))
538 })
539 }
540}
541
542impl ArgFromContext for common::PublicKey {
543 fn arg_from_ctx(
544 ctx: &ChainContext,
545 raw: impl AsRef<str>,
546 ) -> Result<Self, String> {
547 let raw = raw.as_ref();
548 FromStr::from_str(raw)
550 .map_err(|_| SkipErr)
551 .or_else(|SkipErr| {
553 FromStr::from_str(raw).map_err(|_| SkipErr).and_then(
554 |pkh: PublicKeyHash| {
555 ctx.wallet
556 .find_public_key_by_pkh(&pkh)
557 .map_err(|_| SkipErr)
558 },
559 )
560 })
561 .or_else(|SkipErr| {
563 ctx.wallet.find_public_key(raw).map_err(|_| SkipErr)
564 })
565 .or_else(|SkipErr| {
567 let Address::Implicit(implicit_addr) =
568 Address::decode(raw).map_err(|_| SkipErr)?
569 else {
570 return Err(SkipErr);
571 };
572 ctx.wallet
573 .find_public_key_from_implicit_addr(&implicit_addr)
574 .map_err(|_| SkipErr)
575 })
576 .map_err(|SkipErr| {
577 format!("Couldn't look-up public key associated with {raw:?}")
578 })
579 }
580}
581
582impl ArgFromMutContext for ExtendedSpendingKey {
583 fn arg_from_mut_ctx(
584 ctx: &mut ChainContext,
585 raw: impl AsRef<str>,
586 ) -> Result<Self, String> {
587 let raw = raw.as_ref();
588 FromStr::from_str(raw).or_else(|_parse_err| {
590 ctx.wallet
592 .find_spending_key(raw, None)
593 .map_err(|_find_err| format!("Unknown spending key {}", raw))
594 })
595 }
596}
597
598impl ArgFromMutContext for PseudoExtendedKey {
599 fn arg_from_mut_ctx(
600 ctx: &mut ChainContext,
601 raw: impl AsRef<str>,
602 ) -> Result<Self, String> {
603 let raw = raw.as_ref();
604 ExtendedSpendingKey::from_str(raw)
606 .map(|x| PseudoExtendedKey::from(MaspExtendedSpendingKey::from(x)))
607 .or_else(|_parse_err| {
608 ExtendedViewingKey::from_str(raw).map(|x| {
609 PseudoExtendedKey::from(MaspExtendedViewingKey::from(x))
610 })
611 })
612 .or_else(|_parse_err| {
613 ctx.wallet
615 .find_spending_key(raw, None)
616 .map(|k| {
617 PseudoExtendedKey::from(MaspExtendedSpendingKey::from(
618 k,
619 ))
620 })
621 .map_err(|_find_err| {
622 format!("Unknown spending key {}", raw)
623 })
624 })
625 .or_else(|_parse_err| {
626 ctx.wallet
628 .find_viewing_key(raw)
629 .copied()
630 .map(|k| {
631 PseudoExtendedKey::from(MaspExtendedViewingKey::from(k))
632 })
633 .map_err(|_find_err| format!("Unknown viewing key {}", raw))
634 })
635 }
636}
637
638impl ArgFromMutContext for DatedSpendingKey {
639 fn arg_from_mut_ctx(
640 ctx: &mut ChainContext,
641 raw: impl AsRef<str>,
642 ) -> Result<Self, String> {
643 let raw = raw.as_ref();
644 FromStr::from_str(raw).or_else(|_parse_err| {
646 let sk = ctx
648 .wallet
649 .find_spending_key(raw, None)
650 .map_err(|_find_err| format!("Unknown spending key {}", raw))?;
651 let birthday = ctx.wallet.find_birthday(raw);
652 Ok(DatedSpendingKey::new(sk, birthday.copied()))
653 })
654 }
655}
656
657impl ArgFromMutContext for ExtendedViewingKey {
658 fn arg_from_mut_ctx(
659 ctx: &mut ChainContext,
660 raw: impl AsRef<str>,
661 ) -> Result<Self, String> {
662 let raw = raw.as_ref();
663 FromStr::from_str(raw).or_else(|_parse_err| {
665 ctx.wallet
667 .find_viewing_key(raw)
668 .copied()
669 .map_err(|_find_err| format!("Unknown viewing key {}", raw))
670 })
671 }
672}
673
674impl ArgFromMutContext for DatedViewingKey {
675 fn arg_from_mut_ctx(
676 ctx: &mut ChainContext,
677 raw: impl AsRef<str>,
678 ) -> Result<Self, String> {
679 let raw = raw.as_ref();
680 FromStr::from_str(raw).or_else(|_parse_err| {
682 let vk = ctx
684 .wallet
685 .find_viewing_key(raw)
686 .map_err(|_find_err| format!("Unknown viewing key {}", raw))?;
687 let birthday = ctx.wallet.find_birthday(raw);
688 Ok(DatedViewingKey::new(*vk, birthday.copied()))
689 })
690 }
691}
692
693impl ArgFromContext for PaymentAddress {
694 fn arg_from_ctx(
695 ctx: &ChainContext,
696 raw: impl AsRef<str>,
697 ) -> Result<Self, String> {
698 let raw = raw.as_ref();
699 FromStr::from_str(raw).or_else(|_parse_err| {
701 ctx.wallet
703 .find_payment_addr(raw)
704 .cloned()
705 .ok_or_else(|| format!("Unknown payment address {}", raw))
706 })
707 }
708}
709
710impl ArgFromMutContext for TransferSource {
711 fn arg_from_mut_ctx(
712 ctx: &mut ChainContext,
713 raw: impl AsRef<str>,
714 ) -> Result<Self, String> {
715 let raw = raw.as_ref();
716 Address::arg_from_ctx(ctx, raw)
718 .map(Self::Address)
719 .or_else(|_| {
720 ExtendedSpendingKey::arg_from_mut_ctx(ctx, raw).map(|x| {
721 Self::ExtendedKey(PseudoExtendedKey::from(
722 MaspExtendedSpendingKey::from(x),
723 ))
724 })
725 })
726 .or_else(|_| {
727 ExtendedViewingKey::arg_from_mut_ctx(ctx, raw).map(|x| {
728 Self::ExtendedKey(PseudoExtendedKey::from(
729 MaspExtendedViewingKey::from(x),
730 ))
731 })
732 })
733 }
734}
735
736impl ArgFromContext for TransferTarget {
737 fn arg_from_ctx(
738 ctx: &ChainContext,
739 raw: impl AsRef<str>,
740 ) -> Result<Self, String> {
741 let raw = raw.as_ref();
742 Address::arg_from_ctx(ctx, raw)
744 .map(Self::Address)
745 .or_else(|_| {
746 PaymentAddress::arg_from_ctx(ctx, raw).map(Self::PaymentAddress)
747 })
748 }
749}
750
751impl ArgFromMutContext for BalanceOwner {
752 fn arg_from_mut_ctx(
753 ctx: &mut ChainContext,
754 raw: impl AsRef<str>,
755 ) -> Result<Self, String> {
756 let raw = raw.as_ref();
757 Address::arg_from_ctx(ctx, raw)
759 .map(Self::Address)
760 .or_else(|_| {
761 ExtendedViewingKey::arg_from_mut_ctx(ctx, raw)
762 .map(Self::FullViewingKey)
763 })
764 .map_err(|_| {
765 format!(
766 "Could not find {raw} in the wallet, nor parse it as a \
767 transparent address or as a MASP viewing key"
768 )
769 })
770 }
771}