namada_apps_lib/cli/
context.rs

1//! CLI input types can be used for command arguments
2
3use 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
36/// Skip errors encountered while parsing raw string values.
37struct SkipErr;
38
39/// Env. var to set wasm directory
40pub const ENV_VAR_WASM_DIR: &str = "NAMADA_WASM_DIR";
41
42/// Env. var to read the Namada chain id from
43pub const ENV_VAR_CHAIN_ID: &str = "NAMADA_CHAIN_ID";
44
45/// A raw address (bech32m encoding) or an alias of an address that may be found
46/// in the wallet
47pub type WalletAddress = FromContext<Address>;
48
49/// A raw address (bech32m encoding) or an alias of an address that may be found
50/// in the wallet. Defaults to the native token address.
51pub type WalletAddrOrNativeToken = FromContext<AddrOrNativeToken>;
52
53/// A raw extended spending key (bech32m encoding) or an alias of an extended
54/// spending key in the wallet
55pub type WalletSpendingKey = FromContext<PseudoExtendedKey>;
56
57/// A raw dated extended spending key (bech32m encoding) or an alias of an
58/// extended spending key in the wallet
59pub type WalletDatedSpendingKey = FromContext<DatedSpendingKey>;
60
61/// A raw payment address (bech32m encoding) or an alias of a payment address
62/// in the wallet
63pub type WalletPaymentAddr = FromContext<PaymentAddress>;
64
65/// A raw full viewing key (bech32m encoding) or an alias of a full viewing key
66/// in the wallet
67pub type WalletViewingKey = FromContext<ExtendedViewingKey>;
68
69/// A raw full dated viewing key (bech32m encoding) or an alias of a full
70/// viewing key in the wallet
71pub type WalletDatedViewingKey = FromContext<DatedViewingKey>;
72
73/// A raw address or a raw extended spending key (bech32m encoding) or an alias
74/// of either in the wallet
75pub type WalletTransferSource = FromContext<TransferSource>;
76
77/// A raw address or a raw payment address (bech32m encoding) or an alias of
78/// either in the wallet
79pub type WalletTransferTarget = FromContext<TransferTarget>;
80
81/// A raw keypair (hex encoding), an alias, a public key or a public key hash of
82/// a keypair that may be found in the wallet
83pub type WalletKeypair = FromContext<common::SecretKey>;
84
85/// A raw public key (hex encoding), a public key hash (also hex encoding) or an
86/// alias of an public key that may be found in the wallet
87pub type WalletPublicKey = FromContext<common::PublicKey>;
88
89/// A raw address or a raw full viewing key (bech32m encoding) or an alias of
90/// either in the wallet
91pub type WalletBalanceOwner = FromContext<BalanceOwner>;
92
93/// RPC address of a locally configured node
94pub type ConfigRpcAddress = FromContext<tendermint_rpc::Url>;
95
96/// Address that defaults to the native token address.
97#[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/// Command execution context
122#[derive(Debug)]
123pub struct Context {
124    /// Global arguments
125    pub global_args: args::Global,
126    /// The global configuration
127    pub global_config: GlobalConfig,
128    /// Chain-specific context, if any chain is configured in `global_config`
129    pub chain: Option<ChainContext>,
130}
131
132/// Command execution context with chain-specific data
133#[derive(Debug)]
134pub struct ChainContext {
135    /// The wallet
136    pub wallet: Wallet<CliWalletUtils>,
137    /// The ledger configuration for a specific chain ID
138    pub config: Config,
139    /// The context fr shielded operations
140    pub shielded: ShieldedWallet<FsShieldedUtils>,
141    /// Native token's address
142    pub native_token: Address,
143}
144
145/// Convenience function wrapping over [`wasm_dir_from_env_or`].
146pub 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
152/// Return the wasm artifacts path in use.
153pub 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                // Put WASM dir path in the config
195                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    /// Try to take the chain context, or exit the process with an error if no
218    /// chain is configured.
219    pub fn take_chain_or_exit(self) -> ChainContext {
220        self.chain
221            .unwrap_or_else(|| safe_exit_on_missing_chain_context())
222    }
223
224    /// Try to borrow chain context, or exit the process with an error if no
225    /// chain is configured.
226    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    /// Try to borrow mutably chain context, or exit the process with an error
233    /// if no chain is configured.
234    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    /// Make an implementation of Namada from this object and parameters.
241    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    /// Parse and/or look-up the value from the context.
272    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    /// Try to parse and/or look-up an optional value from the context.
280    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    /// Parse and/or look-up the value from the context with cache.
290    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    /// Try to parse and/or look-up an optional value from the context with
298    /// cache.
299    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    /// Get the wasm directory configured for the chain.
312    ///
313    /// Note that in "dev" build, this may be the root `wasm` dir.
314    pub fn wasm_dir(&self) -> PathBuf {
315        self.config.ledger.chain_dir().join(&self.config.wasm_dir)
316    }
317
318    /// Read the given WASM file from the WASM directory or an absolute path.
319    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
324/// Load global config from expected path in the `base_dir` or try to generate a
325/// new one without a chain if it doesn't exist.
326pub 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/// Argument that can be given raw or found in the [`Context`].
336#[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    /// Converts this TransferSource argument to an Address. Call this function
362    /// only when certain that raw represents an Address.
363    pub fn to_address(&self) -> FromContext<Address> {
364        FromContext::<Address> {
365            raw: self.raw.clone(),
366            phantom: PhantomData,
367        }
368    }
369
370    /// Converts this TransferSource argument to an ExtendedSpendingKey. Call
371    /// this function only when certain that raw represents an
372    /// ExtendedSpendingKey.
373    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    /// Converts this TransferTarget argument to an Address. Call this function
383    /// only when certain that raw represents an Address.
384    pub fn to_address(&self) -> FromContext<Address> {
385        FromContext::<Address> {
386            raw: self.raw.clone(),
387            phantom: PhantomData,
388        }
389    }
390
391    /// Converts this TransferTarget argument to a PaymentAddress. Call this
392    /// function only when certain that raw represents a PaymentAddress.
393    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    /// Parse and/or look-up the value from the chain context.
406    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    /// Parse and/or look-up the value from the mutable chain context.
416    fn arg_from_mut_ctx(&self, ctx: &mut ChainContext) -> Result<T, String> {
417        T::arg_from_mut_ctx(ctx, &self.raw)
418    }
419}
420
421/// CLI argument that found via the [`ChainContext`].
422pub trait ArgFromContext: Sized {
423    fn arg_from_ctx(
424        ctx: &ChainContext,
425        raw: impl AsRef<str>,
426    ) -> Result<Self, String>;
427}
428
429/// CLI argument that found via the [`ChainContext`] and cached (as in case of
430/// an encrypted keypair that has been decrypted), hence using mutable context.
431pub 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        // An address can be either raw (bech32m encoding)
445        FromStr::from_str(raw)
446            // An Ethereum address
447            .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            // An IBC token
461            .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 it can be an alias that may be found in the wallet
481            .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        // A keypair can be either a raw keypair in hex string
533        FromStr::from_str(raw).or_else(|_parse_err| {
534            // Or it can be an alias
535            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        // A public key can either be a bech32 encoded (tpknam1...) string
549        FromStr::from_str(raw)
550            .map_err(|_| SkipErr)
551            // Or it can be a hex encoded public key hash
552            .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 it can be an alias that may be found in the wallet
562            .or_else(|SkipErr| {
563                ctx.wallet.find_public_key(raw).map_err(|_| SkipErr)
564            })
565            // Or it can be an implicit address
566            .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        // Either the string is a raw extended spending key
589        FromStr::from_str(raw).or_else(|_parse_err| {
590            // Or it is a stored alias of one
591            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        // Either the string is a raw extended spending key
605        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                // Or it is a stored alias of one
614                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                // Or it is a stored alias of one
627                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        // Either the string is a raw extended spending key
645        FromStr::from_str(raw).or_else(|_parse_err| {
646            // Or it is a stored alias of one
647            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        // Either the string is a raw full viewing key
664        FromStr::from_str(raw).or_else(|_parse_err| {
665            // Or it is a stored alias of one
666            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        // Either the string is a raw full viewing key
681        FromStr::from_str(raw).or_else(|_parse_err| {
682            // Or it is a stored alias of one
683            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        // Either the string is a payment address
700        FromStr::from_str(raw).or_else(|_parse_err| {
701            // Or it is a stored alias of one
702            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        // Either the string is a transparent address or a spending key
717        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        // Either the string is a transparent address or a payment address
743        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        // Either the string is a transparent address or a viewing key
758        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}