diff --git a/.changelog/unreleased/features/870-pre-genesis-wallet.md b/.changelog/unreleased/features/870-pre-genesis-wallet.md new file mode 100644 index 00000000000..b91d8b0c4ac --- /dev/null +++ b/.changelog/unreleased/features/870-pre-genesis-wallet.md @@ -0,0 +1,6 @@ +- Added `--pre-genesis` argument to the wallet commands to allow to generate + keys, implicit addresses and shielded keys without having a chain setup. If + no chain is setup yet (i.e. there's no base-dir `.namada` or it's empty), the + wallet defaults to use the pre-genesis wallet even without the `--pre-genesis` + flag. The pre-genesis wallet is located in `.namada/pre-genesis/wallet.toml`. + ([#870](https://github.com/anoma/namada/pull/870)) diff --git a/.changelog/unreleased/improvements/849-human-readable-string-encoding.md b/.changelog/unreleased/improvements/849-human-readable-string-encoding.md new file mode 100644 index 00000000000..18efcca7940 --- /dev/null +++ b/.changelog/unreleased/improvements/849-human-readable-string-encoding.md @@ -0,0 +1,2 @@ +- Added bech32m string encoding for `common::PublicKey` and `DkgPublicKey`. + ([#849](https://github.com/anoma/namada/pull/849)) diff --git a/apps/src/bin/namada-node/cli.rs b/apps/src/bin/namada-node/cli.rs index 0f5305e4d4c..e2c11cf7ab2 100644 --- a/apps/src/bin/namada-node/cli.rs +++ b/apps/src/bin/namada-node/cli.rs @@ -8,12 +8,15 @@ use namada_apps::node::ledger; pub fn main() -> Result<()> { let (cmd, mut ctx) = cli::namada_node_cli()?; if let Some(mode) = ctx.global_args.mode.clone() { - ctx.config.ledger.tendermint.tendermint_mode = mode; + if let Some(chain) = ctx.chain.as_mut() { + chain.config.ledger.tendermint.tendermint_mode = mode; + } } match cmd { cmds::NamadaNode::Ledger(sub) => match sub { cmds::Ledger::Run(cmds::LedgerRun(args)) => { - let wasm_dir = ctx.wasm_dir(); + let chain_ctx = ctx.take_chain_or_exit(); + let wasm_dir = chain_ctx.wasm_dir(); // Sleep until start time if needed if let Some(time) = args.0 { @@ -31,14 +34,16 @@ pub fn main() -> Result<()> { } } } - ledger::run(ctx.config.ledger, wasm_dir); + ledger::run(chain_ctx.config.ledger, wasm_dir); } cmds::Ledger::Reset(_) => { - ledger::reset(ctx.config.ledger) + let chain_ctx = ctx.take_chain_or_exit(); + ledger::reset(chain_ctx.config.ledger) .wrap_err("Failed to reset Namada node")?; } cmds::Ledger::DumpDb(cmds::LedgerDumpDb(args)) => { - ledger::dump_db(ctx.config.ledger, args); + let chain_ctx = ctx.take_chain_or_exit(); + ledger::dump_db(chain_ctx.config.ledger, args); } }, cmds::NamadaNode::Config(sub) => match sub { @@ -47,18 +52,18 @@ pub fn main() -> Result<()> { // In here, we just need to overwrite the default chain ID, in // case it's been already set to a different value if let Some(chain_id) = ctx.global_args.chain_id.as_ref() { - ctx.global_config.default_chain_id = chain_id.clone(); + ctx.global_config.default_chain_id = Some(chain_id.clone()); ctx.global_config .write(&ctx.global_args.base_dir) .unwrap_or_else(|err| { - eprintln!("Error writing global config: {}", err); + eprintln!("Error writing global config: {err}"); cli::safe_exit(1) }); + tracing::debug!( + "Generated config and set default chain ID to \ + {chain_id}" + ); } - tracing::debug!( - "Generated config and set default chain ID to {}", - &ctx.global_config.default_chain_id - ); } }, } diff --git a/apps/src/bin/namada-wallet/cli.rs b/apps/src/bin/namada-wallet/cli.rs index 82a994b0aca..01c3d18c0ae 100644 --- a/apps/src/bin/namada-wallet/cli.rs +++ b/apps/src/bin/namada-wallet/cli.rs @@ -12,7 +12,8 @@ use namada::types::masp::{MaspValue, PaymentAddress}; use namada_apps::cli; use namada_apps::cli::{args, cmds, Context}; use namada_apps::client::tx::find_valid_diversifier; -use namada_apps::wallet::{DecryptionError, FindKeyError}; +use namada_apps::client::utils::PRE_GENESIS_DIR; +use namada_apps::wallet::{DecryptionError, FindKeyError, Wallet}; use rand_core::OsRng; pub fn main() -> Result<()> { @@ -35,7 +36,9 @@ pub fn main() -> Result<()> { cmds::WalletAddress::Find(cmds::AddressOrAliasFind(args)) => { address_or_alias_find(ctx, args) } - cmds::WalletAddress::List(cmds::AddressList) => address_list(ctx), + cmds::WalletAddress::List(cmds::AddressList(args)) => { + address_list(ctx, args) + } cmds::WalletAddress::Add(cmds::AddressAdd(args)) => { address_add(ctx, args) } @@ -50,8 +53,8 @@ pub fn main() -> Result<()> { cmds::WalletMasp::AddAddrKey(cmds::MaspAddAddrKey(args)) => { address_key_add(ctx, args) } - cmds::WalletMasp::ListPayAddrs(cmds::MaspListPayAddrs) => { - payment_addresses_list(ctx) + cmds::WalletMasp::ListPayAddrs(cmds::MaspListPayAddrs(args)) => { + payment_addresses_list(ctx, args) } cmds::WalletMasp::ListKeys(cmds::MaspListKeys(args)) => { spending_keys_list(ctx, args) @@ -70,9 +73,10 @@ fn address_key_find( args::AddrKeyFind { alias, unsafe_show_secret, + is_pre_genesis, }: args::AddrKeyFind, ) { - let mut wallet = ctx.wallet; + let mut wallet = load_wallet(ctx, is_pre_genesis); let alias = alias.to_lowercase(); if let Ok(viewing_key) = wallet.find_viewing_key(&alias) { // Check if alias is a viewing key @@ -105,9 +109,10 @@ fn spending_keys_list( args::MaspKeysList { decrypt, unsafe_show_secret, + is_pre_genesis, }: args::MaspKeysList, ) { - let wallet = ctx.wallet; + let wallet = load_wallet(ctx, is_pre_genesis); let known_view_keys = wallet.get_viewing_keys(); let known_spend_keys = wallet.get_spending_keys(); if known_view_keys.is_empty() { @@ -171,8 +176,11 @@ fn spending_keys_list( } /// List payment addresses. -fn payment_addresses_list(ctx: Context) { - let wallet = ctx.wallet; +fn payment_addresses_list( + ctx: Context, + args::MaspListPayAddrs { is_pre_genesis }: args::MaspListPayAddrs, +) { + let wallet = load_wallet(ctx, is_pre_genesis); let known_addresses = wallet.get_payment_addrs(); if known_addresses.is_empty() { println!( @@ -194,10 +202,11 @@ fn spending_key_gen( ctx: Context, args::MaspSpendKeyGen { alias, + is_pre_genesis, unsafe_dont_encrypt, }: args::MaspSpendKeyGen, ) { - let mut wallet = ctx.wallet; + let mut wallet = load_wallet(ctx, is_pre_genesis); let alias = alias.to_lowercase(); let (alias, _key) = wallet.gen_spending_key(alias, unsafe_dont_encrypt); wallet.save().unwrap_or_else(|err| eprintln!("{}", err)); @@ -209,23 +218,28 @@ fn spending_key_gen( /// Generate a shielded payment address from the given key. fn payment_address_gen( - mut ctx: Context, + ctx: Context, args::MaspPayAddrGen { alias, viewing_key, pin, + is_pre_genesis, }: args::MaspPayAddrGen, ) { + let mut wallet = load_wallet(ctx, is_pre_genesis); let alias = alias.to_lowercase(); - let viewing_key = - ExtendedFullViewingKey::from(ctx.get_cached(&viewing_key)) - .fvk - .vk; + let viewing_key = wallet + .find_viewing_key(&viewing_key.raw) + .map(Clone::clone) + .unwrap_or_else(|_| { + eprintln!("Unknown viewing key {}", viewing_key.raw); + cli::safe_exit(1) + }); + let viewing_key = ExtendedFullViewingKey::from(viewing_key).fvk.vk; let (div, _g_d) = find_valid_diversifier(&mut OsRng); let payment_addr = viewing_key .to_payment_address(div) .expect("a PaymentAddress"); - let mut wallet = ctx.wallet; let alias = wallet .insert_payment_addr( alias, @@ -244,18 +258,19 @@ fn payment_address_gen( /// Add a viewing key, spending key, or payment address to wallet. fn address_key_add( - mut ctx: Context, + ctx: Context, args::MaspAddrKeyAdd { alias, value, + is_pre_genesis, unsafe_dont_encrypt, }: args::MaspAddrKeyAdd, ) { let alias = alias.to_lowercase(); + let mut wallet = load_wallet(ctx, is_pre_genesis); let (alias, typ) = match value { MaspValue::FullViewingKey(viewing_key) => { - let alias = ctx - .wallet + let alias = wallet .insert_viewing_key(alias, viewing_key) .unwrap_or_else(|| { eprintln!("Viewing key not added"); @@ -264,8 +279,7 @@ fn address_key_add( (alias, "viewing key") } MaspValue::ExtendedSpendingKey(spending_key) => { - let alias = ctx - .wallet + let alias = wallet .encrypt_insert_spending_key( alias, spending_key, @@ -278,8 +292,7 @@ fn address_key_add( (alias, "spending key") } MaspValue::PaymentAddress(payment_addr) => { - let alias = ctx - .wallet + let alias = wallet .insert_payment_addr(alias, payment_addr) .unwrap_or_else(|| { eprintln!("Payment address not added"); @@ -288,7 +301,7 @@ fn address_key_add( (alias, "payment address") } }; - ctx.wallet.save().unwrap_or_else(|err| eprintln!("{}", err)); + wallet.save().unwrap_or_else(|err| eprintln!("{}", err)); println!( "Successfully added a {} with the following alias to wallet: {}", typ, alias, @@ -302,10 +315,11 @@ fn key_and_address_gen( args::KeyAndAddressGen { scheme, alias, + is_pre_genesis, unsafe_dont_encrypt, }: args::KeyAndAddressGen, ) { - let mut wallet = ctx.wallet; + let mut wallet = load_wallet(ctx, is_pre_genesis); let (alias, _key) = wallet.gen_key(scheme, alias, unsafe_dont_encrypt); wallet.save().unwrap_or_else(|err| eprintln!("{}", err)); println!( @@ -321,10 +335,11 @@ fn key_find( public_key, alias, value, + is_pre_genesis, unsafe_show_secret, }: args::KeyFind, ) { - let mut wallet = ctx.wallet; + let mut wallet = load_wallet(ctx, is_pre_genesis); let found_keypair = match public_key { Some(pk) => wallet.find_key_by_pk(&pk), None => { @@ -361,10 +376,11 @@ fn key_list( ctx: Context, args::KeyList { decrypt, + is_pre_genesis, unsafe_show_secret, }: args::KeyList, ) { - let wallet = ctx.wallet; + let wallet = load_wallet(ctx, is_pre_genesis); let known_keys = wallet.get_keys(); if known_keys.is_empty() { println!( @@ -406,8 +422,14 @@ fn key_list( } /// Export a keypair to a file. -fn key_export(ctx: Context, args::KeyExport { alias }: args::KeyExport) { - let mut wallet = ctx.wallet; +fn key_export( + ctx: Context, + args::KeyExport { + alias, + is_pre_genesis, + }: args::KeyExport, +) { + let mut wallet = load_wallet(ctx, is_pre_genesis); wallet .find_key(alias.to_lowercase()) .map(|keypair| { @@ -427,8 +449,11 @@ fn key_export(ctx: Context, args::KeyExport { alias }: args::KeyExport) { } /// List all known addresses. -fn address_list(ctx: Context) { - let wallet = ctx.wallet; +fn address_list( + ctx: Context, + args::AddressList { is_pre_genesis }: args::AddressList, +) { + let wallet = load_wallet(ctx, is_pre_genesis); let known_addresses = wallet.get_addresses(); if known_addresses.is_empty() { println!( @@ -447,50 +472,71 @@ fn address_list(ctx: Context) { } /// Find address (alias) by its alias (address). -fn address_or_alias_find(ctx: Context, args: args::AddressOrAliasFind) { - let wallet = ctx.wallet; - if args.address.is_some() && args.alias.is_some() { +fn address_or_alias_find( + ctx: Context, + args::AddressOrAliasFind { + alias, + address, + is_pre_genesis, + }: args::AddressOrAliasFind, +) { + let wallet = load_wallet(ctx, is_pre_genesis); + if address.is_some() && alias.is_some() { panic!( "This should not be happening: clap should emit its own error \ message." ); - } else if args.alias.is_some() { - if let Some(address) = wallet.find_address(args.alias.as_ref().unwrap()) - { + } else if alias.is_some() { + if let Some(address) = wallet.find_address(alias.as_ref().unwrap()) { println!("Found address {}", address.to_pretty_string()); } else { println!( "No address with alias {} found. Use the command `address \ list` to see all the known addresses.", - args.alias.unwrap().to_lowercase() + alias.unwrap().to_lowercase() ); } - } else if args.address.is_some() { - if let Some(alias) = wallet.find_alias(args.address.as_ref().unwrap()) { + } else if address.is_some() { + if let Some(alias) = wallet.find_alias(address.as_ref().unwrap()) { println!("Found alias {}", alias); } else { println!( "No alias with address {} found. Use the command `address \ list` to see all the known addresses.", - args.address.unwrap() + address.unwrap() ); } } } /// Add an address to the wallet. -fn address_add(ctx: Context, args: args::AddressAdd) { - let mut wallet = ctx.wallet; - if wallet - .add_address(args.alias.clone().to_lowercase(), args.address) - .is_none() - { +fn address_add( + ctx: Context, + args::AddressAdd { + alias, + address, + is_pre_genesis, + }: args::AddressAdd, +) { + let mut wallet = load_wallet(ctx, is_pre_genesis); + if wallet.add_address(alias.to_lowercase(), address).is_none() { eprintln!("Address not added"); cli::safe_exit(1); } wallet.save().unwrap_or_else(|err| eprintln!("{}", err)); println!( "Successfully added a key and an address with alias: \"{}\"", - args.alias.to_lowercase() + alias.to_lowercase() ); } + +/// Load wallet for chain when `ctx.chain.is_some()` or pre-genesis wallet when +/// `is_pre_genesis || ctx.chain.is_none()`. +fn load_wallet(ctx: Context, is_pre_genesis: bool) -> Wallet { + if is_pre_genesis || ctx.chain.is_none() { + let wallet_path = ctx.global_args.base_dir.join(PRE_GENESIS_DIR); + Wallet::load_or_new(&wallet_path) + } else { + ctx.take_chain_or_exit().wallet + } +} diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 9c23cadb468..5466bfcb229 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -558,7 +558,7 @@ pub mod cmds { /// List all known payment addresses #[derive(Clone, Debug)] - pub struct MaspListPayAddrs; + pub struct MaspListPayAddrs(pub args::MaspListPayAddrs); impl SubCmd for MaspListPayAddrs { const CMD: &'static str = "list-addrs"; @@ -566,12 +566,13 @@ pub mod cmds { fn parse(matches: &ArgMatches) -> Option { matches .subcommand_matches(Self::CMD) - .map(|_matches| MaspListPayAddrs) + .map(|matches| Self(args::MaspListPayAddrs::parse(matches))) } fn def() -> App { App::new(Self::CMD) .about("Lists all payment addresses in the wallet") + .add_args::() } } @@ -720,7 +721,7 @@ pub mod cmds { /// List known addresses #[derive(Clone, Debug)] - pub struct AddressList; + pub struct AddressList(pub args::AddressList); impl SubCmd for AddressList { const CMD: &'static str = "list"; @@ -728,11 +729,13 @@ pub mod cmds { fn parse(matches: &ArgMatches) -> Option { matches .subcommand_matches(Self::CMD) - .map(|_matches| AddressList) + .map(|matches| Self(args::AddressList::parse(matches))) } fn def() -> App { - App::new(Self::CMD).about("List all known addresses.") + App::new(Self::CMD) + .about("List all known addresses.") + .add_args::() } } @@ -1657,6 +1660,7 @@ pub mod args { "port-id", DefaultFn(|| PortId::from_str("transfer").unwrap()), ); + const PRE_GENESIS: ArgFlag = flag("pre-genesis"); const PROPOSAL_OFFLINE: ArgFlag = flag("offline"); const PROTOCOL_KEY: ArgOpt = arg_opt("protocol-key"); const PRE_GENESIS_PATH: ArgOpt = arg_opt("pre-genesis-path"); @@ -1879,7 +1883,7 @@ pub mod args { impl TxTransfer { pub fn parse_from_context( &self, - ctx: &mut Context, + ctx: &mut ChainContext, ) -> ParsedTxTransferArgs { ParsedTxTransferArgs { tx: self.tx.parse_from_context(ctx), @@ -2870,7 +2874,10 @@ pub mod args { } impl Tx { - pub fn parse_from_context(&self, ctx: &mut Context) -> ParsedTxArgs { + pub fn parse_from_context( + &self, + ctx: &mut ChainContext, + ) -> ParsedTxArgs { ParsedTxArgs { dry_run: self.dry_run, dump_tx: self.dump_tx, @@ -2998,6 +3005,7 @@ pub mod args { pub alias: String, /// Any MASP value pub value: MaspValue, + pub is_pre_genesis: bool, /// Don't encrypt the keypair pub unsafe_dont_encrypt: bool, } @@ -3006,10 +3014,12 @@ pub mod args { fn parse(matches: &ArgMatches) -> Self { let alias = ALIAS.parse(matches); let value = MASP_VALUE.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); let unsafe_dont_encrypt = UNSAFE_DONT_ENCRYPT.parse(matches); Self { alias, value, + is_pre_genesis, unsafe_dont_encrypt, } } @@ -3025,6 +3035,10 @@ pub mod args { .def() .about("A spending key, viewing key, or payment address."), ) + .arg(PRE_GENESIS.def().about( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) .arg(UNSAFE_DONT_ENCRYPT.def().about( "UNSAFE: Do not encrypt the keypair. Do not use this for keys \ used in a live network.", @@ -3037,6 +3051,7 @@ pub mod args { pub struct MaspSpendKeyGen { /// Key alias pub alias: String, + pub is_pre_genesis: bool, /// Don't encrypt the keypair pub unsafe_dont_encrypt: bool, } @@ -3044,9 +3059,11 @@ pub mod args { impl Args for MaspSpendKeyGen { fn parse(matches: &ArgMatches) -> Self { let alias = ALIAS.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); let unsafe_dont_encrypt = UNSAFE_DONT_ENCRYPT.parse(matches); Self { alias, + is_pre_genesis, unsafe_dont_encrypt, } } @@ -3057,6 +3074,10 @@ pub mod args { .def() .about("An alias to be associated with the spending key."), ) + .arg(PRE_GENESIS.def().about( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) .arg(UNSAFE_DONT_ENCRYPT.def().about( "UNSAFE: Do not encrypt the keypair. Do not use this for keys \ used in a live network.", @@ -3073,6 +3094,7 @@ pub mod args { pub viewing_key: WalletViewingKey, /// Pin pub pin: bool, + pub is_pre_genesis: bool, } impl Args for MaspPayAddrGen { @@ -3080,10 +3102,12 @@ pub mod args { let alias = ALIAS.parse(matches); let viewing_key = VIEWING_KEY.parse(matches); let pin = PIN.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); Self { alias, viewing_key, pin, + is_pre_genesis, } } @@ -3098,6 +3122,10 @@ pub mod args { "Require that the single transaction to this address be \ pinned.", )) + .arg(PRE_GENESIS.def().about( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) } } @@ -3108,6 +3136,8 @@ pub mod args { pub scheme: SchemeType, /// Key alias pub alias: Option, + /// Generate a key for pre-genesis, instead of a current chain + pub is_pre_genesis: bool, /// Don't encrypt the keypair pub unsafe_dont_encrypt: bool, } @@ -3116,10 +3146,12 @@ pub mod args { fn parse(matches: &ArgMatches) -> Self { let scheme = SCHEME.parse(matches); let alias = ALIAS_OPT.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); let unsafe_dont_encrypt = UNSAFE_DONT_ENCRYPT.parse(matches); Self { scheme, alias, + is_pre_genesis, unsafe_dont_encrypt, } } @@ -3134,6 +3166,10 @@ pub mod args { "The key and address alias. If none provided, the alias will \ be the public key hash.", )) + .arg(PRE_GENESIS.def().about( + "Generate a key for pre-genesis, instead of for the current \ + chain, if any.", + )) .arg(UNSAFE_DONT_ENCRYPT.def().about( "UNSAFE: Do not encrypt the keypair. Do not use this for keys \ used in a live network.", @@ -3147,6 +3183,7 @@ pub mod args { pub public_key: Option, pub alias: Option, pub value: Option, + pub is_pre_genesis: bool, pub unsafe_show_secret: bool, } @@ -3155,12 +3192,14 @@ pub mod args { let public_key = RAW_PUBLIC_KEY_OPT.parse(matches); let alias = ALIAS_OPT.parse(matches); let value = VALUE.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); let unsafe_show_secret = UNSAFE_SHOW_SECRET.parse(matches); Self { public_key, alias, value, + is_pre_genesis, unsafe_show_secret, } } @@ -3183,6 +3222,10 @@ pub mod args { "A public key or alias associated with the keypair.", ), ) + .arg(PRE_GENESIS.def().about( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) .arg( UNSAFE_SHOW_SECRET .def() @@ -3196,15 +3239,18 @@ pub mod args { pub struct AddrKeyFind { pub alias: String, pub unsafe_show_secret: bool, + pub is_pre_genesis: bool, } impl Args for AddrKeyFind { fn parse(matches: &ArgMatches) -> Self { let alias = ALIAS.parse(matches); let unsafe_show_secret = UNSAFE_SHOW_SECRET.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); Self { alias, unsafe_show_secret, + is_pre_genesis, } } @@ -3215,6 +3261,10 @@ pub mod args { .def() .about("UNSAFE: Print the spending key values."), ) + .arg(PRE_GENESIS.def().about( + "Use pre-genesis wallet, instead of for the current \ + chain, if any.", + )) } } @@ -3222,21 +3272,28 @@ pub mod args { #[derive(Clone, Debug)] pub struct MaspKeysList { pub decrypt: bool, + pub is_pre_genesis: bool, pub unsafe_show_secret: bool, } impl Args for MaspKeysList { fn parse(matches: &ArgMatches) -> Self { let decrypt = DECRYPT.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); let unsafe_show_secret = UNSAFE_SHOW_SECRET.parse(matches); Self { decrypt, + is_pre_genesis, unsafe_show_secret, } } fn def(app: App) -> App { app.arg(DECRYPT.def().about("Decrypt keys that are encrypted.")) + .arg(PRE_GENESIS.def().about( + "Use pre-genesis wallet, instead of for the current \ + chain, if any.", + )) .arg( UNSAFE_SHOW_SECRET .def() @@ -3245,25 +3302,52 @@ pub mod args { } } + /// Wallet list shielded payment addresses arguments + #[derive(Clone, Debug)] + pub struct MaspListPayAddrs { + pub is_pre_genesis: bool, + } + + impl Args for MaspListPayAddrs { + fn parse(matches: &ArgMatches) -> Self { + let is_pre_genesis = PRE_GENESIS.parse(matches); + Self { is_pre_genesis } + } + + fn def(app: App) -> App { + app.arg(PRE_GENESIS.def().about( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) + } + } + /// Wallet list keys arguments #[derive(Clone, Debug)] pub struct KeyList { pub decrypt: bool, + pub is_pre_genesis: bool, pub unsafe_show_secret: bool, } impl Args for KeyList { fn parse(matches: &ArgMatches) -> Self { let decrypt = DECRYPT.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); let unsafe_show_secret = UNSAFE_SHOW_SECRET.parse(matches); Self { decrypt, + is_pre_genesis, unsafe_show_secret, } } fn def(app: App) -> App { app.arg(DECRYPT.def().about("Decrypt keys that are encrypted.")) + .arg(PRE_GENESIS.def().about( + "Use pre-genesis wallet, instead of for the current \ + chain, if any.", + )) .arg( UNSAFE_SHOW_SECRET .def() @@ -3276,13 +3360,17 @@ pub mod args { #[derive(Clone, Debug)] pub struct KeyExport { pub alias: String, + pub is_pre_genesis: bool, } impl Args for KeyExport { fn parse(matches: &ArgMatches) -> Self { let alias = ALIAS.parse(matches); - - Self { alias } + let is_pre_genesis = PRE_GENESIS.parse(matches); + Self { + alias, + is_pre_genesis, + } } fn def(app: App) -> App { @@ -3291,6 +3379,10 @@ pub mod args { .def() .about("The alias of the key you wish to export."), ) + .arg(PRE_GENESIS.def().about( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) } } @@ -3299,13 +3391,19 @@ pub mod args { pub struct AddressOrAliasFind { pub alias: Option, pub address: Option
, + pub is_pre_genesis: bool, } impl Args for AddressOrAliasFind { fn parse(matches: &ArgMatches) -> Self { let alias = ALIAS_OPT.parse(matches); let address = RAW_ADDRESS_OPT.parse(matches); - Self { alias, address } + let is_pre_genesis = PRE_GENESIS.parse(matches); + Self { + alias, + address, + is_pre_genesis, + } } fn def(app: App) -> App { @@ -3319,6 +3417,10 @@ pub mod args { .def() .about("The bech32m encoded address string."), ) + .arg(PRE_GENESIS.def().about( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) .group( ArgGroup::new("find_flags") .args(&[ALIAS_OPT.name, RAW_ADDRESS_OPT.name]) @@ -3327,18 +3429,44 @@ pub mod args { } } + /// List wallet address + #[derive(Clone, Debug)] + pub struct AddressList { + pub is_pre_genesis: bool, + } + + impl Args for AddressList { + fn parse(matches: &ArgMatches) -> Self { + let is_pre_genesis = PRE_GENESIS.parse(matches); + Self { is_pre_genesis } + } + + fn def(app: App) -> App { + app.arg(PRE_GENESIS.def().about( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) + } + } + /// Wallet address add arguments #[derive(Clone, Debug)] pub struct AddressAdd { pub alias: String, pub address: Address, + pub is_pre_genesis: bool, } impl Args for AddressAdd { fn parse(matches: &ArgMatches) -> Self { let alias = ALIAS.parse(matches); let address = RAW_ADDRESS.parse(matches); - Self { alias, address } + let is_pre_genesis = PRE_GENESIS.parse(matches); + Self { + alias, + address, + is_pre_genesis, + } } fn def(app: App) -> App { @@ -3352,6 +3480,10 @@ pub mod args { .def() .about("The bech32m encoded address string."), ) + .arg(PRE_GENESIS.def().about( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) } } diff --git a/apps/src/lib/cli/context.rs b/apps/src/lib/cli/context.rs index e61fda9dfc0..c9a67fd83d5 100644 --- a/apps/src/lib/cli/context.rs +++ b/apps/src/lib/cli/context.rs @@ -12,6 +12,7 @@ use namada::types::key::*; use namada::types::masp::*; use super::args; +use crate::cli::utils; use crate::client::tx::ShieldedContext; use crate::config::genesis::genesis_config; use crate::config::global::GlobalConfig; @@ -65,10 +66,17 @@ pub type WalletBalanceOwner = FromContext; pub struct Context { /// Global arguments pub global_args: args::Global, - /// The wallet - pub wallet: Wallet, /// The global configuration pub global_config: GlobalConfig, + /// Chain-specific context, if any chain is configured in `global_config` + pub chain: Option, +} + +/// Command execution context with chain-specific data +#[derive(Debug)] +pub struct ChainContext { + /// The wallet + pub wallet: Wallet, /// The ledger configuration for a specific chain ID pub config: Config, /// The context fr shielded operations @@ -80,49 +88,92 @@ pub struct Context { impl Context { pub fn new(global_args: args::Global) -> Result { let global_config = read_or_try_new_global_config(&global_args); - tracing::info!("Chain ID: {}", global_config.default_chain_id); - - let mut config = Config::load( - &global_args.base_dir, - &global_config.default_chain_id, - global_args.mode.clone(), - ); - - let chain_dir = global_args - .base_dir - .join(global_config.default_chain_id.as_str()); - let genesis_file_path = global_args - .base_dir - .join(format!("{}.toml", global_config.default_chain_id.as_str())); - let genesis = genesis_config::read_genesis_config(&genesis_file_path); - let native_token = genesis.native_token; - let default_genesis = - genesis_config::open_genesis_config(genesis_file_path)?; - let wallet = - Wallet::load_or_new_from_genesis(&chain_dir, default_genesis); - - // If the WASM dir specified, put it in the config - match global_args.wasm_dir.as_ref() { - Some(wasm_dir) => { - config.wasm_dir = wasm_dir.clone(); - } - None => { - if let Ok(wasm_dir) = env::var(ENV_VAR_WASM_DIR) { - let wasm_dir: PathBuf = wasm_dir.into(); - config.wasm_dir = wasm_dir; + + let chain = match global_config.default_chain_id.as_ref() { + Some(default_chain_id) => { + tracing::info!("Default chain ID: {default_chain_id}"); + let mut config = Config::load( + &global_args.base_dir, + default_chain_id, + global_args.mode.clone(), + ); + let chain_dir = + global_args.base_dir.join(default_chain_id.as_str()); + let genesis_file_path = global_args + .base_dir + .join(format!("{}.toml", default_chain_id.as_str())); + let genesis = + genesis_config::read_genesis_config(&genesis_file_path); + let native_token = genesis.native_token; + let default_genesis = + genesis_config::open_genesis_config(genesis_file_path)?; + let wallet = Wallet::load_or_new_from_genesis( + &chain_dir, + default_genesis, + ); + + // If the WASM dir specified, put it in the config + match global_args.wasm_dir.as_ref() { + Some(wasm_dir) => { + config.wasm_dir = wasm_dir.clone(); + } + None => { + if let Ok(wasm_dir) = env::var(ENV_VAR_WASM_DIR) { + let wasm_dir: PathBuf = wasm_dir.into(); + config.wasm_dir = wasm_dir; + } + } } + Some(ChainContext { + wallet, + config, + shielded: ShieldedContext::new(chain_dir), + native_token, + }) } - } + None => None, + }; + Ok(Self { global_args, - wallet, global_config, - config, - shielded: ShieldedContext::new(chain_dir), - native_token, + chain, }) } + /// Try to take the chain context, or exit the process with an error if no + /// chain is configured. + pub fn take_chain_or_exit(self) -> ChainContext { + self.chain + .unwrap_or_else(|| safe_exit_on_missing_chain_context()) + } + + /// Try to borrow chain context, or exit the process with an error if no + /// chain is configured. + pub fn borrow_chain_or_exit(&self) -> &ChainContext { + self.chain + .as_ref() + .unwrap_or_else(|| safe_exit_on_missing_chain_context()) + } + + /// Try to borrow mutably chain context, or exit the process with an error + /// if no chain is configured. + pub fn borrow_mut_chain_or_exit(&mut self) -> &mut ChainContext { + self.chain + .as_mut() + .unwrap_or_else(|| safe_exit_on_missing_chain_context()) + } +} + +fn safe_exit_on_missing_chain_context() -> ! { + eprintln!( + "No chain is configured. You may need to run `namada client utils \ + join-network` command." + ); + utils::safe_exit(1) +} + +impl ChainContext { /// Parse and/or look-up the value from the context. pub fn get(&self, from_context: &FromContext) -> T where @@ -165,22 +216,9 @@ impl Context { /// Get the wasm directory configured for the chain. /// - /// Note that in "dev" build, this may be the root `wasm` dir. + /// TODO(Tomas): Note that in "dev" build, this may be the root `wasm` dir. pub fn wasm_dir(&self) -> PathBuf { - let wasm_dir = - self.config.ledger.chain_dir().join(&self.config.wasm_dir); - - // In dev-mode with dev chain (the default), load wasm directly from the - // root wasm dir instead of the chain dir - #[cfg(feature = "dev")] - let wasm_dir = - if self.global_config.default_chain_id == ChainId::default() { - "wasm".into() - } else { - wasm_dir - }; - - wasm_dir + self.config.ledger.chain_dir().join(&self.config.wasm_dir) } /// Read the given WASM file from the WASM directory or an absolute path. @@ -224,7 +262,7 @@ pub fn read_or_try_new_global_config( /// Argument that can be given raw or found in the [`Context`]. #[derive(Debug, Clone)] pub struct FromContext { - raw: String, + pub raw: String, phantom: PhantomData, } @@ -282,8 +320,8 @@ impl FromContext where T: ArgFromContext, { - /// Parse and/or look-up the value from the context. - fn arg_from_ctx(&self, ctx: &Context) -> Result { + /// Parse and/or look-up the value from the chain context. + fn arg_from_ctx(&self, ctx: &ChainContext) -> Result { T::arg_from_ctx(ctx, &self.raw) } } @@ -292,32 +330,32 @@ impl FromContext where T: ArgFromMutContext, { - /// Parse and/or look-up the value from the mutable context. - fn arg_from_mut_ctx(&self, ctx: &mut Context) -> Result { + /// Parse and/or look-up the value from the mutable chain context. + fn arg_from_mut_ctx(&self, ctx: &mut ChainContext) -> Result { T::arg_from_mut_ctx(ctx, &self.raw) } } -/// CLI argument that found via the [`Context`]. +/// CLI argument that found via the [`ChainContext`]. pub trait ArgFromContext: Sized { fn arg_from_ctx( - ctx: &Context, + ctx: &ChainContext, raw: impl AsRef, ) -> Result; } -/// CLI argument that found via the [`Context`] and cached (as in case of an -/// encrypted keypair that has been decrypted), hence using mutable context. +/// CLI argument that found via the [`ChainContext`] and cached (as in case of +/// an encrypted keypair that has been decrypted), hence using mutable context. pub trait ArgFromMutContext: Sized { fn arg_from_mut_ctx( - ctx: &mut Context, + ctx: &mut ChainContext, raw: impl AsRef, ) -> Result; } impl ArgFromContext for Address { fn arg_from_ctx( - ctx: &Context, + ctx: &ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); @@ -335,7 +373,7 @@ impl ArgFromContext for Address { impl ArgFromMutContext for common::SecretKey { fn arg_from_mut_ctx( - ctx: &mut Context, + ctx: &mut ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); @@ -351,7 +389,7 @@ impl ArgFromMutContext for common::SecretKey { impl ArgFromMutContext for common::PublicKey { fn arg_from_mut_ctx( - ctx: &mut Context, + ctx: &mut ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); @@ -376,7 +414,7 @@ impl ArgFromMutContext for common::PublicKey { impl ArgFromMutContext for ExtendedSpendingKey { fn arg_from_mut_ctx( - ctx: &mut Context, + ctx: &mut ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); @@ -392,7 +430,7 @@ impl ArgFromMutContext for ExtendedSpendingKey { impl ArgFromMutContext for ExtendedViewingKey { fn arg_from_mut_ctx( - ctx: &mut Context, + ctx: &mut ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); @@ -409,7 +447,7 @@ impl ArgFromMutContext for ExtendedViewingKey { impl ArgFromContext for PaymentAddress { fn arg_from_ctx( - ctx: &Context, + ctx: &ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); @@ -426,7 +464,7 @@ impl ArgFromContext for PaymentAddress { impl ArgFromMutContext for TransferSource { fn arg_from_mut_ctx( - ctx: &mut Context, + ctx: &mut ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); @@ -442,7 +480,7 @@ impl ArgFromMutContext for TransferSource { impl ArgFromContext for TransferTarget { fn arg_from_ctx( - ctx: &Context, + ctx: &ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); @@ -457,7 +495,7 @@ impl ArgFromContext for TransferTarget { impl ArgFromMutContext for BalanceOwner { fn arg_from_mut_ctx( - ctx: &mut Context, + ctx: &mut ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 4dd36ea2eb5..f94e0f9dea2 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -53,6 +53,7 @@ use namada::types::transaction::{ use namada::types::{address, storage, token}; use tokio::time::{Duration, Instant}; +use crate::cli::context::ChainContext; use crate::cli::{self, args, Context}; use crate::client::tendermint_rpc_types::TxResponse; use crate::client::tx::{ @@ -156,7 +157,7 @@ pub async fn query_results(args: args::Query) -> Vec { /// transactions crediting/debiting the given owner. If token is specified, then /// restrict set to only transactions involving the given token. pub async fn query_tx_deltas( - ctx: &mut Context, + ctx: &mut ChainContext, ledger_address: TendermintAddress, query_owner: &Option, query_token: &Option
, @@ -268,11 +269,12 @@ pub async fn query_tx_deltas( /// Query the specified accepted transfers from the ledger pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { - let query_token = args.token.as_ref().map(|x| ctx.get(x)); - let query_owner = args.owner.as_ref().map(|x| ctx.get_cached(x)); + let chain_ctx = ctx.borrow_mut_chain_or_exit(); + let query_token = args.token.as_ref().map(|x| chain_ctx.get(x)); + let query_owner = args.owner.as_ref().map(|x| chain_ctx.get_cached(x)); // Obtain the effects of all shielded and transparent transactions let transfers = query_tx_deltas( - &mut ctx, + chain_ctx, args.query.ledger_address.clone(), &query_owner, &query_token, @@ -280,7 +282,7 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { .await; // To facilitate lookups of human-readable token names let tokens = tokens(); - let vks = ctx.wallet.get_viewing_keys(); + let vks = chain_ctx.wallet.get_viewing_keys(); // To enable ExtendedFullViewingKeys to be displayed instead of ViewingKeys let fvk_map: HashMap<_, _> = vks .values() @@ -305,7 +307,7 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { for (acc, amt) in tx_delta { // Realize the rewards that would have been attained upon the // transaction's reception - let amt = ctx + let amt = chain_ctx .shielded .compute_exchanged_amount( client.clone(), @@ -315,8 +317,10 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { ) .await .0; - let dec = - ctx.shielded.decode_amount(client.clone(), amt, epoch).await; + let dec = chain_ctx + .shielded + .decode_amount(client.clone(), amt, epoch) + .await; shielded_accounts.insert(acc, dec); } // Check if this transfer pertains to the supplied token @@ -438,32 +442,33 @@ pub async fn query_raw_bytes(_ctx: Context, args: args::QueryRawBytes) { /// Query token balance(s) pub async fn query_balance(mut ctx: Context, args: args::QueryBalance) { + let chain_ctx = ctx.borrow_mut_chain_or_exit(); // Query the balances of shielded or transparent account types depending on // the CLI arguments - match args.owner.as_ref().map(|x| ctx.get_cached(x)) { + match args.owner.as_ref().map(|x| chain_ctx.get_cached(x)) { Some(BalanceOwner::FullViewingKey(_viewing_key)) => { - query_shielded_balance(&mut ctx, args).await + query_shielded_balance(chain_ctx, args).await } Some(BalanceOwner::Address(_owner)) => { - query_transparent_balance(&mut ctx, args).await + query_transparent_balance(chain_ctx, args).await } Some(BalanceOwner::PaymentAddress(_owner)) => { - query_pinned_balance(&mut ctx, args).await + query_pinned_balance(chain_ctx, args).await } None => { // Print pinned balance - query_pinned_balance(&mut ctx, args.clone()).await; + query_pinned_balance(chain_ctx, args.clone()).await; // Print shielded balance - query_shielded_balance(&mut ctx, args.clone()).await; + query_shielded_balance(chain_ctx, args.clone()).await; // Then print transparent balance - query_transparent_balance(&mut ctx, args).await; + query_transparent_balance(chain_ctx, args).await; } }; } /// Query token balance(s) pub async fn query_transparent_balance( - ctx: &mut Context, + ctx: &mut ChainContext, args: args::QueryBalance, ) { let client = HttpClient::new(args.query.ledger_address).unwrap(); @@ -543,7 +548,10 @@ pub async fn query_transparent_balance( } /// Query the token pinned balance(s) -pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { +pub async fn query_pinned_balance( + ctx: &mut ChainContext, + args: args::QueryBalance, +) { // Map addresses to token names let tokens = address::tokens(); let owners = if let Some(pa) = args @@ -681,7 +689,7 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { } fn print_balances( - ctx: &Context, + ctx: &ChainContext, balances: impl Iterator, token: &Address, target: Option<&Address>, @@ -873,7 +881,7 @@ pub fn value_by_address( /// Query token shielded balance(s) pub async fn query_shielded_balance( - ctx: &mut Context, + ctx: &mut ChainContext, args: args::QueryBalance, ) { // Used to control whether balances for all keys or a specific key are @@ -1438,11 +1446,12 @@ pub async fn query_withdrawable_tokens( /// Query PoS bond(s) and unbond(s) pub async fn query_bonds(ctx: Context, args: args::QueryBonds) { + let chain_ctx = ctx.borrow_chain_or_exit(); let _epoch = query_and_print_epoch(args.query.clone()).await; let client = HttpClient::new(args.query.ledger_address).unwrap(); - let source = args.owner.map(|owner| ctx.get(&owner)); - let validator = args.validator.map(|val| ctx.get(&val)); + let source = args.owner.map(|owner| chain_ctx.get(&owner)); + let validator = args.validator.map(|val| chain_ctx.get(&val)); let stdout = io::stdout(); let mut w = stdout.lock(); @@ -1543,6 +1552,7 @@ pub async fn query_bonds(ctx: Context, args: args::QueryBonds) { /// Query PoS bonded stake pub async fn query_bonded_stake(ctx: Context, args: args::QueryBondedStake) { + let chain_ctx = ctx.borrow_chain_or_exit(); let epoch = match args.epoch { Some(epoch) => epoch, None => query_and_print_epoch(args.query.clone()).await, @@ -1551,7 +1561,7 @@ pub async fn query_bonded_stake(ctx: Context, args: args::QueryBondedStake) { match args.validator { Some(validator) => { - let validator = ctx.get(&validator); + let validator = chain_ctx.get(&validator); // Find bonded stake for the given validator let stake = get_validator_stake(&client, epoch, &validator).await; match stake { @@ -1627,8 +1637,9 @@ pub async fn query_and_print_commission_rate( ctx: Context, args: args::QueryCommissionRate, ) { + let chain_ctx = ctx.borrow_chain_or_exit(); let client = HttpClient::new(args.query.ledger_address.clone()).unwrap(); - let validator = ctx.get(&args.validator); + let validator = chain_ctx.get(&args.validator); let info: Option = query_commission_rate(&client, &validator, args.epoch).await; @@ -1656,6 +1667,7 @@ pub async fn query_and_print_commission_rate( /// Query PoS slashes pub async fn query_slashes(ctx: Context, args: args::QuerySlashes) { + let chain_ctx = ctx.borrow_chain_or_exit(); let client = HttpClient::new(args.query.ledger_address).unwrap(); let params_key = pos::params_key(); let params = query_storage_value::(&client, ¶ms_key) @@ -1664,7 +1676,7 @@ pub async fn query_slashes(ctx: Context, args: args::QuerySlashes) { match args.validator { Some(validator) => { - let validator = ctx.get(&validator); + let validator = chain_ctx.get(&validator); // Find slashes for the given validator let slashes: Vec = unwrap_client_response( RPC.vp().pos().validator_slashes(&client, &validator).await, @@ -1716,8 +1728,9 @@ pub async fn query_slashes(ctx: Context, args: args::QuerySlashes) { } pub async fn query_delegations(ctx: Context, args: args::QueryDelegations) { + let chain_ctx = ctx.borrow_chain_or_exit(); let client = HttpClient::new(args.query.ledger_address).unwrap(); - let owner = ctx.get(&args.owner); + let owner = chain_ctx.get(&args.owner); let delegations = unwrap_client_response( RPC.vp().pos().delegation_validators(&client, &owner).await, ); @@ -1830,8 +1843,9 @@ pub async fn get_testnet_pow_challenge( /// Query for all conversions. pub async fn query_conversions(ctx: Context, args: args::QueryConversions) { + let chain_ctx = ctx.borrow_chain_or_exit(); // The chosen token type of the conversions - let target_token = args.token.as_ref().map(|x| ctx.get(x)); + let target_token = args.token.as_ref().map(|x| chain_ctx.get(x)); // To facilitate human readable token addresses let tokens = address::tokens(); let client = HttpClient::new(args.query.ledger_address).unwrap(); @@ -2598,7 +2612,7 @@ pub async fn get_governance_parameters(client: &HttpClient) -> GovParams { /// Try to find an alias for a given address from the wallet. If not found, /// formats the address into a string. -fn lookup_alias(ctx: &Context, addr: &Address) -> String { +fn lookup_alias(ctx: &ChainContext, addr: &Address) -> String { match ctx.wallet.find_alias(addr) { Some(alias) => format!("{}", alias), None => format!("{}", addr), diff --git a/apps/src/lib/client/signing.rs b/apps/src/lib/client/signing.rs index 9b1a00b987d..29c356a2cc3 100644 --- a/apps/src/lib/client/signing.rs +++ b/apps/src/lib/client/signing.rs @@ -13,8 +13,8 @@ use namada::types::token::Amount; use namada::types::transaction::{hash_tx, Fee, WrapperTx, MIN_FEE}; use super::rpc; -use crate::cli::context::{WalletAddress, WalletKeypair}; -use crate::cli::{self, args, Context}; +use crate::cli::context::{ChainContext, WalletAddress, WalletKeypair}; +use crate::cli::{self, args}; use crate::client::tendermint_rpc_types::TxBroadcastData; use crate::facade::tendermint_config::net::Address as TendermintAddress; use crate::facade::tendermint_rpc::HttpClient; @@ -91,7 +91,7 @@ pub enum TxSigningKey { /// possible. If no explicit signer given, use the `default`. If no `default` /// is given, panics. pub async fn tx_signer( - ctx: &mut Context, + ctx: &mut ChainContext, args: &args::Tx, mut default: TxSigningKey, ) -> common::SecretKey { @@ -146,20 +146,20 @@ pub async fn tx_signer( /// /// If it is a dry run, it is not put in a wrapper, but returned as is. pub async fn sign_tx( - mut ctx: Context, + ctx: &mut ChainContext, tx: Tx, args: &args::Tx, default: TxSigningKey, #[cfg(not(feature = "mainnet"))] requires_pow: bool, -) -> (Context, TxBroadcastData) { +) -> TxBroadcastData { if args.dump_tx { - dump_tx_helper(&ctx, &tx, "unsigned", None); + dump_tx_helper(ctx, &tx, "unsigned", None); } - let keypair = tx_signer(&mut ctx, args, default).await; + let keypair = tx_signer(ctx, args, default).await; let tx = tx.sign(&keypair); if args.dump_tx { - dump_tx_helper(&ctx, &tx, "signed", None); + dump_tx_helper(ctx, &tx, "signed", None); } let epoch = rpc::query_and_print_epoch(args::Query { @@ -170,7 +170,7 @@ pub async fn sign_tx( TxBroadcastData::DryRun(tx) } else { sign_wrapper( - &ctx, + ctx, args, epoch, tx, @@ -193,14 +193,14 @@ pub async fn sign_tx( } => (tx, wrapper_hash), }; - dump_tx_helper(&ctx, wrapper_tx, "wrapper", Some(wrapper_hash)); + dump_tx_helper(ctx, wrapper_tx, "wrapper", Some(wrapper_hash)); } - (ctx, broadcast_data) + broadcast_data } pub fn dump_tx_helper( - ctx: &Context, + ctx: &ChainContext, tx: &Tx, extension: &str, precomputed_hash: Option<&String>, @@ -228,7 +228,7 @@ pub fn dump_tx_helper( /// wrapper and its payload which is needed for monitoring its /// progress on chain. pub async fn sign_wrapper( - ctx: &Context, + ctx: &ChainContext, args: &args::Tx, epoch: Epoch, tx: Tx, diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 0014833ca88..4dede27168b 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -66,7 +66,7 @@ use tokio::time::{Duration, Instant}; use super::rpc; use super::types::ShieldedTransferContext; -use crate::cli::context::WalletAddress; +use crate::cli::context::{ChainContext, WalletAddress}; use crate::cli::{args, safe_exit, Context}; use crate::client::rpc::{query_conversion, query_storage_value}; use crate::client::signing::{find_keypair, sign_tx, tx_signer, TxSigningKey}; @@ -101,14 +101,15 @@ const ENV_VAR_NAMADA_EVENTS_MAX_WAIT_TIME_SECONDS: &str = /// and `/applied` ABCI query endpoints. const DEFAULT_NAMADA_EVENTS_MAX_WAIT_TIME_SECONDS: u64 = 60; -pub async fn submit_custom(ctx: Context, args: args::TxCustom) { - let tx_code = ctx.read_wasm(args.code_path); +pub async fn submit_custom(mut ctx: Context, args: args::TxCustom) { + let chain_ctx = ctx.borrow_mut_chain_or_exit(); + let tx_code = chain_ctx.read_wasm(args.code_path); let data = args.data_path.map(|data_path| { std::fs::read(data_path).expect("Expected a file at given data path") }); let tx = Tx::new(tx_code, data); - let (ctx, initialized_accounts) = process_tx( - ctx, + let initialized_accounts = process_tx( + chain_ctx, &args.tx, tx, TxSigningKey::None, @@ -116,11 +117,12 @@ pub async fn submit_custom(ctx: Context, args: args::TxCustom) { false, ) .await; - save_initialized_accounts(ctx, &args.tx, initialized_accounts).await; + save_initialized_accounts(chain_ctx, &args.tx, initialized_accounts).await; } -pub async fn submit_update_vp(ctx: Context, args: args::TxUpdateVp) { - let addr = ctx.get(&args.addr); +pub async fn submit_update_vp(mut ctx: Context, args: args::TxUpdateVp) { + let chain_ctx = ctx.borrow_mut_chain_or_exit(); + let addr = chain_ctx.get(&args.addr); // Check that the address is established and exists on chain match &addr { @@ -155,7 +157,7 @@ pub async fn submit_update_vp(ctx: Context, args: args::TxUpdateVp) { } } - let vp_code = ctx.read_wasm(args.vp_code_path); + let vp_code = chain_ctx.read_wasm(args.vp_code_path); // Validate the VP code if let Err(err) = vm::validate_untrusted_wasm(&vp_code) { eprintln!("Validity predicate code validation failed with {}", err); @@ -164,14 +166,14 @@ pub async fn submit_update_vp(ctx: Context, args: args::TxUpdateVp) { } } - let tx_code = ctx.read_wasm(TX_UPDATE_VP_WASM); + let tx_code = chain_ctx.read_wasm(TX_UPDATE_VP_WASM); let data = UpdateVp { addr, vp_code }; let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); let tx = Tx::new(tx_code, Some(data)); process_tx( - ctx, + chain_ctx, &args.tx, tx, TxSigningKey::WalletAddress(args.addr), @@ -182,11 +184,12 @@ pub async fn submit_update_vp(ctx: Context, args: args::TxUpdateVp) { } pub async fn submit_init_account(mut ctx: Context, args: args::TxInitAccount) { - let public_key = ctx.get_cached(&args.public_key); + let chain_ctx = ctx.borrow_mut_chain_or_exit(); + let public_key = chain_ctx.get_cached(&args.public_key); let vp_code = args .vp_code_path - .map(|path| ctx.read_wasm(path)) - .unwrap_or_else(|| ctx.read_wasm(VP_USER_WASM)); + .map(|path| chain_ctx.read_wasm(path)) + .unwrap_or_else(|| chain_ctx.read_wasm(VP_USER_WASM)); // Validate the VP code if let Err(err) = vm::validate_untrusted_wasm(&vp_code) { eprintln!("Validity predicate code validation failed with {}", err); @@ -195,7 +198,7 @@ pub async fn submit_init_account(mut ctx: Context, args: args::TxInitAccount) { } } - let tx_code = ctx.read_wasm(TX_INIT_ACCOUNT_WASM); + let tx_code = chain_ctx.read_wasm(TX_INIT_ACCOUNT_WASM); let data = InitAccount { public_key, vp_code, @@ -203,8 +206,8 @@ pub async fn submit_init_account(mut ctx: Context, args: args::TxInitAccount) { let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); let tx = Tx::new(tx_code, Some(data)); - let (ctx, initialized_accounts) = process_tx( - ctx, + let initialized_accounts = process_tx( + chain_ctx, &args.tx, tx, TxSigningKey::WalletAddress(args.source), @@ -212,7 +215,7 @@ pub async fn submit_init_account(mut ctx: Context, args: args::TxInitAccount) { false, ) .await; - save_initialized_accounts(ctx, &args.tx, initialized_accounts).await; + save_initialized_accounts(chain_ctx, &args.tx, initialized_accounts).await; } pub async fn submit_init_validator( @@ -230,6 +233,7 @@ pub async fn submit_init_validator( unsafe_dont_encrypt, }: args::TxInitValidator, ) { + let chain_ctx = ctx.borrow_mut_chain_or_exit(); let alias = tx_args .initialized_account_alias .as_ref() @@ -238,19 +242,21 @@ pub async fn submit_init_validator( let validator_key_alias = format!("{}-key", alias); let consensus_key_alias = format!("{}-consensus-key", alias); - let account_key = ctx.get_opt_cached(&account_key).unwrap_or_else(|| { - println!("Generating validator account key..."); - ctx.wallet - .gen_key( - scheme, - Some(validator_key_alias.clone()), - unsafe_dont_encrypt, - ) - .1 - .ref_to() - }); + let account_key = + chain_ctx.get_opt_cached(&account_key).unwrap_or_else(|| { + println!("Generating validator account key..."); + chain_ctx + .wallet + .gen_key( + scheme, + Some(validator_key_alias.clone()), + unsafe_dont_encrypt, + ) + .1 + .ref_to() + }); - let consensus_key = ctx + let consensus_key = chain_ctx .get_opt_cached(&consensus_key) .map(|key| match key { common::SecretKey::Ed25519(_) => key, @@ -261,7 +267,8 @@ pub async fn submit_init_validator( }) .unwrap_or_else(|| { println!("Generating consensus key..."); - ctx.wallet + chain_ctx + .wallet .gen_key( // Note that TM only allows ed25519 for consensus key SchemeType::Ed25519, @@ -271,14 +278,16 @@ pub async fn submit_init_validator( .1 }); - let protocol_key = ctx.get_opt_cached(&protocol_key); + let protocol_key = chain_ctx.get_opt_cached(&protocol_key); if protocol_key.is_none() { println!("Generating protocol signing key..."); } // Generate the validator keys - let validator_keys = - ctx.wallet.gen_validator_keys(protocol_key, scheme).unwrap(); + let validator_keys = chain_ctx + .wallet + .gen_validator_keys(protocol_key, scheme) + .unwrap(); let protocol_key = validator_keys.get_protocol_keypair().ref_to(); let dkg_key = validator_keys .dkg_keypair @@ -286,11 +295,14 @@ pub async fn submit_init_validator( .expect("DKG sessions keys should have been created") .public(); - ctx.wallet.save().unwrap_or_else(|err| eprintln!("{}", err)); + chain_ctx + .wallet + .save() + .unwrap_or_else(|err| eprintln!("{}", err)); let validator_vp_code = validator_vp_code_path - .map(|path| ctx.read_wasm(path)) - .unwrap_or_else(|| ctx.read_wasm(VP_USER_WASM)); + .map(|path| chain_ctx.read_wasm(path)) + .unwrap_or_else(|| chain_ctx.read_wasm(VP_USER_WASM)); // Validate the commission rate data if commission_rate > Decimal::ONE || commission_rate < Decimal::ZERO { @@ -323,7 +335,7 @@ pub async fn submit_init_validator( safe_exit(1) } } - let tx_code = ctx.read_wasm(TX_INIT_VALIDATOR_WASM); + let tx_code = chain_ctx.read_wasm(TX_INIT_VALIDATOR_WASM); let data = InitValidator { account_key, @@ -336,8 +348,8 @@ pub async fn submit_init_validator( }; let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); let tx = Tx::new(tx_code, Some(data)); - let (mut ctx, initialized_accounts) = process_tx( - ctx, + let initialized_accounts = process_tx( + chain_ctx, &tx_args, tx, TxSigningKey::WalletAddress(source), @@ -374,7 +386,7 @@ pub async fn submit_init_validator( } else { validator_address_alias }; - if let Some(new_alias) = ctx.wallet.add_address( + if let Some(new_alias) = chain_ctx.wallet.add_address( validator_address_alias.clone(), validator_address.clone(), ) { @@ -392,11 +404,15 @@ pub async fn submit_init_validator( } }; // add validator address and keys to the wallet - ctx.wallet + chain_ctx + .wallet .add_validator_data(validator_address, validator_keys); - ctx.wallet.save().unwrap_or_else(|err| eprintln!("{}", err)); + chain_ctx + .wallet + .save() + .unwrap_or_else(|err| eprintln!("{}", err)); - let tendermint_home = ctx.config.ledger.tendermint_dir(); + let tendermint_home = chain_ctx.config.ledger.tendermint_dir(); tendermint_node::write_validator_key(&tendermint_home, &consensus_key); tendermint_node::write_validator_state(tendermint_home); @@ -1503,7 +1519,8 @@ where } pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { - let parsed_args = args.parse_from_context(&mut ctx); + let chain_ctx = ctx.borrow_mut_chain_or_exit(); + let parsed_args = args.parse_from_context(chain_ctx); let source = parsed_args.source.effective_address(); let target = parsed_args.target.effective_address(); // Check that the source address exists on chain @@ -1590,7 +1607,7 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { ( TxSigningKey::SecretKey(masp_tx_key()), 0.into(), - ctx.native_token.clone(), + chain_ctx.native_token.clone(), ) } else if source == masp_addr { ( @@ -1607,12 +1624,12 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { }; // If our chosen signer is the MASP sentinel key, then our shielded inputs // will need to cover the gas fees. - let chosen_signer = tx_signer(&mut ctx, &args.tx, default_signer.clone()) + let chosen_signer = tx_signer(chain_ctx, &args.tx, default_signer.clone()) .await .ref_to(); let shielded_gas = masp_tx_key().ref_to() == chosen_signer; // Determine whether to pin this transaction to a storage key - let key = match ctx.get(&args.target) { + let key = match chain_ctx.get(&args.target) { TransferTarget::PaymentAddress(pa) if pa.is_pinned() => Some(pa.hash()), _ => None, }; @@ -1642,16 +1659,20 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { let spending_keys: Vec<_> = spending_key.into_iter().collect(); // Load the current shielded context given the spending key we // possess - let _ = ctx.shielded.load(); - ctx.shielded + let _ = chain_ctx.shielded.load(); + chain_ctx + .shielded .fetch(&args.tx.ledger_address, &spending_keys, &[]) .await; // Save the update state so that future fetches can be // short-circuited - let _ = ctx.shielded.save(); - let stx_result = - gen_shielded_transfer(&mut ctx, &parsed_args, shielded_gas) - .await; + let _ = chain_ctx.shielded.save(); + let stx_result = gen_shielded_transfer( + chain_ctx, + &parsed_args, + shielded_gas, + ) + .await; match stx_result { Ok(stx) => stx.map(|x| x.0), Err(builder::Error::ChangeIsNegative(_)) => { @@ -1676,12 +1697,12 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { let data = transfer .try_to_vec() .expect("Encoding tx data shouldn't fail"); - let tx_code = ctx.read_wasm(TX_TRANSFER_WASM); + let tx_code = chain_ctx.read_wasm(TX_TRANSFER_WASM); let tx = Tx::new(tx_code, Some(data)); let signing_address = TxSigningKey::WalletAddress(args.source.to_address()); process_tx( - ctx, + chain_ctx, &args.tx, tx, signing_address, @@ -1691,8 +1712,9 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { .await; } -pub async fn submit_ibc_transfer(ctx: Context, args: args::TxIbcTransfer) { - let source = ctx.get(&args.source); +pub async fn submit_ibc_transfer(mut ctx: Context, args: args::TxIbcTransfer) { + let chain_ctx = ctx.borrow_mut_chain_or_exit(); + let source = chain_ctx.get(&args.source); // Check that the source address exists on chain let source_exists = rpc::known_address(&source, args.tx.ledger_address.clone()).await; @@ -1705,7 +1727,7 @@ pub async fn submit_ibc_transfer(ctx: Context, args: args::TxIbcTransfer) { // We cannot check the receiver - let token = ctx.get(&args.token); + let token = chain_ctx.get(&args.token); // Check that the token address exists on chain let token_exists = rpc::known_address(&token, args.tx.ledger_address.clone()).await; @@ -1753,7 +1775,7 @@ pub async fn submit_ibc_transfer(ctx: Context, args: args::TxIbcTransfer) { } } } - let tx_code = ctx.read_wasm(TX_IBC_WASM); + let tx_code = chain_ctx.read_wasm(TX_IBC_WASM); let denom = match sub_prefix { // To parse IbcToken address, remove the address prefix @@ -1799,7 +1821,7 @@ pub async fn submit_ibc_transfer(ctx: Context, args: args::TxIbcTransfer) { let tx = Tx::new(tx_code, Some(data)); process_tx( - ctx, + chain_ctx, &args.tx, tx, TxSigningKey::WalletAddress(args.source), @@ -1810,6 +1832,7 @@ pub async fn submit_ibc_transfer(ctx: Context, args: args::TxIbcTransfer) { } pub async fn submit_init_proposal(mut ctx: Context, args: args::InitProposal) { + let chain_ctx = ctx.borrow_mut_chain_or_exit(); let file = File::open(&args.proposal_data).expect("File must exist."); let proposal: Proposal = serde_json::from_reader(file).expect("JSON was not well-formatted"); @@ -1878,9 +1901,9 @@ pub async fn submit_init_proposal(mut ctx: Context, args: args::InitProposal) { } if args.offline { - let signer = ctx.get(&signer); + let signer = chain_ctx.get(&signer); let signing_key = find_keypair( - &mut ctx.wallet, + &mut chain_ctx.wallet, &signer, args.tx.ledger_address.clone(), ) @@ -1916,7 +1939,7 @@ pub async fn submit_init_proposal(mut ctx: Context, args: args::InitProposal) { let balance = rpc::get_token_balance( &client, - &ctx.native_token, + &chain_ctx.native_token, &proposal.author, ) .await @@ -1941,11 +1964,11 @@ pub async fn submit_init_proposal(mut ctx: Context, args: args::InitProposal) { let data = init_proposal_data .try_to_vec() .expect("Encoding proposal data shouldn't fail"); - let tx_code = ctx.read_wasm(TX_INIT_PROPOSAL); + let tx_code = chain_ctx.read_wasm(TX_INIT_PROPOSAL); let tx = Tx::new(tx_code, Some(data)); process_tx( - ctx, + chain_ctx, &args.tx, tx, TxSigningKey::WalletAddress(signer), @@ -1957,6 +1980,7 @@ pub async fn submit_init_proposal(mut ctx: Context, args: args::InitProposal) { } pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { + let chain_ctx = ctx.borrow_mut_chain_or_exit(); let signer = if let Some(addr) = &args.tx.signer { addr } else { @@ -1965,7 +1989,7 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { }; if args.offline { - let signer = ctx.get(signer); + let signer = chain_ctx.get(signer); let proposal_file_path = args.proposal_data.expect("Proposal file should exist."); let file = File::open(&proposal_file_path).expect("File must exist."); @@ -1984,7 +2008,7 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { } let signing_key = find_keypair( - &mut ctx.wallet, + &mut chain_ctx.wallet, &signer, args.tx.ledger_address.clone(), ) @@ -2020,7 +2044,7 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { }) .await; - let voter_address = ctx.get(signer); + let voter_address = chain_ctx.get(signer); let proposal_id = args.proposal_id.unwrap(); let proposal_start_epoch_key = gov_storage::get_voting_start_epoch_key(proposal_id); @@ -2081,11 +2105,11 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { let data = tx_data .try_to_vec() .expect("Encoding proposal data shouldn't fail"); - let tx_code = ctx.read_wasm(TX_VOTE_PROPOSAL); + let tx_code = chain_ctx.read_wasm(TX_VOTE_PROPOSAL); let tx = Tx::new(tx_code, Some(data)); process_tx( - ctx, + chain_ctx, &args.tx, tx, TxSigningKey::WalletAddress(signer.clone()), @@ -2108,19 +2132,20 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { } pub async fn submit_reveal_pk(mut ctx: Context, args: args::RevealPk) { + let chain_ctx = ctx.borrow_mut_chain_or_exit(); let args::RevealPk { tx: args, public_key, } = args; - let public_key = ctx.get_cached(&public_key); - if !reveal_pk_if_needed(&mut ctx, &public_key, &args).await { + let public_key = chain_ctx.get_cached(&public_key); + if !reveal_pk_if_needed(chain_ctx, &public_key, &args).await { let addr: Address = (&public_key).into(); println!("PK for {addr} is already revealed, nothing to do."); } } pub async fn reveal_pk_if_needed( - ctx: &mut Context, + ctx: &mut ChainContext, public_key: &common::PublicKey, args: &args::Tx, ) -> bool { @@ -2144,7 +2169,7 @@ pub async fn has_revealed_pk( } pub async fn submit_reveal_pk_aux( - ctx: &mut Context, + ctx: &mut ChainContext, public_key: &common::PublicKey, args: &args::Tx, ) { @@ -2294,8 +2319,9 @@ async fn filter_delegations( delegations.into_iter().flatten().collect() } -pub async fn submit_bond(ctx: Context, args: args::Bond) { - let validator = ctx.get(&args.validator); +pub async fn submit_bond(mut ctx: Context, args: args::Bond) { + let chain_ctx = ctx.borrow_mut_chain_or_exit(); + let validator = chain_ctx.get(&args.validator); // Check that the validator address exists on chain let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); @@ -2309,7 +2335,7 @@ pub async fn submit_bond(ctx: Context, args: args::Bond) { safe_exit(1) } } - let source = ctx.get_opt(&args.source); + let source = chain_ctx.get_opt(&args.source); // Check that the source address exists on chain if let Some(source) = &source { let source_exists = @@ -2324,7 +2350,7 @@ pub async fn submit_bond(ctx: Context, args: args::Bond) { // Check bond's source (source for delegation or validator for self-bonds) // balance let bond_source = source.as_ref().unwrap_or(&validator); - let balance_key = token::balance_key(&ctx.native_token, bond_source); + let balance_key = token::balance_key(&chain_ctx.native_token, bond_source); match rpc::query_storage_value::(&client, &balance_key).await { Some(balance) => { @@ -2348,7 +2374,7 @@ pub async fn submit_bond(ctx: Context, args: args::Bond) { } } } - let tx_code = ctx.read_wasm(TX_BOND_WASM); + let tx_code = chain_ctx.read_wasm(TX_BOND_WASM); println!("Wasm tx bond code bytes length = {}\n", tx_code.len()); let bond = pos::Bond { validator, @@ -2360,7 +2386,7 @@ pub async fn submit_bond(ctx: Context, args: args::Bond) { let tx = Tx::new(tx_code, Some(data)); let default_signer = args.source.unwrap_or(args.validator); process_tx( - ctx, + chain_ctx, &args.tx, tx, TxSigningKey::WalletAddress(default_signer), @@ -2370,9 +2396,10 @@ pub async fn submit_bond(ctx: Context, args: args::Bond) { .await; } -pub async fn submit_unbond(ctx: Context, args: args::Unbond) { - let validator = ctx.get(&args.validator); - let source = ctx.get_opt(&args.source); +pub async fn submit_unbond(mut ctx: Context, args: args::Unbond) { + let chain_ctx = ctx.borrow_mut_chain_or_exit(); + let validator = chain_ctx.get(&args.validator); + let source = chain_ctx.get_opt(&args.source); // Check that the validator address exists on chain let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); @@ -2411,11 +2438,11 @@ pub async fn submit_unbond(ctx: Context, args: args::Unbond) { }; let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); - let tx_code = ctx.read_wasm(TX_UNBOND_WASM); + let tx_code = chain_ctx.read_wasm(TX_UNBOND_WASM); let tx = Tx::new(tx_code, Some(data)); let default_signer = args.source.unwrap_or(args.validator); - let (_ctx, _) = process_tx( - ctx, + process_tx( + chain_ctx, &args.tx, tx, TxSigningKey::WalletAddress(default_signer), @@ -2427,9 +2454,10 @@ pub async fn submit_unbond(ctx: Context, args: args::Unbond) { rpc::query_and_print_unbonds(&client, &bond_source, &validator).await; } -pub async fn submit_withdraw(ctx: Context, args: args::Withdraw) { - let validator = ctx.get(&args.validator); - let source = ctx.get_opt(&args.source); +pub async fn submit_withdraw(mut ctx: Context, args: args::Withdraw) { + let chain_ctx = ctx.borrow_mut_chain_or_exit(); + let validator = chain_ctx.get(&args.validator); + let source = chain_ctx.get_opt(&args.source); let epoch = rpc::query_and_print_epoch(args::Query { ledger_address: args.tx.ledger_address.clone(), @@ -2476,11 +2504,11 @@ pub async fn submit_withdraw(ctx: Context, args: args::Withdraw) { let data = pos::Withdraw { validator, source }; let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); - let tx_code = ctx.read_wasm(TX_WITHDRAW_WASM); + let tx_code = chain_ctx.read_wasm(TX_WITHDRAW_WASM); let tx = Tx::new(tx_code, Some(data)); let default_signer = args.source.unwrap_or(args.validator); process_tx( - ctx, + chain_ctx, &args.tx, tx, TxSigningKey::WalletAddress(default_signer), @@ -2491,15 +2519,16 @@ pub async fn submit_withdraw(ctx: Context, args: args::Withdraw) { } pub async fn submit_validator_commission_change( - ctx: Context, + mut ctx: Context, args: args::TxCommissionRateChange, ) { + let chain_ctx = ctx.borrow_mut_chain_or_exit(); let epoch = rpc::query_and_print_epoch(args::Query { ledger_address: args.tx.ledger_address.clone(), }) .await; - let tx_code = ctx.read_wasm(TX_CHANGE_COMMISSION_WASM); + let tx_code = chain_ctx.read_wasm(TX_CHANGE_COMMISSION_WASM); let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); // TODO: put following two let statements in its own function @@ -2508,7 +2537,7 @@ pub async fn submit_validator_commission_change( .await .expect("Parameter should be defined."); - let validator = ctx.get(&args.validator); + let validator = chain_ctx.get(&args.validator); if rpc::is_validator(&client, &validator).await { if args.rate < Decimal::ZERO || args.rate > Decimal::ONE { eprintln!("Invalid new commission rate, received {}", args.rate); @@ -2558,7 +2587,7 @@ pub async fn submit_validator_commission_change( } let data = pos::CommissionChange { - validator: ctx.get(&args.validator), + validator: chain_ctx.get(&args.validator), new_rate: args.rate, }; let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); @@ -2566,7 +2595,7 @@ pub async fn submit_validator_commission_change( let tx = Tx::new(tx_code, Some(data)); let default_signer = args.validator; process_tx( - ctx, + chain_ctx, &args.tx, tx, TxSigningKey::WalletAddress(default_signer), @@ -2579,13 +2608,13 @@ pub async fn submit_validator_commission_change( /// Submit transaction and wait for result. Returns a list of addresses /// initialized in the transaction if any. In dry run, this is always empty. async fn process_tx( - ctx: Context, + ctx: &mut ChainContext, args: &args::Tx, tx: Tx, default_signer: TxSigningKey, #[cfg(not(feature = "mainnet"))] requires_pow: bool, -) -> (Context, Vec
) { - let (ctx, to_broadcast) = sign_tx( +) -> Vec
{ + let to_broadcast = sign_tx( ctx, tx, args, @@ -2607,7 +2636,7 @@ async fn process_tx( if args.dry_run { if let TxBroadcastData::DryRun(tx) = to_broadcast { rpc::dry_run_tx(&args.ledger_address, tx.to_bytes()).await; - (ctx, vec![]) + vec![] } else { panic!( "Expected a dry-run transaction, received a wrapper \ @@ -2625,8 +2654,8 @@ async fn process_tx( // Return result based on executed operation, otherwise deal with // the encountered errors uniformly match result { - Right(Ok(result)) => (ctx, result.initialized_accounts), - Left(Ok(_)) => (ctx, Vec::default()), + Right(Ok(result)) => result.initialized_accounts, + Left(Ok(_)) => Vec::default(), Right(Err(err)) => { eprintln!( "Encountered error while broadcasting transaction: {}", @@ -2647,7 +2676,7 @@ async fn process_tx( /// Save accounts initialized from a tx into the wallet, if any. async fn save_initialized_accounts( - mut ctx: Context, + ctx: &mut ChainContext, args: &args::Tx, initialized_accounts: Vec
, ) { diff --git a/apps/src/lib/client/types.rs b/apps/src/lib/client/types.rs index d75d5a596c9..ef3d3efedf8 100644 --- a/apps/src/lib/client/types.rs +++ b/apps/src/lib/client/types.rs @@ -10,7 +10,8 @@ use namada::types::transaction::GasLimit; use namada::types::{key, token}; use super::rpc; -use crate::cli::{args, Context}; +use crate::cli::args; +use crate::cli::context::ChainContext; use crate::client::tx::Conversions; use crate::facade::tendermint_config::net::Address as TendermintAddress; @@ -73,7 +74,7 @@ pub trait ShieldedTransferContext { } #[async_trait(?Send)] -impl ShieldedTransferContext for Context { +impl ShieldedTransferContext for ChainContext { async fn collect_unspent_notes( &mut self, ledger_address: TendermintAddress, diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index 0dbb385208c..de9030b4760 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -83,29 +83,11 @@ pub mod genesis_config { #[derive(Error, Debug)] pub enum HexKeyError { #[error("Invalid hex string: {0:?}")] - InvalidHexString(data_encoding::DecodeError), + InvalidHexString(#[from] data_encoding::DecodeError), #[error("Invalid sha256 checksum: {0}")] - InvalidSha256(TryFromSliceError), + InvalidSha256(#[from] TryFromSliceError), #[error("Invalid public key: {0}")] - InvalidPublicKey(ParsePublicKeyError), - } - - impl From for HexKeyError { - fn from(err: data_encoding::DecodeError) -> Self { - Self::InvalidHexString(err) - } - } - - impl From for HexKeyError { - fn from(err: ParsePublicKeyError) -> Self { - Self::InvalidPublicKey(err) - } - } - - impl From for HexKeyError { - fn from(err: TryFromSliceError) -> Self { - Self::InvalidSha256(err) - } + InvalidPublicKey(#[from] common::DecodeError), } #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/apps/src/lib/config/global.rs b/apps/src/lib/config/global.rs index f1d8e5d4837..6c9bae0e4ad 100644 --- a/apps/src/lib/config/global.rs +++ b/apps/src/lib/config/global.rs @@ -26,29 +26,30 @@ pub enum Error { pub type Result = std::result::Result; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize)] pub struct GlobalConfig { /// The default chain ID - pub default_chain_id: ChainId, + pub default_chain_id: Option, // NOTE: There will be sub-chains in here in future } impl GlobalConfig { pub fn new(default_chain_id: ChainId) -> Self { - Self { default_chain_id } + Self { + default_chain_id: Some(default_chain_id), + } } /// Try to read the global config from a file. pub fn read(base_dir: impl AsRef) -> Result { let file_path = Self::file_path(base_dir.as_ref()); let file_name = file_path.to_str().expect("Expected UTF-8 file path"); - if !file_path.exists() { - return Err(Error::FileNotFound(file_name.to_string())); - }; let mut config = config::Config::new(); - config - .merge(config::File::with_name(file_name)) - .map_err(Error::ReadError)?; + if file_path.exists() { + config + .merge(config::File::with_name(file_name)) + .map_err(Error::ReadError)?; + }; config.try_into().map_err(Error::DeserializationError) } diff --git a/core/src/types/address.rs b/core/src/types/address.rs index 5104615cb7a..e23e024373c 100644 --- a/core/src/types/address.rs +++ b/core/src/types/address.rs @@ -6,26 +6,20 @@ use std::fmt::{Debug, Display}; use std::hash::Hash; use std::str::FromStr; -use bech32::{self, FromBase32, ToBase32, Variant}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use thiserror::Error; -use crate::types::key; +use crate::impl_display_and_from_str_via_format; use crate::types::key::PublicKeyHash; +use crate::types::{key, string_encoding}; /// The length of an established [`Address`] encoded with Borsh. pub const ESTABLISHED_ADDRESS_BYTES_LEN: usize = 45; /// The length of [`Address`] encoded with Bech32m. -pub const ADDRESS_LEN: usize = 79 + ADDRESS_HRP.len(); +pub const ADDRESS_LEN: usize = 79 + string_encoding::hrp_len::
(); -/// human-readable part of Bech32m encoded address -// TODO use "a" for live network -const ADDRESS_HRP: &str = "atest"; -/// We're using "Bech32m" variant -pub const BECH32M_VARIANT: bech32::Variant = Variant::Bech32m; pub(crate) const HASH_LEN: usize = 40; /// An address string before bech32m encoding must be this size. @@ -71,6 +65,12 @@ mod internal { "ano::ETH Bridge Address "; } +/// Error from decoding address from string +pub type DecodeError = string_encoding::DecodeError; + +/// Result of decoding address from string +pub type Result = std::result::Result; + /// Fixed-length address strings prefix for established addresses. const PREFIX_ESTABLISHED: &str = "est"; /// Fixed-length address strings prefix for implicit addresses. @@ -80,24 +80,6 @@ const PREFIX_INTERNAL: &str = "ano"; /// Fixed-length address strings prefix for IBC addresses. const PREFIX_IBC: &str = "ibc"; -#[allow(missing_docs)] -#[derive(Error, Debug)] -pub enum DecodeError { - #[error("Error decoding address from Bech32m: {0}")] - DecodeBech32(bech32::Error), - #[error("Error decoding address from base32: {0}")] - DecodeBase32(bech32::Error), - #[error("Unexpected Bech32m human-readable part {0}, expected {1}")] - UnexpectedBech32Prefix(String, String), - #[error("Unexpected Bech32m variant {0:?}, expected {BECH32M_VARIANT:?}")] - UnexpectedBech32Variant(bech32::Variant), - #[error("Invalid address encoding")] - InvalidInnerEncoding(std::io::Error), -} - -/// Result of a function that may fail -pub type Result = std::result::Result; - /// An account's address #[derive( Clone, @@ -122,34 +104,12 @@ pub enum Address { impl Address { /// Encode an address with Bech32m encoding pub fn encode(&self) -> String { - let bytes = self.to_fixed_len_string(); - bech32::encode(ADDRESS_HRP, bytes.to_base32(), BECH32M_VARIANT) - .unwrap_or_else(|_| { - panic!( - "The human-readable part {} should never cause a failure", - ADDRESS_HRP - ) - }) + string_encoding::Format::encode(self) } /// Decode an address from Bech32m encoding pub fn decode(string: impl AsRef) -> Result { - let (prefix, hash_base32, variant) = bech32::decode(string.as_ref()) - .map_err(DecodeError::DecodeBech32)?; - if prefix != ADDRESS_HRP { - return Err(DecodeError::UnexpectedBech32Prefix( - prefix, - ADDRESS_HRP.into(), - )); - } - match variant { - BECH32M_VARIANT => {} - _ => return Err(DecodeError::UnexpectedBech32Variant(variant)), - } - let bytes: Vec = FromBase32::from_base32(&hash_base32) - .map_err(DecodeError::DecodeBase32)?; - Self::try_from_fixed_len_string(&mut &bytes[..]) - .map_err(DecodeError::InvalidInnerEncoding) + string_encoding::Format::decode(string) } /// Try to get a raw hash of an address, only defined for established and @@ -163,7 +123,7 @@ impl Address { } /// Convert an address to a fixed length 7-bit ascii string bytes - fn to_fixed_len_string(&self) -> Vec { + pub fn to_fixed_len_string(&self) -> Vec { let mut string = match self { Address::Established(EstablishedAddress { hash }) => { format!("{}::{}", PREFIX_ESTABLISHED, hash) @@ -209,7 +169,7 @@ impl Address { } /// Try to parse an address from fixed-length utf-8 encoded address string. - fn try_from_fixed_len_string(buf: &mut &[u8]) -> std::io::Result { + pub fn try_from_fixed_len_string(buf: &mut &[u8]) -> std::io::Result { use std::io::{Error, ErrorKind}; let string = std::str::from_utf8(buf) .map_err(|err| Error::new(ErrorKind::InvalidData, err))?; @@ -302,6 +262,20 @@ impl Address { } } +impl string_encoding::Format for Address { + const HRP: &'static str = string_encoding::ADDRESS_HRP; + + fn to_bytes(&self) -> Vec { + Self::to_fixed_len_string(self) + } + + fn decode_bytes(bytes: &[u8]) -> std::result::Result { + Self::try_from_fixed_len_string(&mut &bytes[..]) + } +} + +impl_display_and_from_str_via_format!(Address); + impl serde::Serialize for Address { fn serialize( &self, @@ -326,26 +300,12 @@ impl<'de> serde::Deserialize<'de> for Address { } } -impl Display for Address { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.encode()) - } -} - impl Debug for Address { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.pretty_fmt(f) } } -impl FromStr for Address { - type Err = DecodeError; - - fn from_str(s: &str) -> Result { - Address::decode(s) - } -} - /// An established address is generated on-chain #[derive( Debug, diff --git a/core/src/types/key/common.rs b/core/src/types/key/common.rs index b0da165bd94..8f933677436 100644 --- a/core/src/types/key/common.rs +++ b/core/src/types/key/common.rs @@ -14,6 +14,8 @@ use super::{ ParseSignatureError, RefTo, SchemeType, SigScheme as SigSchemeTrait, VerifySigError, }; +use crate::impl_display_and_from_str_via_format; +use crate::types::string_encoding; /// Public key #[derive( @@ -66,24 +68,23 @@ impl super::PublicKey for PublicKey { } } -impl Display for PublicKey { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", HEXLOWER.encode(&self.try_to_vec().unwrap())) - } -} +/// String decoding error +pub type DecodeError = string_encoding::DecodeError; -impl FromStr for PublicKey { - type Err = ParsePublicKeyError; +impl string_encoding::Format for PublicKey { + const HRP: &'static str = string_encoding::COMMON_PK_HRP; - fn from_str(str: &str) -> Result { - let vec = HEXLOWER - .decode(str.as_ref()) - .map_err(ParsePublicKeyError::InvalidHex)?; - Self::try_from_slice(vec.as_slice()) - .map_err(ParsePublicKeyError::InvalidEncoding) + fn to_bytes(&self) -> Vec { + BorshSerialize::try_to_vec(self).unwrap() + } + + fn decode_bytes(bytes: &[u8]) -> Result { + BorshDeserialize::try_from_slice(bytes) } } +impl_display_and_from_str_via_format!(PublicKey); + /// Secret key #[derive(Debug, Clone, BorshSerialize, BorshDeserialize, BorshSchema)] #[allow(clippy::large_enum_variant)] @@ -222,6 +223,20 @@ pub enum Signature { Secp256k1(secp256k1::Signature), } +impl string_encoding::Format for Signature { + const HRP: &'static str = string_encoding::COMMON_SIG_HRP; + + fn to_bytes(&self) -> Vec { + BorshSerialize::try_to_vec(self).unwrap() + } + + fn decode_bytes(bytes: &[u8]) -> Result { + BorshDeserialize::try_from_slice(bytes) + } +} + +impl_display_and_from_str_via_format!(Signature); + impl super::Signature for Signature { const TYPE: SchemeType = SigScheme::TYPE; @@ -330,3 +345,22 @@ impl super::SigScheme for SigScheme { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::key::ed25519; + + /// Run `cargo test gen_ed25519_keypair -- --nocapture` to generate a + /// new ed25519 keypair wrapped in `common` key types. + #[test] + fn gen_ed25519_keypair() { + let secret_key = + SecretKey::Ed25519(crate::types::key::testing::gen_keypair::< + ed25519::SigScheme, + >()); + let public_key = secret_key.to_public(); + println!("Public key: {}", public_key); + println!("Secret key: {}", secret_key); + } +} diff --git a/core/src/types/key/dkg_session_keys.rs b/core/src/types/key/dkg_session_keys.rs index f2cafb639ca..15f299cf42b 100644 --- a/core/src/types/key/dkg_session_keys.rs +++ b/core/src/types/key/dkg_session_keys.rs @@ -1,18 +1,16 @@ //! Utilities around the DKG session keys use std::cmp::Ordering; -use std::fmt::Display; use std::io::{Error, ErrorKind}; -use std::str::FromStr; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use data_encoding::HEXLOWER; use serde::{Deserialize, Serialize}; +use crate::impl_display_and_from_str_via_format; use crate::types::address::Address; -use crate::types::key::ParsePublicKeyError; use crate::types::storage::{DbKeySeg, Key, KeySeg}; +use crate::types::string_encoding; use crate::types::transaction::EllipticCurve; /// A keypair used in the DKG protocol @@ -138,27 +136,21 @@ impl BorshSchema for DkgPublicKey { } } -impl Display for DkgPublicKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let vec = self - .try_to_vec() - .expect("Encoding public key shouldn't fail"); - write!(f, "{}", HEXLOWER.encode(&vec)) - } -} +impl string_encoding::Format for DkgPublicKey { + const HRP: &'static str = string_encoding::DKG_PK_HRP; -impl FromStr for DkgPublicKey { - type Err = ParsePublicKeyError; + fn to_bytes(&self) -> Vec { + self.try_to_vec() + .expect("Encoding public key shouldn't fail") + } - fn from_str(s: &str) -> Result { - let vec = HEXLOWER - .decode(s.as_ref()) - .map_err(ParsePublicKeyError::InvalidHex)?; - BorshDeserialize::try_from_slice(&vec) - .map_err(ParsePublicKeyError::InvalidEncoding) + fn decode_bytes(bytes: &[u8]) -> Result { + BorshDeserialize::try_from_slice(bytes) } } +impl_display_and_from_str_via_format!(DkgPublicKey); + /// Obtain a storage key for user's public dkg session key. pub fn dkg_pk_key(owner: &Address) -> Key { Key::from(owner.to_db_key()) diff --git a/core/src/types/key/mod.rs b/core/src/types/key/mod.rs index 157b0f4f5b4..16ce79eec95 100644 --- a/core/src/types/key/mod.rs +++ b/core/src/types/key/mod.rs @@ -187,7 +187,6 @@ pub trait PublicKey: + Display + Debug + PartialOrd - + FromStr + Hash + Send + Sync diff --git a/core/src/types/masp.rs b/core/src/types/masp.rs index c0ccb67d1ee..7a0fd49205b 100644 --- a/core/src/types/masp.rs +++ b/core/src/types/masp.rs @@ -8,17 +8,14 @@ use bech32::{FromBase32, ToBase32}; use borsh::{BorshDeserialize, BorshSerialize}; use sha2::{Digest, Sha256}; -use crate::types::address::{ - masp, Address, DecodeError, BECH32M_VARIANT, HASH_LEN, +use crate::impl_display_and_from_str_via_format; +use crate::types::address::{masp, Address, DecodeError, HASH_LEN}; +use crate::types::string_encoding::{ + self, BECH32M_VARIANT, MASP_EXT_FULL_VIEWING_KEY_HRP, + MASP_EXT_SPENDING_KEY_HRP, MASP_PAYMENT_ADDRESS_HRP, + MASP_PINNED_PAYMENT_ADDRESS_HRP, }; -/// human-readable part of Bech32m encoded address -// TODO remove "test" suffix for live network -const EXT_FULL_VIEWING_KEY_HRP: &str = "xfvktest"; -const PAYMENT_ADDRESS_HRP: &str = "patest"; -const PINNED_PAYMENT_ADDRESS_HRP: &str = "ppatest"; -const EXT_SPENDING_KEY_HRP: &str = "xsktest"; - /// Wrapper for masp_primitive's FullViewingKey #[derive( Clone, @@ -34,51 +31,99 @@ const EXT_SPENDING_KEY_HRP: &str = "xsktest"; )] pub struct ExtendedViewingKey(masp_primitives::zip32::ExtendedFullViewingKey); -impl Display for ExtendedViewingKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl ExtendedViewingKey { + /// Encode `Self` to bytes + pub fn to_bytes(&self) -> Vec { let mut bytes = [0; 169]; self.0 .write(&mut bytes[..]) .expect("should be able to serialize an ExtendedFullViewingKey"); - let encoded = bech32::encode( - EXT_FULL_VIEWING_KEY_HRP, - bytes.to_base32(), - BECH32M_VARIANT, + bytes.to_vec() + } + + /// Try to decode `Self` from bytes + pub fn decode_bytes(bytes: &[u8]) -> Result { + masp_primitives::zip32::ExtendedFullViewingKey::read(&mut &bytes[..]) + .map(Self) + } +} + +impl string_encoding::Format for ExtendedViewingKey { + const HRP: &'static str = MASP_EXT_FULL_VIEWING_KEY_HRP; + + fn to_bytes(&self) -> Vec { + self.to_bytes() + } + + fn decode_bytes(bytes: &[u8]) -> Result { + Self::decode_bytes(bytes) + } +} + +impl_display_and_from_str_via_format!(ExtendedViewingKey); + +impl string_encoding::Format for PaymentAddress { + const HRP: &'static str = MASP_PAYMENT_ADDRESS_HRP; + + fn to_bytes(&self) -> Vec { + self.to_bytes() + } + + fn decode_bytes(_bytes: &[u8]) -> Result { + unimplemented!( + "Cannot determine if the PaymentAddress is pinned from bytes. Use \ + `PaymentAddress::decode_bytes(bytes, is_pinned)` instead." ) - .unwrap_or_else(|_| { + } + + // We override `encode` because we need to determine whether the address + // is pinned from its HRP + fn encode(&self) -> String { + let hrp = if self.is_pinned() { + MASP_PINNED_PAYMENT_ADDRESS_HRP + } else { + MASP_PAYMENT_ADDRESS_HRP + }; + let base32 = self.to_bytes().to_base32(); + bech32::encode(hrp, base32, BECH32M_VARIANT).unwrap_or_else(|_| { panic!( "The human-readable part {} should never cause a failure", - EXT_FULL_VIEWING_KEY_HRP + hrp ) - }); - write!(f, "{encoded}") + }) } -} - -impl FromStr for ExtendedViewingKey { - type Err = DecodeError; - fn from_str(string: &str) -> Result { - let (prefix, base32, variant) = - bech32::decode(string).map_err(DecodeError::DecodeBech32)?; - if prefix != EXT_FULL_VIEWING_KEY_HRP { - return Err(DecodeError::UnexpectedBech32Prefix( + // We override `decode` because we need to use different HRP for pinned and + // non-pinned address + fn decode( + string: impl AsRef, + ) -> Result { + let (prefix, base32, variant) = bech32::decode(string.as_ref()) + .map_err(DecodeError::DecodeBech32)?; + let is_pinned = if prefix == MASP_PAYMENT_ADDRESS_HRP { + false + } else if prefix == MASP_PINNED_PAYMENT_ADDRESS_HRP { + true + } else { + return Err(DecodeError::UnexpectedBech32Hrp( prefix, - EXT_FULL_VIEWING_KEY_HRP.into(), + MASP_PAYMENT_ADDRESS_HRP.into(), )); - } + }; match variant { BECH32M_VARIANT => {} _ => return Err(DecodeError::UnexpectedBech32Variant(variant)), } let bytes: Vec = FromBase32::from_base32(&base32) .map_err(DecodeError::DecodeBase32)?; - masp_primitives::zip32::ExtendedFullViewingKey::read(&mut &bytes[..]) - .map_err(DecodeError::InvalidInnerEncoding) - .map(Self) + + PaymentAddress::decode_bytes(&bytes, is_pinned) + .map_err(DecodeError::InvalidBytes) } } +impl_display_and_from_str_via_format!(PaymentAddress); + impl From for masp_primitives::zip32::ExtendedFullViewingKey { @@ -155,78 +200,45 @@ impl PaymentAddress { // hex of the first 40 chars of the hash format!("{:.width$X}", hasher.finalize(), width = HASH_LEN) } -} -impl From for masp_primitives::primitives::PaymentAddress { - fn from(addr: PaymentAddress) -> Self { - addr.0 - } -} - -impl From for PaymentAddress { - fn from(addr: masp_primitives::primitives::PaymentAddress) -> Self { - Self(addr, false) - } -} - -impl Display for PaymentAddress { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let bytes = self.0.to_bytes(); - let hrp = if self.1 { - PINNED_PAYMENT_ADDRESS_HRP - } else { - PAYMENT_ADDRESS_HRP - }; - let encoded = bech32::encode(hrp, bytes.to_base32(), BECH32M_VARIANT) - .unwrap_or_else(|_| { - panic!( - "The human-readable part {} should never cause a failure", - PAYMENT_ADDRESS_HRP - ) - }); - write!(f, "{encoded}") + /// Encode `Self` to bytes + pub fn to_bytes(&self) -> Vec { + self.0.to_bytes().to_vec() } -} - -impl FromStr for PaymentAddress { - type Err = DecodeError; - fn from_str(string: &str) -> Result { - let (prefix, base32, variant) = - bech32::decode(string).map_err(DecodeError::DecodeBech32)?; - let pinned = if prefix == PAYMENT_ADDRESS_HRP { - false - } else if prefix == PINNED_PAYMENT_ADDRESS_HRP { - true - } else { - return Err(DecodeError::UnexpectedBech32Prefix( - prefix, - PAYMENT_ADDRESS_HRP.into(), - )); - }; - match variant { - BECH32M_VARIANT => {} - _ => return Err(DecodeError::UnexpectedBech32Variant(variant)), - } + /// Try to decode `Self` from bytes + pub fn decode_bytes( + bytes: &[u8], + is_pinned: bool, + ) -> Result { let addr_len_err = |_| { - DecodeError::InvalidInnerEncoding(Error::new( + Error::new( ErrorKind::InvalidData, "expected 43 bytes for the payment address", - )) + ) }; let addr_data_err = || { - DecodeError::InvalidInnerEncoding(Error::new( + Error::new( ErrorKind::InvalidData, "invalid payment address provided", - )) + ) }; - let bytes: Vec = FromBase32::from_base32(&base32) - .map_err(DecodeError::DecodeBase32)?; - masp_primitives::primitives::PaymentAddress::from_bytes( - &bytes.try_into().map_err(addr_len_err)?, - ) - .ok_or_else(addr_data_err) - .map(|x| Self(x, pinned)) + let bytes: &[u8; 43] = &bytes.try_into().map_err(addr_len_err)?; + masp_primitives::primitives::PaymentAddress::from_bytes(bytes) + .ok_or_else(addr_data_err) + .map(|addr| Self(addr, is_pinned)) + } +} + +impl From for masp_primitives::primitives::PaymentAddress { + fn from(addr: PaymentAddress) -> Self { + addr.0 + } +} + +impl From for PaymentAddress { + fn from(addr: masp_primitives::primitives::PaymentAddress) -> Self { + Self(addr, false) } } @@ -258,51 +270,25 @@ impl<'de> serde::Deserialize<'de> for PaymentAddress { #[derive(Clone, Debug, Copy, BorshSerialize, BorshDeserialize)] pub struct ExtendedSpendingKey(masp_primitives::zip32::ExtendedSpendingKey); -impl Display for ExtendedSpendingKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl string_encoding::Format for ExtendedSpendingKey { + const HRP: &'static str = MASP_EXT_SPENDING_KEY_HRP; + + fn to_bytes(&self) -> Vec { let mut bytes = [0; 169]; self.0 .write(&mut &mut bytes[..]) .expect("should be able to serialize an ExtendedSpendingKey"); - let encoded = bech32::encode( - EXT_SPENDING_KEY_HRP, - bytes.to_base32(), - BECH32M_VARIANT, - ) - .unwrap_or_else(|_| { - panic!( - "The human-readable part {} should never cause a failure", - EXT_SPENDING_KEY_HRP - ) - }); - write!(f, "{encoded}") + bytes.to_vec() } -} -impl FromStr for ExtendedSpendingKey { - type Err = DecodeError; - - fn from_str(string: &str) -> Result { - let (prefix, base32, variant) = - bech32::decode(string).map_err(DecodeError::DecodeBech32)?; - if prefix != EXT_SPENDING_KEY_HRP { - return Err(DecodeError::UnexpectedBech32Prefix( - prefix, - EXT_SPENDING_KEY_HRP.into(), - )); - } - match variant { - BECH32M_VARIANT => {} - _ => return Err(DecodeError::UnexpectedBech32Variant(variant)), - } - let bytes: Vec = FromBase32::from_base32(&base32) - .map_err(DecodeError::DecodeBase32)?; + fn decode_bytes(bytes: &[u8]) -> Result { masp_primitives::zip32::ExtendedSpendingKey::read(&mut &bytes[..]) - .map_err(DecodeError::InvalidInnerEncoding) .map(Self) } } +impl_display_and_from_str_via_format!(ExtendedSpendingKey); + impl From for masp_primitives::zip32::ExtendedSpendingKey { fn from(key: ExtendedSpendingKey) -> Self { key.0 diff --git a/core/src/types/mod.rs b/core/src/types/mod.rs index 05500604985..2c8af14d324 100644 --- a/core/src/types/mod.rs +++ b/core/src/types/mod.rs @@ -9,6 +9,7 @@ pub mod internal; pub mod key; pub mod masp; pub mod storage; +pub mod string_encoding; pub mod time; pub mod token; pub mod transaction; diff --git a/core/src/types/string_encoding.rs b/core/src/types/string_encoding.rs new file mode 100644 index 00000000000..4dcfc8dca37 --- /dev/null +++ b/core/src/types/string_encoding.rs @@ -0,0 +1,127 @@ +//! Namada's standard string encoding for public types. +//! +//! We're using [bech32m](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki), +//! a format with a human-readable, followed by base32 encoding with a limited +//! character set with checksum check. +//! +//! To use this encoding for a new type, add a HRP (human-readable part) const +//! below and use it to `impl string_encoding::Format for YourType`. + +use bech32::{self, FromBase32, ToBase32, Variant}; +use thiserror::Error; + +/// We're using "Bech32m" variant +pub const BECH32M_VARIANT: bech32::Variant = Variant::Bech32m; + +// Human-readable parts of Bech32m encoding +// +// Invariant: HRPs must be unique !!! +// +// TODO: remove "test" suffix for live network +/// `Address` human-readable part +pub const ADDRESS_HRP: &str = "atest"; +/// MASP extended viewing key human-readable part +pub const MASP_EXT_FULL_VIEWING_KEY_HRP: &str = "xfvktest"; +/// MASP payment address (not pinned) human-readable part +pub const MASP_PAYMENT_ADDRESS_HRP: &str = "patest"; +/// MASP pinned payment address human-readable part +pub const MASP_PINNED_PAYMENT_ADDRESS_HRP: &str = "ppatest"; +/// MASP extended spending key human-readable part +pub const MASP_EXT_SPENDING_KEY_HRP: &str = "xsktest"; +/// `common::PublicKey` human-readable part +pub const COMMON_PK_HRP: &str = "pktest"; +/// `DkgPublicKey` human-readable part +pub const DKG_PK_HRP: &str = "dpktest"; +/// `common::Signature` human-readable part +pub const COMMON_SIG_HRP: &str = "sigtest"; + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum DecodeError { + #[error("Error decoding from Bech32m: {0}")] + DecodeBech32(bech32::Error), + #[error("Error decoding from base32: {0}")] + DecodeBase32(bech32::Error), + #[error("Unexpected Bech32m human-readable part {0}, expected {1}")] + UnexpectedBech32Hrp(String, String), + #[error("Unexpected Bech32m variant {0:?}, expected {BECH32M_VARIANT:?}")] + UnexpectedBech32Variant(bech32::Variant), + #[error("Invalid bytes: {0}")] + InvalidBytes(std::io::Error), +} + +/// Format to string with bech32m +pub trait Format: Sized { + /// Human-readable part + const HRP: &'static str; + + /// Encode `Self` to a string + fn encode(&self) -> String { + let base32 = self.to_bytes().to_base32(); + bech32::encode(Self::HRP, base32, BECH32M_VARIANT).unwrap_or_else( + |_| { + panic!( + "The human-readable part {} should never cause a failure", + Self::HRP + ) + }, + ) + } + + /// Try to decode `Self` from a string + fn decode(string: impl AsRef) -> Result { + let (hrp, hash_base32, variant) = bech32::decode(string.as_ref()) + .map_err(DecodeError::DecodeBech32)?; + if hrp != Self::HRP { + return Err(DecodeError::UnexpectedBech32Hrp( + hrp, + Self::HRP.into(), + )); + } + match variant { + BECH32M_VARIANT => {} + _ => return Err(DecodeError::UnexpectedBech32Variant(variant)), + } + let bytes: Vec = FromBase32::from_base32(&hash_base32) + .map_err(DecodeError::DecodeBase32)?; + + Self::decode_bytes(&bytes).map_err(DecodeError::InvalidBytes) + } + + /// Encode `Self` to bytes + fn to_bytes(&self) -> Vec; + + /// Try to decode `Self` from bytes + fn decode_bytes(bytes: &[u8]) -> Result; +} + +/// Implement [`std::fmt::Display`] and [`std::str::FromStr`] via +/// [`Format`]. +#[macro_export] +macro_rules! impl_display_and_from_str_via_format { + ($t:path) => { + impl std::fmt::Display for $t { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + $crate::types::string_encoding::Format::encode(self) + ) + } + } + + impl std::str::FromStr for $t { + type Err = $crate::types::string_encoding::DecodeError; + + fn from_str(s: &str) -> std::result::Result { + $crate::types::string_encoding::Format::decode(s) + } + } + }; +} + +/// Get the length of the human-readable part +// Not in the `Format` trait, cause functions in traits cannot be const +pub const fn hrp_len() -> usize { + T::HRP.len() +} diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 94371035469..41a23fe01a7 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -379,8 +379,8 @@ fn ledger_txs_and_queries() -> Result<()> { "--source", BERTHA, "--public-key", - // Value obtained from `namada::types::key::ed25519::tests::gen_keypair` - "001be519a321e29020fa3cbfbfd01bd5e92db134305609270b71dace25b5a21168", + // Value obtained from `cargo test gen_ed25519_keypair -- --nocapture` + "pktest1qzyhgvhrqaj5fut8n9j8wgtleysf4agfq5tnynecq3d63qqfy5a5y7e8exd", "--code-path", &vp_user, "--alias", @@ -394,8 +394,8 @@ fn ledger_txs_and_queries() -> Result<()> { "--ledger-address", &validator_one_rpc, ], - // 6. Submit a tx to withdraw from faucet account (requires PoW challenge - // solution) + // 6. Submit a tx to withdraw from faucet account (requires PoW + // challenge solution) vec![ "transfer", "--source", diff --git a/wasm/checksums.json b/wasm/checksums.json index 7c2b824921b..24ab16164ea 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,20 +1,20 @@ { - "tx_bond.wasm": "tx_bond.f0094b887c57565472bede01d98fb77f6faac2f72597e2efb2ebfe9b1bf7c234.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.02dca468021b1ec811d0f35cc4b55a24f7c3f7b5e51f16399709257421f4a1f4.wasm", - "tx_ibc.wasm": "tx_ibc.a1735e3221f1ae055c74bb52327765dd37e8676e15fab496f9ab0ed4d0628f51.wasm", - "tx_init_account.wasm": "tx_init_account.7b6eafeceb81b679c382279a5d9c40dfd81fcf37e5a1940340355c9f55af1543.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.f2ed71fe70fc564e1d67e4e7d2ea25466327b62ba2eee18ece0021abff9e2c82.wasm", - "tx_init_validator.wasm": "tx_init_validator.fedcfaecaf37e3e7d050c76a4512baa399fc528710a27038573df53596613a2c.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.3e5417561e8108d4045775bf6d095cbaad22c73ff17a5ba2ad11a1821665a58a.wasm", - "tx_transfer.wasm": "tx_transfer.833a3849ca2c417f4e907c95c6eb15e6b52827458cf603e1c4f5511ab3e4fe76.wasm", - "tx_unbond.wasm": "tx_unbond.d4fd6c94abb947533a2728940b43fb021a008ad593c7df7a3886c4260cac39b5.wasm", - "tx_update_vp.wasm": "tx_update_vp.6d1eabab15dc6d04eec5b25ad687f026a4d6c3f118a1d7aca9232394496bd845.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.54b594f686a72869b0d7f15492591854f26e287a1cf3b6e543b0246d5ac61003.wasm", - "tx_withdraw.wasm": "tx_withdraw.342c222d0707eb5b5a44b89fc1245f527be3fdf841af64152a9ab35a4766e1b5.wasm", - "vp_implicit.wasm": "vp_implicit.73678ac01aa009ac4e0d4a49eecaa19b49cdd3d95f6862a9558c9b175ae68260.wasm", - "vp_masp.wasm": "vp_masp.85446251f8e1defed81549dab37edfe8e640339c7230e678b65340cf71ce1369.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.573b882a806266d6cdfa635fe803e46d6ce89c99321196c231c61d05193a086d.wasm", - "vp_token.wasm": "vp_token.8c6e5a86f047e7b1f1004f0d8a4e91fad1b1c0226a6e42d7fe350f98dc84359b.wasm", - "vp_user.wasm": "vp_user.75c68f018f163d18d398cb4082b261323d115aae43ec021c868d1128e4b0ee29.wasm", - "vp_validator.wasm": "vp_validator.2dc9f1c8f106deeef5ee988955733955444d16b400ebb16a25e7d71e4b1be874.wasm" + "tx_bond.wasm": "tx_bond.822087f85981e2e11f15cc1a4dd04b494586d0d0d2fbe729fe51943926aeac56.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.45c856fab718be65cad4fd301db8e4171268be386a130439973e064706f84a16.wasm", + "tx_ibc.wasm": "tx_ibc.8cf23e322426a9f67a4a7e6d3b782d2de735947da1ea9116b52b80902482b228.wasm", + "tx_init_account.wasm": "tx_init_account.d49cbf5e5b06e3409c60db2236e0fe53689212def1bb305aa55006b994ea9c33.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.514f6426c83ddac57f05b6a834c3575e54915f48faeaa944f4b1aec614f678de.wasm", + "tx_init_validator.wasm": "tx_init_validator.57a47bd691bae2b1c9e92cc4819b331a71091e90c4de972a0fb578fa63d4c2f9.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.425ba4ec10f9fb438c9842ac14ee80f178b892b2d2219dcde21bfe842174c5c3.wasm", + "tx_transfer.wasm": "tx_transfer.1b6d9bd6e7cd1451af5b549ebfe099c983b34402def2570992ba1d169758c32c.wasm", + "tx_unbond.wasm": "tx_unbond.a9dcd42ce3221870aea62303bf797ec8d0728913e398f8edf5af1e6520669c0d.wasm", + "tx_update_vp.wasm": "tx_update_vp.c55982e274b48d6c693a63fa1d41c398ff6aab99cd08e99a21b76db9e63e9d2f.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.41e120d0c1ea5341b38a2c69ddf2ad04af8732bc87f782d7f8e33f589098b56b.wasm", + "tx_withdraw.wasm": "tx_withdraw.54d0502d0b6f7424bc7afa171b9c2fb2cad8fd87eac96918067aee9ecb5187ef.wasm", + "vp_implicit.wasm": "vp_implicit.8fac8f5e73bbc6d6ef990870bf05fb4b211b8fd1c2787737b8940cfe4ac33d89.wasm", + "vp_masp.wasm": "vp_masp.8665245ea81977f1a644f402c689f98a85f76eb255170f17b07b85182d0aba83.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.bbd44e460676c1174cd19804a11c2b45f0bc59c3921c636ea0a5b2fd1bf21309.wasm", + "vp_token.wasm": "vp_token.c82d6534b543a29b63858edf2e12fd3094dde96ce9edfffb42a9c3c792c46b64.wasm", + "vp_user.wasm": "vp_user.35422c1cfe447b0a7de40c45509b5ecdbfdd5c87f8a6ec76dd500bed713851cc.wasm", + "vp_validator.wasm": "vp_validator.5c11a9832a19d88ab36d5c1213026a255fc31c10b5124936662aac421b334212.wasm" } \ No newline at end of file