diff --git a/Cargo.lock b/Cargo.lock index 1a0a9ae50b..c55e818508 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1885,7 +1885,7 @@ dependencies = [ [[package]] name = "equihash" version = "0.1.0" -source = "git+https://github.com/zcash/librustzcash/?rev=2425a08#2425a0869098e3b0588ccd73c42716bcf418612c" +source = "git+https://github.com/zcash/librustzcash?rev=2425a08#2425a0869098e3b0588ccd73c42716bcf418612c" dependencies = [ "blake2b_simd 1.0.0", "byteorder", @@ -3638,8 +3638,10 @@ name = "namada" version = "0.12.0" dependencies = [ "assert_matches", + "async-std", "async-trait", "bellman", + "bimap", "bls12_381", "borsh", "byte-unit", @@ -3658,26 +3660,28 @@ dependencies = [ "masp_proofs", "namada_core", "namada_proof_of_stake", + "orion", "parity-wasm", "paste", "pretty_assertions", "proptest", "prost", "pwasm-utils", + "rand 0.8.5", + "rand_core 0.6.4", "rayon", "rust_decimal", + "serde 1.0.147", "serde_json", "sha2 0.9.9", "tempfile", - "tendermint 0.23.5", "tendermint 0.23.6", - "tendermint-proto 0.23.5", "tendermint-proto 0.23.6", - "tendermint-rpc 0.23.5", "tendermint-rpc 0.23.6", "test-log", "thiserror", "tokio", + "toml", "tracing 0.1.37", "tracing-subscriber 0.3.16", "wasmer", @@ -6131,7 +6135,7 @@ dependencies = [ [[package]] name = "tendermint" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=679227ab920dbb45cdd5acb97f49c4fe2ebb5a44#679227ab920dbb45cdd5acb97f49c4fe2ebb5a44" dependencies = [ "async-trait", "bytes 1.2.1", @@ -6174,7 +6178,7 @@ dependencies = [ [[package]] name = "tendermint-config" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=679227ab920dbb45cdd5acb97f49c4fe2ebb5a44#679227ab920dbb45cdd5acb97f49c4fe2ebb5a44" dependencies = [ "flex-error", "serde 1.0.147", @@ -6187,7 +6191,7 @@ dependencies = [ [[package]] name = "tendermint-light-client" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=679227ab920dbb45cdd5acb97f49c4fe2ebb5a44#679227ab920dbb45cdd5acb97f49c4fe2ebb5a44" dependencies = [ "contracts", "crossbeam-channel 0.4.4", @@ -6221,7 +6225,7 @@ dependencies = [ [[package]] name = "tendermint-light-client-verifier" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=679227ab920dbb45cdd5acb97f49c4fe2ebb5a44#679227ab920dbb45cdd5acb97f49c4fe2ebb5a44" dependencies = [ "derive_more", "flex-error", @@ -6250,7 +6254,7 @@ dependencies = [ [[package]] name = "tendermint-proto" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=679227ab920dbb45cdd5acb97f49c4fe2ebb5a44#679227ab920dbb45cdd5acb97f49c4fe2ebb5a44" dependencies = [ "bytes 1.2.1", "flex-error", @@ -6300,7 +6304,7 @@ dependencies = [ [[package]] name = "tendermint-rpc" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=679227ab920dbb45cdd5acb97f49c4fe2ebb5a44#679227ab920dbb45cdd5acb97f49c4fe2ebb5a44" dependencies = [ "async-trait", "async-tungstenite", @@ -6348,7 +6352,7 @@ dependencies = [ [[package]] name = "tendermint-testgen" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=679227ab920dbb45cdd5acb97f49c4fe2ebb5a44#679227ab920dbb45cdd5acb97f49c4fe2ebb5a44" dependencies = [ "ed25519-dalek", "gumdrop", @@ -8018,7 +8022,7 @@ dependencies = [ [[package]] name = "zcash_encoding" version = "0.0.0" -source = "git+https://github.com/zcash/librustzcash/?rev=2425a08#2425a0869098e3b0588ccd73c42716bcf418612c" +source = "git+https://github.com/zcash/librustzcash?rev=2425a08#2425a0869098e3b0588ccd73c42716bcf418612c" dependencies = [ "byteorder", "nonempty", @@ -8039,7 +8043,7 @@ dependencies = [ [[package]] name = "zcash_note_encryption" version = "0.1.0" -source = "git+https://github.com/zcash/librustzcash/?rev=2425a08#2425a0869098e3b0588ccd73c42716bcf418612c" +source = "git+https://github.com/zcash/librustzcash?rev=2425a08#2425a0869098e3b0588ccd73c42716bcf418612c" dependencies = [ "chacha20", "chacha20poly1305", @@ -8050,7 +8054,7 @@ dependencies = [ [[package]] name = "zcash_primitives" version = "0.5.0" -source = "git+https://github.com/zcash/librustzcash/?rev=2425a08#2425a0869098e3b0588ccd73c42716bcf418612c" +source = "git+https://github.com/zcash/librustzcash?rev=2425a08#2425a0869098e3b0588ccd73c42716bcf418612c" dependencies = [ "aes", "bip0039", @@ -8076,13 +8080,13 @@ dependencies = [ "sha2 0.9.9", "subtle", "zcash_encoding", - "zcash_note_encryption 0.1.0 (git+https://github.com/zcash/librustzcash/?rev=2425a08)", + "zcash_note_encryption 0.1.0 (git+https://github.com/zcash/librustzcash?rev=2425a08)", ] [[package]] name = "zcash_proofs" version = "0.5.0" -source = "git+https://github.com/zcash/librustzcash/?rev=2425a08#2425a0869098e3b0588ccd73c42716bcf418612c" +source = "git+https://github.com/zcash/librustzcash?rev=2425a08#2425a0869098e3b0588ccd73c42716bcf418612c" dependencies = [ "bellman", "blake2b_simd 1.0.0", diff --git a/Cargo.toml b/Cargo.toml index 42a99343fc..9a769cd2d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,13 +36,13 @@ async-process = {git = "https://github.com/heliaxdev/async-process.git", rev = " # borsh-schema-derive-internal = {path = "../borsh-rs/borsh-schema-derive-internal"} # patched to a commit on the `eth-bridge-integration+consensus-timeout` branch of our fork -tendermint = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} -tendermint-config = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} -tendermint-proto = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} -tendermint-rpc = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} -tendermint-testgen = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} -tendermint-light-client = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} -tendermint-light-client-verifier = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} +tendermint = {git="https://github.com/heliaxdev/tendermint-rs.git", rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} +tendermint-config = {git="https://github.com/heliaxdev/tendermint-rs.git", rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} +tendermint-proto = {git="https://github.com/heliaxdev/tendermint-rs.git", rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} +tendermint-rpc = {git="https://github.com/heliaxdev/tendermint-rs.git", rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} +tendermint-testgen = {git="https://github.com/heliaxdev/tendermint-rs.git", rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} +tendermint-light-client = {git="https://github.com/heliaxdev/tendermint-rs.git", rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} +tendermint-light-client-verifier = {git="https://github.com/heliaxdev/tendermint-rs.git", rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} # patched to a commit on the `eth-bridge-integration` branch of our fork ibc = {git = "https://github.com/heliaxdev/ibc-rs.git", rev = "f4703dfe2c1f25cc431279ab74f10f3e0f6827e2"} diff --git a/apps/Cargo.toml b/apps/Cargo.toml index bba5ca030e..e33cf51d99 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -67,7 +67,7 @@ abciplus = [ ] [dependencies] -namada = {path = "../shared", default-features = false, features = ["wasm-runtime", "ferveo-tpke"]} +namada = {path = "../shared", default-features = false, features = ["wasm-runtime", "ferveo-tpke", "masp-tx-gen"]} ark-serialize = "0.3.0" ark-std = "0.3.0" # branch = "bat/arse-merkle-tree" diff --git a/apps/src/bin/namada-client/cli.rs b/apps/src/bin/namada-client/cli.rs index 770928c7c0..e18e317676 100644 --- a/apps/src/bin/namada-client/cli.rs +++ b/apps/src/bin/namada-client/cli.rs @@ -2,95 +2,283 @@ use color_eyre::eyre::Result; use namada_apps::cli; +use namada_apps::cli::args::CliToSdk; use namada_apps::cli::cmds::*; use namada_apps::client::{rpc, tx, utils}; +use namada_apps::wallet::CliWalletUtils; +use tendermint_rpc::HttpClient; pub async fn main() -> Result<()> { match cli::namada_client_cli()? { cli::NamadaClient::WithContext(cmd_box) => { - let (cmd, ctx) = *cmd_box; + let (cmd, mut ctx) = *cmd_box; use NamadaClientWithContext as Sub; match cmd { // Ledger cmds Sub::TxCustom(TxCustom(args)) => { - tx::submit_custom(ctx, args).await; + let client = + HttpClient::new(args.tx.ledger_address.clone()) + .unwrap(); + let args = args.to_sdk(&mut ctx); + let dry_run = args.tx.dry_run; + tx::submit_custom::( + &client, + &mut ctx.wallet, + args, + ) + .await?; + if !dry_run { + namada_apps::wallet::save(&ctx.wallet) + .unwrap_or_else(|err| eprintln!("{}", err)); + } else { + println!( + "Transaction dry run. No addresses have been \ + saved." + ) + } } Sub::TxTransfer(TxTransfer(args)) => { - tx::submit_transfer(ctx, args).await; + let client = + HttpClient::new(args.tx.ledger_address.clone()) + .unwrap(); + let args = args.to_sdk(&mut ctx); + tx::submit_transfer::( + &client, + &mut ctx.wallet, + &mut ctx.shielded, + args, + ) + .await?; } Sub::TxIbcTransfer(TxIbcTransfer(args)) => { - tx::submit_ibc_transfer(ctx, args).await; + let client = + HttpClient::new(args.tx.ledger_address.clone()) + .unwrap(); + let args = args.to_sdk(&mut ctx); + tx::submit_ibc_transfer::( + &client, + &mut ctx.wallet, + args, + ) + .await?; } Sub::TxUpdateVp(TxUpdateVp(args)) => { - tx::submit_update_vp(ctx, args).await; + let client = + HttpClient::new(args.tx.ledger_address.clone()) + .unwrap(); + let args = args.to_sdk(&mut ctx); + tx::submit_update_vp::( + &client, + &mut ctx.wallet, + args, + ) + .await?; } Sub::TxInitAccount(TxInitAccount(args)) => { - tx::submit_init_account(ctx, args).await; + let client = + HttpClient::new(args.tx.ledger_address.clone()) + .unwrap(); + let args = args.to_sdk(&mut ctx); + let dry_run = args.tx.dry_run; + tx::submit_init_account::( + &client, + &mut ctx.wallet, + args, + ) + .await?; + if !dry_run { + namada_apps::wallet::save(&ctx.wallet) + .unwrap_or_else(|err| eprintln!("{}", err)); + } else { + println!( + "Transaction dry run. No addresses have been \ + saved." + ) + } } Sub::TxInitValidator(TxInitValidator(args)) => { - tx::submit_init_validator(ctx, args).await; + let client = + HttpClient::new(args.tx.ledger_address.clone()) + .unwrap(); + let args = args.to_sdk(&mut ctx); + tx::submit_init_validator::(&client, ctx, args) + .await; } Sub::TxInitProposal(TxInitProposal(args)) => { - tx::submit_init_proposal(ctx, args).await; + let client = + HttpClient::new(args.tx.ledger_address.clone()) + .unwrap(); + let args = args.to_sdk(&mut ctx); + tx::submit_init_proposal::(&client, ctx, args) + .await?; } Sub::TxVoteProposal(TxVoteProposal(args)) => { - tx::submit_vote_proposal(ctx, args).await; + let client = + HttpClient::new(args.tx.ledger_address.clone()) + .unwrap(); + let args = args.to_sdk(&mut ctx); + tx::submit_vote_proposal::( + &client, + &mut ctx.wallet, + args, + ) + .await?; } Sub::TxRevealPk(TxRevealPk(args)) => { - tx::submit_reveal_pk(ctx, args).await; + let client = + HttpClient::new(args.tx.ledger_address.clone()) + .unwrap(); + let args = args.to_sdk(&mut ctx); + tx::submit_reveal_pk::( + &client, + &mut ctx.wallet, + args, + ) + .await?; } Sub::Bond(Bond(args)) => { - tx::submit_bond(ctx, args).await; + let client = + HttpClient::new(args.tx.ledger_address.clone()) + .unwrap(); + let args = args.to_sdk(&mut ctx); + tx::submit_bond::( + &client, + &mut ctx.wallet, + args, + ) + .await?; } Sub::Unbond(Unbond(args)) => { - tx::submit_unbond(ctx, args).await; + let client = + HttpClient::new(args.tx.ledger_address.clone()) + .unwrap(); + let args = args.to_sdk(&mut ctx); + tx::submit_unbond::( + &client, + &mut ctx.wallet, + args, + ) + .await?; } Sub::Withdraw(Withdraw(args)) => { - tx::submit_withdraw(ctx, args).await; + let client = + HttpClient::new(args.tx.ledger_address.clone()) + .unwrap(); + let args = args.to_sdk(&mut ctx); + tx::submit_withdraw::( + &client, + &mut ctx.wallet, + args, + ) + .await?; } // Ledger queries Sub::QueryEpoch(QueryEpoch(args)) => { - rpc::query_epoch(args).await; + let client = HttpClient::new(args.ledger_address).unwrap(); + rpc::query_epoch(&client).await; } Sub::QueryTransfers(QueryTransfers(args)) => { - rpc::query_transfers(ctx, args).await; + let client = + HttpClient::new(args.query.ledger_address.clone()) + .unwrap(); + let args = args.to_sdk(&mut ctx); + rpc::query_transfers( + &client, + &mut ctx.wallet, + &mut ctx.shielded, + args, + ) + .await; } Sub::QueryConversions(QueryConversions(args)) => { - rpc::query_conversions(ctx, args).await; + let client = + HttpClient::new(args.query.ledger_address.clone()) + .unwrap(); + let args = args.to_sdk(&mut ctx); + rpc::query_conversions(&client, args).await; } Sub::QueryBlock(QueryBlock(args)) => { - rpc::query_block(args).await; + let client = + HttpClient::new(args.ledger_address.clone()).unwrap(); + rpc::query_block(&client).await; } Sub::QueryBalance(QueryBalance(args)) => { - rpc::query_balance(ctx, args).await; + let client = + HttpClient::new(args.query.ledger_address.clone()) + .unwrap(); + let args = args.to_sdk(&mut ctx); + rpc::query_balance( + &client, + &mut ctx.wallet, + &mut ctx.shielded, + args, + ) + .await; } Sub::QueryBonds(QueryBonds(args)) => { - rpc::query_bonds(ctx, args).await; + let client = + HttpClient::new(args.query.ledger_address.clone()) + .unwrap(); + let args = args.to_sdk(&mut ctx); + rpc::query_bonds(&client, args).await; } Sub::QueryBondedStake(QueryBondedStake(args)) => { - rpc::query_bonded_stake(ctx, args).await; + let client = + HttpClient::new(args.query.ledger_address.clone()) + .unwrap(); + let args = args.to_sdk(&mut ctx); + rpc::query_bonded_stake(&client, args).await; } Sub::QueryCommissionRate(QueryCommissionRate(args)) => { - rpc::query_commission_rate(ctx, args).await; + let client = + HttpClient::new(args.query.ledger_address.clone()) + .unwrap(); + let args = args.to_sdk(&mut ctx); + rpc::query_commission_rate(&client, args).await; } Sub::QuerySlashes(QuerySlashes(args)) => { - rpc::query_slashes(ctx, args).await; + let client = + HttpClient::new(args.query.ledger_address.clone()) + .unwrap(); + let args = args.to_sdk(&mut ctx); + rpc::query_slashes(&client, args).await; } Sub::QueryResult(QueryResult(args)) => { - rpc::query_result(ctx, args).await; + // Connect to the Tendermint server holding the transactions + let client = + HttpClient::new(args.query.ledger_address.clone()) + .unwrap(); + let args = args.to_sdk(&mut ctx); + rpc::query_result(&client, args).await; } Sub::QueryRawBytes(QueryRawBytes(args)) => { - rpc::query_raw_bytes(ctx, args).await; + let client = + HttpClient::new(args.query.ledger_address.clone()) + .unwrap(); + let args = args.to_sdk(&mut ctx); + rpc::query_raw_bytes(&client, args).await; } Sub::QueryProposal(QueryProposal(args)) => { - rpc::query_proposal(ctx, args).await; + let client = + HttpClient::new(args.query.ledger_address.clone()) + .unwrap(); + let args = args.to_sdk(&mut ctx); + rpc::query_proposal(&client, args).await; } Sub::QueryProposalResult(QueryProposalResult(args)) => { - rpc::query_proposal_result(ctx, args).await; + let client = + HttpClient::new(args.query.ledger_address.clone()) + .unwrap(); + let args = args.to_sdk(&mut ctx); + rpc::query_proposal_result(&client, args).await; } Sub::QueryProtocolParameters(QueryProtocolParameters(args)) => { - rpc::query_protocol_parameters(ctx, args).await; + let client = + HttpClient::new(args.query.ledger_address.clone()) + .unwrap(); + let args = args.to_sdk(&mut ctx); + rpc::query_protocol_parameters(&client, args).await; } } } diff --git a/apps/src/bin/namada-wallet/cli.rs b/apps/src/bin/namada-wallet/cli.rs index 82a994b0ac..a3673b6399 100644 --- a/apps/src/bin/namada-wallet/cli.rs +++ b/apps/src/bin/namada-wallet/cli.rs @@ -7,16 +7,20 @@ use borsh::BorshSerialize; use color_eyre::eyre::Result; use itertools::sorted; use masp_primitives::zip32::ExtendedFullViewingKey; +use namada::ledger::masp::find_valid_diversifier; +use namada::ledger::wallet::FindKeyError; use namada::types::key::*; use namada::types::masp::{MaspValue, PaymentAddress}; use namada_apps::cli; +use namada_apps::cli::args::CliToSdk; use namada_apps::cli::{args, cmds, Context}; -use namada_apps::client::tx::find_valid_diversifier; -use namada_apps::wallet::{DecryptionError, FindKeyError}; +use namada_apps::wallet::{ + read_and_confirm_pwd, CliWalletUtils, DecryptionError, +}; use rand_core::OsRng; pub fn main() -> Result<()> { - let (cmd, ctx) = cli::namada_wallet_cli()?; + let (cmd, mut ctx) = cli::namada_wallet_cli()?; match cmd { cmds::NamadaWallet::Key(sub) => match sub { cmds::WalletKey::Gen(cmds::KeyGen(args)) => { @@ -45,6 +49,7 @@ pub fn main() -> Result<()> { spending_key_gen(ctx, args) } cmds::WalletMasp::GenPayAddr(cmds::MaspGenPayAddr(args)) => { + let args = args.to_sdk(&mut ctx); payment_address_gen(ctx, args) } cmds::WalletMasp::AddAddrKey(cmds::MaspAddAddrKey(args)) => { @@ -79,7 +84,7 @@ fn address_key_find( println!("Viewing key: {}", viewing_key); if unsafe_show_secret { // Check if alias is also a spending key - match wallet.find_spending_key(&alias) { + match wallet.find_spending_key(&alias, None) { Ok(spending_key) => println!("Spending key: {}", spending_key), Err(FindKeyError::KeyNotFound) => {} Err(err) => eprintln!("{}", err), @@ -141,7 +146,7 @@ fn spending_keys_list( // Print those too if they are available and requested. if unsafe_show_secret { if let Some(spending_key) = spending_key_opt { - match spending_key.get(decrypt, None) { + match spending_key.get::(decrypt, None) { // Here the spending key is unencrypted or successfully // decrypted Ok(spending_key) => { @@ -199,8 +204,10 @@ fn spending_key_gen( ) { let mut wallet = ctx.wallet; let alias = alias.to_lowercase(); - let (alias, _key) = wallet.gen_spending_key(alias, unsafe_dont_encrypt); - wallet.save().unwrap_or_else(|err| eprintln!("{}", err)); + let password = read_and_confirm_pwd(unsafe_dont_encrypt); + let (alias, _key) = wallet.gen_spending_key(alias, password); + namada_apps::wallet::save(&wallet) + .unwrap_or_else(|err| eprintln!("{}", err)); println!( "Successfully added a spending key with alias: \"{}\"", alias @@ -209,7 +216,7 @@ 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, @@ -217,10 +224,7 @@ fn payment_address_gen( }: args::MaspPayAddrGen, ) { let alias = alias.to_lowercase(); - let viewing_key = - ExtendedFullViewingKey::from(ctx.get_cached(&viewing_key)) - .fvk - .vk; + 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) @@ -235,7 +239,8 @@ fn payment_address_gen( eprintln!("Payment address not added"); cli::safe_exit(1); }); - wallet.save().unwrap_or_else(|err| eprintln!("{}", err)); + namada_apps::wallet::save(&wallet) + .unwrap_or_else(|err| eprintln!("{}", err)); println!( "Successfully generated a payment address with the following alias: {}", alias, @@ -264,13 +269,10 @@ fn address_key_add( (alias, "viewing key") } MaspValue::ExtendedSpendingKey(spending_key) => { + let password = read_and_confirm_pwd(unsafe_dont_encrypt); let alias = ctx .wallet - .encrypt_insert_spending_key( - alias, - spending_key, - unsafe_dont_encrypt, - ) + .encrypt_insert_spending_key(alias, spending_key, password) .unwrap_or_else(|| { eprintln!("Spending key not added"); cli::safe_exit(1); @@ -288,7 +290,8 @@ fn address_key_add( (alias, "payment address") } }; - ctx.wallet.save().unwrap_or_else(|err| eprintln!("{}", err)); + namada_apps::wallet::save(&ctx.wallet) + .unwrap_or_else(|err| eprintln!("{}", err)); println!( "Successfully added a {} with the following alias to wallet: {}", typ, alias, @@ -306,8 +309,10 @@ fn key_and_address_gen( }: args::KeyAndAddressGen, ) { let mut wallet = ctx.wallet; - let (alias, _key) = wallet.gen_key(scheme, alias, unsafe_dont_encrypt); - wallet.save().unwrap_or_else(|err| eprintln!("{}", err)); + let password = read_and_confirm_pwd(unsafe_dont_encrypt); + let (alias, _key) = wallet.gen_key(scheme, alias, password); + namada_apps::wallet::save(&wallet) + .unwrap_or_else(|err| eprintln!("{}", err)); println!( "Successfully added a key and an address with alias: \"{}\"", alias @@ -326,7 +331,7 @@ fn key_find( ) { let mut wallet = ctx.wallet; let found_keypair = match public_key { - Some(pk) => wallet.find_key_by_pk(&pk), + Some(pk) => wallet.find_key_by_pk(&pk, None), None => { let alias = alias.or(value); match alias { @@ -337,7 +342,7 @@ fn key_find( ); cli::safe_exit(1) } - Some(alias) => wallet.find_key(alias.to_lowercase()), + Some(alias) => wallet.find_key(alias.to_lowercase(), None), } } }; @@ -385,7 +390,7 @@ fn key_list( if let Some(pkh) = pkh { writeln!(w, " Public key hash: {}", pkh).unwrap(); } - match stored_keypair.get(decrypt, None) { + match stored_keypair.get::(decrypt, None) { Ok(keypair) => { writeln!(w, " Public key: {}", keypair.ref_to()) .unwrap(); @@ -409,7 +414,7 @@ fn key_list( fn key_export(ctx: Context, args::KeyExport { alias }: args::KeyExport) { let mut wallet = ctx.wallet; wallet - .find_key(alias.to_lowercase()) + .find_key(alias.to_lowercase(), None) .map(|keypair| { let file_data = keypair .try_to_vec() @@ -488,7 +493,8 @@ fn address_add(ctx: Context, args: args::AddressAdd) { eprintln!("Address not added"); cli::safe_exit(1); } - wallet.save().unwrap_or_else(|err| eprintln!("{}", err)); + namada_apps::wallet::save(&wallet) + .unwrap_or_else(|err| eprintln!("{}", err)); println!( "Successfully added a key and an address with alias: \"{}\"", args.alias.to_lowercase() diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index aa240b704b..5d91067509 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -612,7 +612,7 @@ pub mod cmds { /// Generate a payment address from a viewing key or payment address #[derive(Clone, Debug)] - pub struct MaspGenPayAddr(pub args::MaspPayAddrGen); + pub struct MaspGenPayAddr(pub args::MaspPayAddrGen); impl SubCmd for MaspGenPayAddr { const CMD: &'static str = "gen-addr"; @@ -628,7 +628,7 @@ pub mod cmds { .about( "Generates a payment address from the given spending key", ) - .add_args::() + .add_args::>() } } @@ -853,7 +853,7 @@ pub mod cmds { } #[derive(Clone, Debug)] - pub struct QueryResult(pub args::QueryResult); + pub struct QueryResult(pub args::QueryResult); impl SubCmd for QueryResult { const CMD: &'static str = "tx-result"; @@ -867,12 +867,12 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about("Query the result of a transaction.") - .add_args::() + .add_args::>() } } #[derive(Clone, Debug)] - pub struct QueryProposal(pub args::QueryProposal); + pub struct QueryProposal(pub args::QueryProposal); impl SubCmd for QueryProposal { const CMD: &'static str = "query-proposal"; @@ -889,12 +889,14 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about("Query proposals.") - .add_args::() + .add_args::>() } } #[derive(Clone, Debug)] - pub struct QueryProposalResult(pub args::QueryProposalResult); + pub struct QueryProposalResult( + pub args::QueryProposalResult, + ); impl SubCmd for QueryProposalResult { const CMD: &'static str = "query-proposal-result"; @@ -911,12 +913,14 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about("Query proposals result.") - .add_args::() + .add_args::>() } } #[derive(Clone, Debug)] - pub struct QueryProtocolParameters(pub args::QueryProtocolParameters); + pub struct QueryProtocolParameters( + pub args::QueryProtocolParameters, + ); impl SubCmd for QueryProtocolParameters { const CMD: &'static str = "query-protocol-parameters"; @@ -935,12 +939,12 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about("Query protocol parameters.") - .add_args::() + .add_args::>() } } #[derive(Clone, Debug)] - pub struct TxCustom(pub args::TxCustom); + pub struct TxCustom(pub args::TxCustom); impl SubCmd for TxCustom { const CMD: &'static str = "tx"; @@ -954,12 +958,12 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about("Send a transaction with custom WASM code.") - .add_args::() + .add_args::>() } } #[derive(Clone, Debug)] - pub struct TxTransfer(pub args::TxTransfer); + pub struct TxTransfer(pub args::TxTransfer); impl SubCmd for TxTransfer { const CMD: &'static str = "transfer"; @@ -973,12 +977,12 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about("Send a signed transfer transaction.") - .add_args::() + .add_args::>() } } #[derive(Clone, Debug)] - pub struct TxIbcTransfer(pub args::TxIbcTransfer); + pub struct TxIbcTransfer(pub args::TxIbcTransfer); impl SubCmd for TxIbcTransfer { const CMD: &'static str = "ibc-transfer"; @@ -992,12 +996,12 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about("Send a signed IBC transfer transaction.") - .add_args::() + .add_args::>() } } #[derive(Clone, Debug)] - pub struct TxUpdateVp(pub args::TxUpdateVp); + pub struct TxUpdateVp(pub args::TxUpdateVp); impl SubCmd for TxUpdateVp { const CMD: &'static str = "update"; @@ -1014,12 +1018,12 @@ pub mod cmds { "Send a signed transaction to update account's validity \ predicate.", ) - .add_args::() + .add_args::>() } } #[derive(Clone, Debug)] - pub struct TxInitAccount(pub args::TxInitAccount); + pub struct TxInitAccount(pub args::TxInitAccount); impl SubCmd for TxInitAccount { const CMD: &'static str = "init-account"; @@ -1036,12 +1040,12 @@ pub mod cmds { "Send a signed transaction to create a new established \ account.", ) - .add_args::() + .add_args::>() } } #[derive(Clone, Debug)] - pub struct TxInitValidator(pub args::TxInitValidator); + pub struct TxInitValidator(pub args::TxInitValidator); impl SubCmd for TxInitValidator { const CMD: &'static str = "init-validator"; @@ -1058,12 +1062,12 @@ pub mod cmds { "Send a signed transaction to create a new validator \ account.", ) - .add_args::() + .add_args::>() } } #[derive(Clone, Debug)] - pub struct Bond(pub args::Bond); + pub struct Bond(pub args::Bond); impl SubCmd for Bond { const CMD: &'static str = "bond"; @@ -1077,12 +1081,12 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about("Bond tokens in PoS system.") - .add_args::() + .add_args::>() } } #[derive(Clone, Debug)] - pub struct Unbond(pub args::Unbond); + pub struct Unbond(pub args::Unbond); impl SubCmd for Unbond { const CMD: &'static str = "unbond"; @@ -1096,12 +1100,12 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about("Unbond tokens from a PoS bond.") - .add_args::() + .add_args::>() } } #[derive(Clone, Debug)] - pub struct Withdraw(pub args::Withdraw); + pub struct Withdraw(pub args::Withdraw); impl SubCmd for Withdraw { const CMD: &'static str = "withdraw"; @@ -1115,12 +1119,12 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about("Withdraw tokens from previously unbonded PoS bond.") - .add_args::() + .add_args::>() } } #[derive(Clone, Debug)] - pub struct QueryEpoch(pub args::Query); + pub struct QueryEpoch(pub args::Query); impl SubCmd for QueryEpoch { const CMD: &'static str = "epoch"; @@ -1134,12 +1138,12 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about("Query the epoch of the last committed block.") - .add_args::() + .add_args::>() } } #[derive(Clone, Debug)] - pub struct QueryConversions(pub args::QueryConversions); + pub struct QueryConversions(pub args::QueryConversions); impl SubCmd for QueryConversions { const CMD: &'static str = "conversions"; @@ -1153,12 +1157,12 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about("Query currently applicable conversions.") - .add_args::() + .add_args::>() } } #[derive(Clone, Debug)] - pub struct QueryBlock(pub args::Query); + pub struct QueryBlock(pub args::Query); impl SubCmd for QueryBlock { const CMD: &'static str = "block"; @@ -1172,12 +1176,12 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about("Query the last committed block.") - .add_args::() + .add_args::>() } } #[derive(Clone, Debug)] - pub struct QueryBalance(pub args::QueryBalance); + pub struct QueryBalance(pub args::QueryBalance); impl SubCmd for QueryBalance { const CMD: &'static str = "balance"; @@ -1191,12 +1195,12 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about("Query balance(s) of tokens.") - .add_args::() + .add_args::>() } } #[derive(Clone, Debug)] - pub struct QueryBonds(pub args::QueryBonds); + pub struct QueryBonds(pub args::QueryBonds); impl SubCmd for QueryBonds { const CMD: &'static str = "bonds"; @@ -1210,12 +1214,12 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about("Query PoS bond(s).") - .add_args::() + .add_args::>() } } #[derive(Clone, Debug)] - pub struct QueryBondedStake(pub args::QueryBondedStake); + pub struct QueryBondedStake(pub args::QueryBondedStake); impl SubCmd for QueryBondedStake { const CMD: &'static str = "bonded-stake"; @@ -1229,12 +1233,12 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about("Query PoS bonded stake.") - .add_args::() + .add_args::>() } } #[derive(Clone, Debug)] - pub struct QueryTransfers(pub args::QueryTransfers); + pub struct QueryTransfers(pub args::QueryTransfers); impl SubCmd for QueryTransfers { const CMD: &'static str = "show-transfers"; @@ -1248,12 +1252,14 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about("Query the accepted transfers to date.") - .add_args::() + .add_args::>() } } #[derive(Clone, Debug)] - pub struct QueryCommissionRate(pub args::QueryCommissionRate); + pub struct QueryCommissionRate( + pub args::QueryCommissionRate, + ); impl SubCmd for QueryCommissionRate { const CMD: &'static str = "commission-rate"; @@ -1267,12 +1273,12 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about("Query commission rate.") - .add_args::() + .add_args::>() } } #[derive(Clone, Debug)] - pub struct QuerySlashes(pub args::QuerySlashes); + pub struct QuerySlashes(pub args::QuerySlashes); impl SubCmd for QuerySlashes { const CMD: &'static str = "slashes"; @@ -1289,12 +1295,12 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about("Query PoS applied slashes.") - .add_args::() + .add_args::>() } } #[derive(Clone, Debug)] - pub struct QueryRawBytes(pub args::QueryRawBytes); + pub struct QueryRawBytes(pub args::QueryRawBytes); impl SubCmd for QueryRawBytes { const CMD: &'static str = "query-bytes"; @@ -1308,12 +1314,12 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about("Query the raw bytes of a given storage key") - .add_args::() + .add_args::>() } } #[derive(Clone, Debug)] - pub struct TxInitProposal(pub args::InitProposal); + pub struct TxInitProposal(pub args::InitProposal); impl SubCmd for TxInitProposal { const CMD: &'static str = "init-proposal"; @@ -1330,12 +1336,12 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about("Create a new proposal.") - .add_args::() + .add_args::>() } } #[derive(Clone, Debug)] - pub struct TxVoteProposal(pub args::VoteProposal); + pub struct TxVoteProposal(pub args::VoteProposal); impl SubCmd for TxVoteProposal { const CMD: &'static str = "vote-proposal"; @@ -1352,12 +1358,12 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about("Vote a proposal.") - .add_args::() + .add_args::>() } } #[derive(Clone, Debug)] - pub struct TxRevealPk(pub args::RevealPk); + pub struct TxRevealPk(pub args::RevealPk); impl SubCmd for TxRevealPk { const CMD: &'static str = "reveal-pk"; @@ -1382,7 +1388,7 @@ pub mod cmds { signature verification on transactions authorized by \ this account.", ) - .add_args::() + .add_args::>() } } @@ -1513,6 +1519,7 @@ pub mod args { use std::str::FromStr; use namada::ibc::core::ics24_host::identifier::{ChannelId, PortId}; + pub use namada::ledger::args::*; use namada::types::address::Address; use namada::types::chain::{ChainId, ChainIdPrefix}; use namada::types::governance::ProposalVote; @@ -1520,18 +1527,31 @@ pub mod args { use namada::types::masp::MaspValue; use namada::types::storage::{self, Epoch}; use namada::types::token; - use namada::types::transaction::GasLimit; use rust_decimal::Decimal; use super::context::*; use super::utils::*; use super::{ArgGroup, ArgMatches}; - use crate::client::types::{ParsedTxArgs, ParsedTxTransferArgs}; use crate::config; use crate::config::TendermintMode; use crate::facade::tendermint::Timeout; use crate::facade::tendermint_config::net::Address as TendermintAddress; + const TX_INIT_ACCOUNT_WASM: &str = "tx_init_account.wasm"; + const TX_INIT_VALIDATOR_WASM: &str = "tx_init_validator.wasm"; + const TX_INIT_PROPOSAL: &str = "tx_init_proposal.wasm"; + const TX_VOTE_PROPOSAL: &str = "tx_vote_proposal.wasm"; + const TX_REVEAL_PK: &str = "tx_reveal_pk.wasm"; + const TX_UPDATE_VP_WASM: &str = "tx_update_vp.wasm"; + const TX_TRANSFER_WASM: &str = "tx_transfer.wasm"; + const TX_IBC_WASM: &str = "tx_ibc.wasm"; + const VP_USER_WASM: &str = "vp_user.wasm"; + const TX_BOND_WASM: &str = "tx_bond.wasm"; + const TX_UNBOND_WASM: &str = "tx_unbond.wasm"; + const TX_WITHDRAW_WASM: &str = "tx_withdraw.wasm"; + const TX_CHANGE_COMMISSION_WASM: &str = + "tx_change_validator_commission.wasm"; + const ADDRESS: Arg = arg("address"); const ALIAS_OPT: ArgOpt = ALIAS.opt(); const ALIAS: Arg = arg("alias"); @@ -1686,16 +1706,20 @@ pub mod args { } } - /// Transaction associated results arguments - #[derive(Clone, Debug)] - pub struct QueryResult { - /// Common query args - pub query: Query, - /// Hash of transaction to lookup - pub tx_hash: String, + pub trait CliToSdk: Args { + fn to_sdk(self, ctx: &mut Context) -> X; } - impl Args for QueryResult { + impl CliToSdk> for QueryResult { + fn to_sdk(self, ctx: &mut Context) -> QueryResult { + QueryResult:: { + query: self.query.to_sdk(ctx), + tx_hash: self.tx_hash, + } + } + } + + impl Args for QueryResult { fn parse(matches: &ArgMatches) -> Self { let query = Query::parse(matches); let tx_hash = TX_HASH.parse(matches); @@ -1703,7 +1727,7 @@ pub mod args { } fn def(app: App) -> App { - app.add_args::().arg( + app.add_args::>().arg( TX_HASH .def() .about("The hash of the transaction being looked up."), @@ -1711,18 +1735,20 @@ pub mod args { } } - /// Custom transaction arguments - #[derive(Clone, Debug)] - pub struct TxCustom { - /// Common tx arguments - pub tx: Tx, - /// Path to the tx WASM code file - pub code_path: PathBuf, - /// Path to the data file - pub data_path: Option, + impl CliToSdk> for TxCustom { + fn to_sdk(self, ctx: &mut Context) -> TxCustom { + TxCustom:: { + tx: self.tx.to_sdk(ctx), + code_path: ctx.read_wasm(self.code_path), + data_path: self.data_path.map(|data_path| { + std::fs::read(data_path) + .expect("Expected a file at given data path") + }), + } + } } - impl Args for TxCustom { + impl Args for TxCustom { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); let code_path = CODE_PATH.parse(matches); @@ -1735,7 +1761,7 @@ pub mod args { } fn def(app: App) -> App { - app.add_args::() + app.add_args::>() .arg( CODE_PATH .def() @@ -1749,39 +1775,22 @@ pub mod args { } } - /// Transfer transaction arguments - #[derive(Clone, Debug)] - pub struct TxTransfer { - /// Common tx arguments - pub tx: Tx, - /// Transfer source address - pub source: WalletTransferSource, - /// Transfer target address - pub target: WalletTransferTarget, - /// Transferred token address - pub token: WalletAddress, - /// Transferred token address - pub sub_prefix: Option, - /// Transferred token amount - pub amount: token::Amount, - } - - impl TxTransfer { - pub fn parse_from_context( - &self, - ctx: &mut Context, - ) -> ParsedTxTransferArgs { - ParsedTxTransferArgs { - tx: self.tx.parse_from_context(ctx), + impl CliToSdk> for TxTransfer { + fn to_sdk(self, ctx: &mut Context) -> TxTransfer { + TxTransfer:: { + tx: self.tx.to_sdk(ctx), source: ctx.get_cached(&self.source), target: ctx.get(&self.target), token: ctx.get(&self.token), + sub_prefix: self.sub_prefix, amount: self.amount, + native_token: ctx.native_token.clone(), + tx_code_path: ctx.read_wasm(self.tx_code_path), } } } - impl Args for TxTransfer { + impl Args for TxTransfer { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); let source = TRANSFER_SOURCE.parse(matches); @@ -1789,6 +1798,7 @@ pub mod args { let token = TOKEN.parse(matches); let sub_prefix = SUB_PREFIX.parse(matches); let amount = AMOUNT.parse(matches); + let tx_code_path = PathBuf::from(TX_TRANSFER_WASM); Self { tx, source, @@ -1796,11 +1806,13 @@ pub mod args { token, sub_prefix, amount, + native_token: (), + tx_code_path, } } fn def(app: App) -> App { - app.add_args::() + app.add_args::>() .arg(TRANSFER_SOURCE.def().about( "The source account address. The source's key may be used \ to produce the signature.", @@ -1815,32 +1827,25 @@ pub mod args { } } - /// IBC transfer transaction arguments - #[derive(Clone, Debug)] - pub struct TxIbcTransfer { - /// Common tx arguments - pub tx: Tx, - /// Transfer source address - pub source: WalletAddress, - /// Transfer target address - pub receiver: String, - /// Transferred token address - pub token: WalletAddress, - /// Transferred token address - pub sub_prefix: Option, - /// Transferred token amount - pub amount: token::Amount, - /// Port ID - pub port_id: PortId, - /// Channel ID - pub channel_id: ChannelId, - /// Timeout height of the destination chain - pub timeout_height: Option, - /// Timeout timestamp offset - pub timeout_sec_offset: Option, - } - - impl Args for TxIbcTransfer { + impl CliToSdk> for TxIbcTransfer { + fn to_sdk(self, ctx: &mut Context) -> TxIbcTransfer { + TxIbcTransfer:: { + tx: self.tx.to_sdk(ctx), + source: ctx.get(&self.source), + receiver: self.receiver, + token: ctx.get(&self.token), + sub_prefix: self.sub_prefix, + amount: self.amount, + port_id: self.port_id, + channel_id: self.channel_id, + timeout_height: self.timeout_height, + timeout_sec_offset: self.timeout_sec_offset, + tx_code_path: ctx.read_wasm(self.tx_code_path), + } + } + } + + impl Args for TxIbcTransfer { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); let source = SOURCE.parse(matches); @@ -1852,6 +1857,7 @@ pub mod args { let channel_id = CHANNEL_ID.parse(matches); let timeout_height = TIMEOUT_HEIGHT.parse(matches); let timeout_sec_offset = TIMEOUT_SEC_OFFSET.parse(matches); + let tx_code_path = PathBuf::from(TX_IBC_WASM); Self { tx, source, @@ -1863,11 +1869,12 @@ pub mod args { channel_id, timeout_height, timeout_sec_offset, + tx_code_path, } } fn def(app: App) -> App { - app.add_args::() + app.add_args::>() .arg(SOURCE.def().about( "The source account address. The source's key is used to \ produce the signature.", @@ -1889,35 +1896,38 @@ pub mod args { } } - /// Transaction to initialize a new account - #[derive(Clone, Debug)] - pub struct TxInitAccount { - /// Common tx arguments - pub tx: Tx, - /// Address of the source account - pub source: WalletAddress, - /// Path to the VP WASM code file for the new account - pub vp_code_path: Option, - /// Public key for the new account - pub public_key: WalletPublicKey, + impl CliToSdk> for TxInitAccount { + fn to_sdk(self, ctx: &mut Context) -> TxInitAccount { + TxInitAccount:: { + tx: self.tx.to_sdk(ctx), + source: ctx.get(&self.source), + vp_code_path: ctx.read_wasm(self.vp_code_path), + tx_code_path: ctx.read_wasm(self.tx_code_path), + public_key: ctx.get_cached(&self.public_key), + } + } } - impl Args for TxInitAccount { + impl Args for TxInitAccount { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); let source = SOURCE.parse(matches); - let vp_code_path = CODE_PATH_OPT.parse(matches); + let vp_code_path = CODE_PATH_OPT + .parse(matches) + .unwrap_or_else(|| PathBuf::from(VP_USER_WASM)); + let tx_code_path = PathBuf::from(TX_INIT_ACCOUNT_WASM); let public_key = PUBLIC_KEY.parse(matches); Self { tx, source, vp_code_path, public_key, + tx_code_path, } } fn def(app: App) -> App { - app.add_args::() + app.add_args::>() .arg(SOURCE.def().about( "The source account's address that signs the transaction.", )) @@ -1933,22 +1943,26 @@ pub mod args { } } - /// Transaction to initialize a new account - #[derive(Clone, Debug)] - pub struct TxInitValidator { - pub tx: Tx, - pub source: WalletAddress, - pub scheme: SchemeType, - pub account_key: Option, - pub consensus_key: Option, - pub protocol_key: Option, - pub commission_rate: Decimal, - pub max_commission_rate_change: Decimal, - pub validator_vp_code_path: Option, - pub unsafe_dont_encrypt: bool, + impl CliToSdk> for TxInitValidator { + fn to_sdk(self, ctx: &mut Context) -> TxInitValidator { + TxInitValidator:: { + tx: self.tx.to_sdk(ctx), + source: ctx.get(&self.source), + scheme: self.scheme, + account_key: self.account_key.map(|x| ctx.get_cached(&x)), + consensus_key: self.consensus_key.map(|x| ctx.get_cached(&x)), + protocol_key: self.protocol_key.map(|x| ctx.get_cached(&x)), + commission_rate: self.commission_rate, + max_commission_rate_change: self.max_commission_rate_change, + validator_vp_code_path: ctx + .read_wasm(self.validator_vp_code_path), + unsafe_dont_encrypt: self.unsafe_dont_encrypt, + tx_code_path: ctx.read_wasm(self.tx_code_path), + } + } } - impl Args for TxInitValidator { + impl Args for TxInitValidator { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); let source = SOURCE.parse(matches); @@ -1959,8 +1973,11 @@ pub mod args { let commission_rate = COMMISSION_RATE.parse(matches); let max_commission_rate_change = MAX_COMMISSION_RATE_CHANGE.parse(matches); - let validator_vp_code_path = VALIDATOR_CODE_PATH.parse(matches); + let validator_vp_code_path = VALIDATOR_CODE_PATH + .parse(matches) + .unwrap_or_else(|| PathBuf::from(VP_USER_WASM)); let unsafe_dont_encrypt = UNSAFE_DONT_ENCRYPT.parse(matches); + let tx_code_path = PathBuf::from(TX_INIT_VALIDATOR_WASM); Self { tx, source, @@ -1972,11 +1989,12 @@ pub mod args { max_commission_rate_change, validator_vp_code_path, unsafe_dont_encrypt, + tx_code_path, } } fn def(app: App) -> App { - app.add_args::() + app.add_args::>() .arg(SOURCE.def().about( "The source account's address that signs the transaction.", )) @@ -2019,31 +2037,33 @@ pub mod args { } } - /// Transaction to update a VP arguments - #[derive(Clone, Debug)] - pub struct TxUpdateVp { - /// Common tx arguments - pub tx: Tx, - /// Path to the VP WASM code file - pub vp_code_path: PathBuf, - /// Address of the account whose VP is to be updated - pub addr: WalletAddress, + impl CliToSdk> for TxUpdateVp { + fn to_sdk(self, ctx: &mut Context) -> TxUpdateVp { + TxUpdateVp:: { + tx: self.tx.to_sdk(ctx), + vp_code_path: ctx.read_wasm(self.vp_code_path), + tx_code_path: ctx.read_wasm(self.tx_code_path), + addr: ctx.get(&self.addr), + } + } } - impl Args for TxUpdateVp { + impl Args for TxUpdateVp { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); let vp_code_path = CODE_PATH.parse(matches); let addr = ADDRESS.parse(matches); + let tx_code_path = PathBuf::from(TX_UPDATE_VP_WASM); Self { tx, vp_code_path, addr, + tx_code_path, } } fn def(app: App) -> App { - app.add_args::() + app.add_args::>() .arg( CODE_PATH.def().about( "The path to the new validity predicate WASM code.", @@ -2056,36 +2076,38 @@ pub mod args { } } - /// Bond arguments - #[derive(Clone, Debug)] - pub struct Bond { - /// Common tx arguments - pub tx: Tx, - /// Validator address - pub validator: WalletAddress, - /// Amount of tokens to stake in a bond - pub amount: token::Amount, - /// Source address for delegations. For self-bonds, the validator is - /// also the source. - pub source: Option, + impl CliToSdk> for Bond { + fn to_sdk(self, ctx: &mut Context) -> Bond { + Bond:: { + tx: self.tx.to_sdk(ctx), + validator: ctx.get(&self.validator), + amount: self.amount, + source: self.source.map(|x| ctx.get(&x)), + native_token: ctx.native_token.clone(), + tx_code_path: ctx.read_wasm(self.tx_code_path), + } + } } - impl Args for Bond { + impl Args for Bond { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); let validator = VALIDATOR.parse(matches); let amount = AMOUNT.parse(matches); let source = SOURCE_OPT.parse(matches); + let tx_code_path = PathBuf::from(TX_BOND_WASM); Self { tx, validator, amount, source, + native_token: (), + tx_code_path, } } fn def(app: App) -> App { - app.add_args::() + app.add_args::>() .arg(VALIDATOR.def().about("Validator address.")) .arg(AMOUNT.def().about("Amount of tokens to stake in a bond.")) .arg(SOURCE_OPT.def().about( @@ -2095,36 +2117,36 @@ pub mod args { } } - /// Unbond arguments - #[derive(Clone, Debug)] - pub struct Unbond { - /// Common tx arguments - pub tx: Tx, - /// Validator address - pub validator: WalletAddress, - /// Amount of tokens to unbond from a bond - pub amount: token::Amount, - /// Source address for unbonding from delegations. For unbonding from - /// self-bonds, the validator is also the source - pub source: Option, + impl CliToSdk> for Unbond { + fn to_sdk(self, ctx: &mut Context) -> Unbond { + Unbond:: { + tx: self.tx.to_sdk(ctx), + validator: ctx.get(&self.validator), + amount: self.amount, + source: self.source.map(|x| ctx.get(&x)), + tx_code_path: ctx.read_wasm(self.tx_code_path), + } + } } - impl Args for Unbond { + impl Args for Unbond { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); let validator = VALIDATOR.parse(matches); let amount = AMOUNT.parse(matches); let source = SOURCE_OPT.parse(matches); + let tx_code_path = PathBuf::from(TX_UNBOND_WASM); Self { tx, validator, amount, source, + tx_code_path, } } fn def(app: App) -> App { - app.add_args::() + app.add_args::>() .arg(VALIDATOR.def().about("Validator address.")) .arg( AMOUNT @@ -2140,30 +2162,49 @@ pub mod args { } #[derive(Clone, Debug)] - pub struct InitProposal { + pub struct InitProposal { /// Common tx arguments - pub tx: Tx, + pub tx: Tx, /// The proposal file path pub proposal_data: PathBuf, /// Flag if proposal should be run offline pub offline: bool, + /// Native token address + pub native_token: C::NativeAddress, + /// Path to the TX WASM code file + pub tx_code_path: C::Data, + } + + impl CliToSdk> for InitProposal { + fn to_sdk(self, ctx: &mut Context) -> InitProposal { + InitProposal:: { + tx: self.tx.to_sdk(ctx), + proposal_data: self.proposal_data, + offline: self.offline, + native_token: ctx.native_token.clone(), + tx_code_path: ctx.read_wasm(self.tx_code_path), + } + } } - impl Args for InitProposal { + impl Args for InitProposal { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); let proposal_data = DATA_PATH.parse(matches); let offline = PROPOSAL_OFFLINE.parse(matches); + let tx_code_path = PathBuf::from(TX_INIT_PROPOSAL); Self { tx, proposal_data, offline, + native_token: (), + tx_code_path, } } fn def(app: App) -> App { - app.add_args::() + app.add_args::>() .arg(DATA_PATH.def().about( "The data path file (json) that describes the proposal.", )) @@ -2176,9 +2217,9 @@ pub mod args { } #[derive(Clone, Debug)] - pub struct VoteProposal { + pub struct VoteProposal { /// Common tx arguments - pub tx: Tx, + pub tx: Tx, /// Proposal id pub proposal_id: Option, /// The vote @@ -2187,15 +2228,31 @@ pub mod args { pub offline: bool, /// The proposal file path pub proposal_data: Option, + /// Path to the TX WASM code file + pub tx_code_path: C::Data, + } + + impl CliToSdk> for VoteProposal { + fn to_sdk(self, ctx: &mut Context) -> VoteProposal { + VoteProposal:: { + tx: self.tx.to_sdk(ctx), + proposal_id: self.proposal_id, + vote: self.vote, + offline: self.offline, + proposal_data: self.proposal_data, + tx_code_path: ctx.read_wasm(self.tx_code_path), + } + } } - impl Args for VoteProposal { + impl Args for VoteProposal { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); let proposal_id = PROPOSAL_ID_OPT.parse(matches); let vote = PROPOSAL_VOTE.parse(matches); let offline = PROPOSAL_OFFLINE.parse(matches); let proposal_data = DATA_PATH_OPT.parse(matches); + let tx_code_path = PathBuf::from(TX_VOTE_PROPOSAL); Self { tx, @@ -2203,11 +2260,12 @@ pub mod args { vote, offline, proposal_data, + tx_code_path, } } fn def(app: App) -> App { - app.add_args::() + app.add_args::>() .arg( PROPOSAL_ID_OPT .def() @@ -2240,15 +2298,16 @@ pub mod args { } } - #[derive(Clone, Debug)] - pub struct RevealPk { - /// Common tx arguments - pub tx: Tx, - /// A public key to be revealed on-chain - pub public_key: WalletPublicKey, + impl CliToSdk> for RevealPk { + fn to_sdk(self, ctx: &mut Context) -> RevealPk { + RevealPk:: { + tx: self.tx.to_sdk(ctx), + public_key: ctx.get_cached(&self.public_key), + } + } } - impl Args for RevealPk { + impl Args for RevealPk { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); let public_key = PUBLIC_KEY.parse(matches); @@ -2257,20 +2316,21 @@ pub mod args { } fn def(app: App) -> App { - app.add_args::() + app.add_args::>() .arg(PUBLIC_KEY.def().about("A public key to reveal.")) } } - #[derive(Clone, Debug)] - pub struct QueryProposal { - /// Common query args - pub query: Query, - /// Proposal id - pub proposal_id: Option, + impl CliToSdk> for QueryProposal { + fn to_sdk(self, ctx: &mut Context) -> QueryProposal { + QueryProposal:: { + query: self.query.to_sdk(ctx), + proposal_id: self.proposal_id, + } + } } - impl Args for QueryProposal { + impl Args for QueryProposal { fn parse(matches: &ArgMatches) -> Self { let query = Query::parse(matches); let proposal_id = PROPOSAL_ID_OPT.parse(matches); @@ -2279,15 +2339,15 @@ pub mod args { } fn def(app: App) -> App { - app.add_args::() + app.add_args::>() .arg(PROPOSAL_ID_OPT.def().about("The proposal identifier.")) } } #[derive(Clone, Debug)] - pub struct QueryProposalResult { + pub struct QueryProposalResult { /// Common query args - pub query: Query, + pub query: Query, /// Proposal id pub proposal_id: Option, /// Flag if proposal result should be run on offline data @@ -2296,7 +2356,18 @@ pub mod args { pub proposal_folder: Option, } - impl Args for QueryProposalResult { + impl CliToSdk> for QueryProposalResult { + fn to_sdk(self, ctx: &mut Context) -> QueryProposalResult { + QueryProposalResult:: { + query: self.query.to_sdk(ctx), + proposal_id: self.proposal_id, + offline: self.offline, + proposal_folder: self.proposal_folder, + } + } + } + + impl Args for QueryProposalResult { fn parse(matches: &ArgMatches) -> Self { let query = Query::parse(matches); let proposal_id = PROPOSAL_ID_OPT.parse(matches); @@ -2312,7 +2383,7 @@ pub mod args { } fn def(app: App) -> App { - app.add_args::() + app.add_args::>() .arg(PROPOSAL_ID_OPT.def().about("The proposal identifier.")) .arg( PROPOSAL_OFFLINE @@ -2335,13 +2406,20 @@ pub mod args { } } - #[derive(Clone, Debug)] - pub struct QueryProtocolParameters { - /// Common query args - pub query: Query, + impl CliToSdk> + for QueryProtocolParameters + { + fn to_sdk( + self, + ctx: &mut Context, + ) -> QueryProtocolParameters { + QueryProtocolParameters:: { + query: self.query.to_sdk(ctx), + } + } } - impl Args for QueryProtocolParameters { + impl Args for QueryProtocolParameters { fn parse(matches: &ArgMatches) -> Self { let query = Query::parse(matches); @@ -2349,36 +2427,37 @@ pub mod args { } fn def(app: App) -> App { - app.add_args::() + app.add_args::>() } } - /// Withdraw arguments - #[derive(Clone, Debug)] - pub struct Withdraw { - /// Common tx arguments - pub tx: Tx, - /// Validator address - pub validator: WalletAddress, - /// Source address for withdrawing from delegations. For withdrawing - /// from self-bonds, the validator is also the source - pub source: Option, + impl CliToSdk> for Withdraw { + fn to_sdk(self, ctx: &mut Context) -> Withdraw { + Withdraw:: { + tx: self.tx.to_sdk(ctx), + validator: ctx.get(&self.validator), + source: self.source.map(|x| ctx.get(&x)), + tx_code_path: ctx.read_wasm(self.tx_code_path), + } + } } - impl Args for Withdraw { + impl Args for Withdraw { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); let validator = VALIDATOR.parse(matches); let source = SOURCE_OPT.parse(matches); + let tx_code_path = PathBuf::from(TX_WITHDRAW_WASM); Self { tx, validator, source, + tx_code_path, } } fn def(app: App) -> App { - app.add_args::() + app.add_args::>() .arg(VALIDATOR.def().about("Validator address.")) .arg(SOURCE_OPT.def().about( "Source address for withdrawing from delegations. For \ @@ -2388,18 +2467,17 @@ pub mod args { } } - /// Query asset conversions - #[derive(Clone, Debug)] - pub struct QueryConversions { - /// Common query args - pub query: Query, - /// Address of a token - pub token: Option, - /// Epoch of the asset - pub epoch: Option, + impl CliToSdk> for QueryConversions { + fn to_sdk(self, ctx: &mut Context) -> QueryConversions { + QueryConversions:: { + query: self.query.to_sdk(ctx), + token: self.token.map(|x| ctx.get(&x)), + epoch: self.epoch, + } + } } - impl Args for QueryConversions { + impl Args for QueryConversions { fn parse(matches: &ArgMatches) -> Self { let query = Query::parse(matches); let token = TOKEN_OPT.parse(matches); @@ -2412,7 +2490,7 @@ pub mod args { } fn def(app: App) -> App { - app.add_args::() + app.add_args::>() .arg( EPOCH .def() @@ -2426,22 +2504,19 @@ pub mod args { } } - /// Query token balance(s) - #[derive(Clone, Debug)] - pub struct QueryBalance { - /// Common query args - pub query: Query, - /// Address of an owner - pub owner: Option, - /// Address of a token - pub token: Option, - /// Whether not to convert balances - pub no_conversions: bool, - /// Sub prefix of an account - pub sub_prefix: Option, - } - - impl Args for QueryBalance { + impl CliToSdk> for QueryBalance { + fn to_sdk(self, ctx: &mut Context) -> QueryBalance { + QueryBalance:: { + query: self.query.to_sdk(ctx), + owner: self.owner.map(|x| ctx.get_cached(&x)), + token: self.token.map(|x| ctx.get(&x)), + no_conversions: self.no_conversions, + sub_prefix: self.sub_prefix, + } + } + } + + impl Args for QueryBalance { fn parse(matches: &ArgMatches) -> Self { let query = Query::parse(matches); let owner = BALANCE_OWNER.parse(matches); @@ -2458,7 +2533,7 @@ pub mod args { } fn def(app: App) -> App { - app.add_args::() + app.add_args::>() .arg( BALANCE_OWNER .def() @@ -2482,18 +2557,17 @@ pub mod args { } } - /// Query historical transfer(s) - #[derive(Clone, Debug)] - pub struct QueryTransfers { - /// Common query args - pub query: Query, - /// Address of an owner - pub owner: Option, - /// Address of a token - pub token: Option, + impl CliToSdk> for QueryTransfers { + fn to_sdk(self, ctx: &mut Context) -> QueryTransfers { + QueryTransfers:: { + query: self.query.to_sdk(ctx), + owner: self.owner.map(|x| ctx.get_cached(&x)), + token: self.token.map(|x| ctx.get(&x)), + } + } } - impl Args for QueryTransfers { + impl Args for QueryTransfers { fn parse(matches: &ArgMatches) -> Self { let query = Query::parse(matches); let owner = BALANCE_OWNER.parse(matches); @@ -2506,7 +2580,7 @@ pub mod args { } fn def(app: App) -> App { - app.add_args::() + app.add_args::>() .arg(BALANCE_OWNER.def().about( "The account address that queried transfers must involve.", )) @@ -2516,18 +2590,17 @@ pub mod args { } } - /// Query PoS bond(s) - #[derive(Clone, Debug)] - pub struct QueryBonds { - /// Common query args - pub query: Query, - /// Address of an owner - pub owner: Option, - /// Address of a validator - pub validator: Option, + impl CliToSdk> for QueryBonds { + fn to_sdk(self, ctx: &mut Context) -> QueryBonds { + QueryBonds:: { + query: self.query.to_sdk(ctx), + owner: self.owner.map(|x| ctx.get(&x)), + validator: self.validator.map(|x| ctx.get(&x)), + } + } } - impl Args for QueryBonds { + impl Args for QueryBonds { fn parse(matches: &ArgMatches) -> Self { let query = Query::parse(matches); let owner = OWNER.parse(matches); @@ -2540,7 +2613,7 @@ pub mod args { } fn def(app: App) -> App { - app.add_args::() + app.add_args::>() .arg( OWNER.def().about( "The owner account address whose bonds to query.", @@ -2554,18 +2627,17 @@ pub mod args { } } - /// Query PoS bonded stake - #[derive(Clone, Debug)] - pub struct QueryBondedStake { - /// Common query args - pub query: Query, - /// Address of a validator - pub validator: Option, - /// Epoch in which to find bonded stake - pub epoch: Option, + impl CliToSdk> for QueryBondedStake { + fn to_sdk(self, ctx: &mut Context) -> QueryBondedStake { + QueryBondedStake:: { + query: self.query.to_sdk(ctx), + validator: self.validator.map(|x| ctx.get(&x)), + epoch: self.epoch, + } + } } - impl Args for QueryBondedStake { + impl Args for QueryBondedStake { fn parse(matches: &ArgMatches) -> Self { let query = Query::parse(matches); let validator = VALIDATOR_OPT.parse(matches); @@ -2578,7 +2650,7 @@ pub mod args { } fn def(app: App) -> App { - app.add_args::() + app.add_args::>() .arg(VALIDATOR_OPT.def().about( "The validator's address whose bonded stake to query.", )) @@ -2589,31 +2661,35 @@ pub mod args { } } - #[derive(Clone, Debug)] - /// Commission rate change args - pub struct TxCommissionRateChange { - /// Common tx arguments - pub tx: Tx, - /// Validator address (should be self) - pub validator: WalletAddress, - /// Value to which the tx changes the commission rate - pub rate: Decimal, + impl CliToSdk> + for TxCommissionRateChange + { + fn to_sdk(self, ctx: &mut Context) -> TxCommissionRateChange { + TxCommissionRateChange:: { + tx: self.tx.to_sdk(ctx), + validator: ctx.get(&self.validator), + rate: self.rate, + tx_code_path: ctx.read_wasm(self.tx_code_path), + } + } } - impl Args for TxCommissionRateChange { + impl Args for TxCommissionRateChange { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); let validator = VALIDATOR.parse(matches); let rate = COMMISSION_RATE.parse(matches); + let tx_code_path = PathBuf::from(TX_CHANGE_COMMISSION_WASM); Self { tx, validator, rate, + tx_code_path, } } fn def(app: App) -> App { - app.add_args::() + app.add_args::>() .arg(VALIDATOR.def().about( "The validator's address whose commission rate to change.", )) @@ -2625,18 +2701,17 @@ pub mod args { } } - /// Query PoS commission rate - #[derive(Clone, Debug)] - pub struct QueryCommissionRate { - /// Common query args - pub query: Query, - /// Address of a validator - pub validator: WalletAddress, - /// Epoch in which to find commission rate - pub epoch: Option, + impl CliToSdk> for QueryCommissionRate { + fn to_sdk(self, ctx: &mut Context) -> QueryCommissionRate { + QueryCommissionRate:: { + query: self.query.to_sdk(ctx), + validator: ctx.get(&self.validator), + epoch: self.epoch, + } + } } - impl Args for QueryCommissionRate { + impl Args for QueryCommissionRate { fn parse(matches: &ArgMatches) -> Self { let query = Query::parse(matches); let validator = VALIDATOR.parse(matches); @@ -2649,7 +2724,7 @@ pub mod args { } fn def(app: App) -> App { - app.add_args::() + app.add_args::>() .arg(VALIDATOR.def().about( "The validator's address whose commission rate to query.", )) @@ -2660,16 +2735,16 @@ pub mod args { } } - /// Query PoS slashes - #[derive(Clone, Debug)] - pub struct QuerySlashes { - /// Common query args - pub query: Query, - /// Address of a validator - pub validator: Option, + impl CliToSdk> for QuerySlashes { + fn to_sdk(self, ctx: &mut Context) -> QuerySlashes { + QuerySlashes:: { + query: self.query.to_sdk(ctx), + validator: self.validator.map(|x| ctx.get(&x)), + } + } } - impl Args for QuerySlashes { + impl Args for QuerySlashes { fn parse(matches: &ArgMatches) -> Self { let query = Query::parse(matches); let validator = VALIDATOR_OPT.parse(matches); @@ -2677,23 +2752,24 @@ pub mod args { } fn def(app: App) -> App { - app.add_args::().arg( + app.add_args::>().arg( VALIDATOR_OPT .def() .about("The validator's address whose slashes to query."), ) } } - /// Query the raw bytes of given storage key - #[derive(Clone, Debug)] - pub struct QueryRawBytes { - /// The storage key to query - pub storage_key: storage::Key, - /// Common query args - pub query: Query, + + impl CliToSdk> for QueryRawBytes { + fn to_sdk(self, ctx: &mut Context) -> QueryRawBytes { + QueryRawBytes:: { + query: self.query.to_sdk(ctx), + storage_key: self.storage_key, + } + } } - impl Args for QueryRawBytes { + impl Args for QueryRawBytes { fn parse(matches: &ArgMatches) -> Self { let storage_key = STORAGE_KEY.parse(matches); let query = Query::parse(matches); @@ -2701,59 +2777,48 @@ pub mod args { } fn def(app: App) -> App { - app.add_args::() + app.add_args::>() .arg(STORAGE_KEY.def().about("Storage key")) } } - /// Common transaction arguments - #[derive(Clone, Debug)] - pub struct Tx { - /// Simulate applying the transaction - pub dry_run: bool, - /// Submit the transaction even if it doesn't pass client checks - pub force: bool, - /// Do not wait for the transaction to be added to the blockchain - pub broadcast_only: bool, - /// The address of the ledger node as host:port - pub ledger_address: TendermintAddress, - /// If any new account is initialized by the tx, use the given alias to - /// save it in the wallet. - pub initialized_account_alias: Option, - /// The amount being payed to include the transaction - pub fee_amount: token::Amount, - /// The token in which the fee is being paid - pub fee_token: WalletAddress, - /// The max amount of gas used to process tx - pub gas_limit: GasLimit, - /// Sign the tx with the key for the given alias from your wallet - pub signing_key: Option, - /// Sign the tx with the keypair of the public key of the given address - pub signer: Option, - } - - impl Tx { - pub fn parse_from_context(&self, ctx: &mut Context) -> ParsedTxArgs { - ParsedTxArgs { + + /// The concrete types being used in the CLI + #[derive(Clone, Debug)] + pub struct CliTypes; + + impl NamadaTypes for CliTypes { + type Address = WalletAddress; + type BalanceOwner = WalletBalanceOwner; + type Data = PathBuf; + type Keypair = WalletKeypair; + type NativeAddress = (); + type PublicKey = WalletPublicKey; + type TendermintAddress = TendermintAddress; + type TransferSource = WalletTransferSource; + type TransferTarget = WalletTransferTarget; + type ViewingKey = WalletViewingKey; + } + + impl CliToSdk> for Tx { + fn to_sdk(self, ctx: &mut Context) -> Tx { + Tx:: { dry_run: self.dry_run, force: self.force, broadcast_only: self.broadcast_only, - ledger_address: self.ledger_address.clone(), - initialized_account_alias: self - .initialized_account_alias - .clone(), + ledger_address: (), + initialized_account_alias: self.initialized_account_alias, fee_amount: self.fee_amount, fee_token: ctx.get(&self.fee_token), - gas_limit: self.gas_limit.clone(), - signing_key: self - .signing_key - .as_ref() - .map(|sk| ctx.get_cached(sk)), - signer: self.signer.as_ref().map(|signer| ctx.get(signer)), + gas_limit: self.gas_limit, + signing_key: self.signing_key.map(|x| ctx.get_cached(&x)), + signer: self.signer.map(|x| ctx.get(&x)), + tx_code_path: ctx.read_wasm(self.tx_code_path), + password: self.password, } } } - impl Args for Tx { + impl Args for Tx { fn def(app: App) -> App { app.arg( DRY_RUN_TX @@ -2813,9 +2878,10 @@ pub mod args { let fee_amount = GAS_AMOUNT.parse(matches); let fee_token = GAS_TOKEN.parse(matches); let gas_limit = GAS_LIMIT.parse(matches).into(); - let signing_key = SIGNING_KEY_OPT.parse(matches); let signer = SIGNER.parse(matches); + let tx_code_path = PathBuf::from(TX_REVEAL_PK); + let password = None; Self { dry_run, force, @@ -2827,18 +2893,19 @@ pub mod args { gas_limit, signing_key, signer, + tx_code_path, + password, } } } - /// Common query arguments - #[derive(Clone, Debug)] - pub struct Query { - /// The address of the ledger node as host:port - pub ledger_address: TendermintAddress, + impl CliToSdk> for Query { + fn to_sdk(self, _ctx: &mut Context) -> Query { + Query:: { ledger_address: () } + } } - impl Args for Query { + impl Args for Query { fn def(app: App) -> App { app.arg(LEDGER_ADDRESS_DEFAULT.def().about(LEDGER_ADDRESS_ABOUT)) } @@ -2849,17 +2916,6 @@ pub mod args { } } - /// MASP add key or address arguments - #[derive(Clone, Debug)] - pub struct MaspAddrKeyAdd { - /// Key alias - pub alias: String, - /// Any MASP value - pub value: MaspValue, - /// Don't encrypt the keypair - pub unsafe_dont_encrypt: bool, - } - impl Args for MaspAddrKeyAdd { fn parse(matches: &ArgMatches) -> Self { let alias = ALIAS.parse(matches); @@ -2890,15 +2946,6 @@ pub mod args { } } - /// MASP generate spending key arguments - #[derive(Clone, Debug)] - pub struct MaspSpendKeyGen { - /// Key alias - pub alias: String, - /// Don't encrypt the keypair - pub unsafe_dont_encrypt: bool, - } - impl Args for MaspSpendKeyGen { fn parse(matches: &ArgMatches) -> Self { let alias = ALIAS.parse(matches); @@ -2922,18 +2969,17 @@ pub mod args { } } - /// MASP generate payment address arguments - #[derive(Clone, Debug)] - pub struct MaspPayAddrGen { - /// Key alias - pub alias: String, - /// Viewing key - pub viewing_key: WalletViewingKey, - /// Pin - pub pin: bool, + impl CliToSdk> for MaspPayAddrGen { + fn to_sdk(self, ctx: &mut Context) -> MaspPayAddrGen { + MaspPayAddrGen:: { + alias: self.alias, + viewing_key: ctx.get_cached(&self.viewing_key), + pin: self.pin, + } + } } - impl Args for MaspPayAddrGen { + impl Args for MaspPayAddrGen { fn parse(matches: &ArgMatches) -> Self { let alias = ALIAS.parse(matches); let viewing_key = VIEWING_KEY.parse(matches); @@ -2959,17 +3005,6 @@ pub mod args { } } - /// Wallet generate key and implicit address arguments - #[derive(Clone, Debug)] - pub struct KeyAndAddressGen { - /// Scheme type - pub scheme: SchemeType, - /// Key alias - pub alias: Option, - /// Don't encrypt the keypair - pub unsafe_dont_encrypt: bool, - } - impl Args for KeyAndAddressGen { fn parse(matches: &ArgMatches) -> Self { let scheme = SCHEME.parse(matches); @@ -2999,15 +3034,6 @@ pub mod args { } } - /// Wallet key lookup arguments - #[derive(Clone, Debug)] - pub struct KeyFind { - pub public_key: Option, - pub alias: Option, - pub value: Option, - pub unsafe_show_secret: bool, - } - impl Args for KeyFind { fn parse(matches: &ArgMatches) -> Self { let public_key = RAW_PUBLIC_KEY_OPT.parse(matches); @@ -3049,13 +3075,6 @@ pub mod args { } } - /// Wallet find shielded address or key arguments - #[derive(Clone, Debug)] - pub struct AddrKeyFind { - pub alias: String, - pub unsafe_show_secret: bool, - } - impl Args for AddrKeyFind { fn parse(matches: &ArgMatches) -> Self { let alias = ALIAS.parse(matches); @@ -3076,13 +3095,6 @@ pub mod args { } } - /// Wallet list shielded keys arguments - #[derive(Clone, Debug)] - pub struct MaspKeysList { - pub decrypt: bool, - pub unsafe_show_secret: bool, - } - impl Args for MaspKeysList { fn parse(matches: &ArgMatches) -> Self { let decrypt = DECRYPT.parse(matches); @@ -3103,13 +3115,6 @@ pub mod args { } } - /// Wallet list keys arguments - #[derive(Clone, Debug)] - pub struct KeyList { - pub decrypt: bool, - pub unsafe_show_secret: bool, - } - impl Args for KeyList { fn parse(matches: &ArgMatches) -> Self { let decrypt = DECRYPT.parse(matches); @@ -3130,12 +3135,6 @@ pub mod args { } } - /// Wallet key export arguments - #[derive(Clone, Debug)] - pub struct KeyExport { - pub alias: String, - } - impl Args for KeyExport { fn parse(matches: &ArgMatches) -> Self { let alias = ALIAS.parse(matches); @@ -3152,13 +3151,6 @@ pub mod args { } } - /// Wallet address lookup arguments - #[derive(Clone, Debug)] - pub struct AddressOrAliasFind { - pub alias: Option, - pub address: Option
, - } - impl Args for AddressOrAliasFind { fn parse(matches: &ArgMatches) -> Self { let alias = ALIAS_OPT.parse(matches); @@ -3185,13 +3177,6 @@ pub mod args { } } - /// Wallet address add arguments - #[derive(Clone, Debug)] - pub struct AddressAdd { - pub alias: String, - pub address: Address, - } - impl Args for AddressAdd { fn parse(matches: &ArgMatches) -> Self { let alias = ALIAS.parse(matches); diff --git a/apps/src/lib/cli/context.rs b/apps/src/lib/cli/context.rs index e61fda9dfc..f9d95bdb04 100644 --- a/apps/src/lib/cli/context.rs +++ b/apps/src/lib/cli/context.rs @@ -6,17 +6,19 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use color_eyre::eyre::Result; +use namada::ledger::masp::ShieldedContext; +use namada::ledger::wallet::Wallet; use namada::types::address::Address; use namada::types::chain::ChainId; use namada::types::key::*; use namada::types::masp::*; use super::args; -use crate::client::tx::ShieldedContext; +use crate::client::tx::CLIShieldedUtils; use crate::config::genesis::genesis_config; use crate::config::global::GlobalConfig; use crate::config::{self, Config}; -use crate::wallet::Wallet; +use crate::wallet::CliWalletUtils; use crate::wasm_loader; /// Env. var to set chain ID @@ -66,13 +68,13 @@ pub struct Context { /// Global arguments pub global_args: args::Global, /// The wallet - pub wallet: Wallet, + pub wallet: Wallet, /// The global configuration pub global_config: GlobalConfig, /// The ledger configuration for a specific chain ID pub config: Config, /// The context fr shielded operations - pub shielded: ShieldedContext, + pub shielded: ShieldedContext, /// Native token's address pub native_token: Address, } @@ -98,8 +100,10 @@ impl Context { 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); + let wallet = crate::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() { @@ -118,7 +122,7 @@ impl Context { wallet, global_config, config, - shielded: ShieldedContext::new(chain_dir), + shielded: CLIShieldedUtils::new(chain_dir), native_token, }) } @@ -343,7 +347,7 @@ impl ArgFromMutContext for common::SecretKey { FromStr::from_str(raw).or_else(|_parse_err| { // Or it can be an alias ctx.wallet - .find_key(raw) + .find_key(raw, None) .map_err(|_find_err| format!("Unknown key {}", raw)) }) } @@ -360,13 +364,13 @@ impl ArgFromMutContext for common::PublicKey { // Or it can be a public key hash in hex string FromStr::from_str(raw) .map(|pkh: PublicKeyHash| { - let key = ctx.wallet.find_key_by_pkh(&pkh).unwrap(); + let key = ctx.wallet.find_key_by_pkh(&pkh, None).unwrap(); key.ref_to() }) // Or it can be an alias that may be found in the wallet .or_else(|_parse_err| { ctx.wallet - .find_key(raw) + .find_key(raw, None) .map(|x| x.ref_to()) .map_err(|x| x.to_string()) }) @@ -384,7 +388,7 @@ impl ArgFromMutContext for ExtendedSpendingKey { FromStr::from_str(raw).or_else(|_parse_err| { // Or it is a stored alias of one ctx.wallet - .find_spending_key(raw) + .find_spending_key(raw, None) .map_err(|_find_err| format!("Unknown spending key {}", raw)) }) } diff --git a/apps/src/lib/client/mod.rs b/apps/src/lib/client/mod.rs index 486eb3c26d..57f3c5a043 100644 --- a/apps/src/lib/client/mod.rs +++ b/apps/src/lib/client/mod.rs @@ -1,6 +1,4 @@ pub mod rpc; pub mod signing; -pub mod tendermint_rpc_types; pub mod tx; -pub mod types; pub mod utils; diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 3fb4d8eea8..19c5fc1893 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -2,20 +2,20 @@ use std::borrow::Cow; use std::cmp::Ordering; -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::{HashMap, HashSet}; use std::convert::TryInto; use std::fs::File; use std::io::{self, Write}; use std::iter::Iterator; use std::str::FromStr; +use std::time::Duration; use async_std::fs; use async_std::path::PathBuf; use async_std::prelude::*; use borsh::{BorshDeserialize, BorshSerialize}; use data_encoding::HEXLOWER; -use eyre::{eyre, Context as EyreContext}; -use itertools::Itertools; +use itertools::{Either, Itertools}; use masp_primitives::asset_type::AssetType; use masp_primitives::merkle_tree::MerklePath; use masp_primitives::primitives::ViewingKey; @@ -25,294 +25,127 @@ use masp_primitives::zip32::ExtendedFullViewingKey; use namada::ledger::events::Event; use namada::ledger::governance::parameters::GovParams; use namada::ledger::governance::storage as gov_storage; +use namada::ledger::masp::{ + Conversions, PinnedBalanceError, ShieldedContext, ShieldedUtils, +}; use namada::ledger::native_vp::governance::utils::Votes; use namada::ledger::parameters::{storage as param_storage, EpochDuration}; use namada::ledger::pos::types::{decimal_mult_u64, WeightedValidator}; use namada::ledger::pos::{ - self, is_validator_slashes_key, BondId, Bonds, PosParams, Slash, Unbonds, + self, is_validator_slashes_key, Bonds, PosParams, Slash, Unbonds, }; -use namada::ledger::queries::{self, RPC}; +use namada::ledger::queries::RPC; +use namada::ledger::rpc::TxResponse; use namada::ledger::storage::ConversionState; -use namada::proto::{SignedTxData, Tx}; +use namada::ledger::wallet::Wallet; use namada::types::address::{masp, tokens, Address}; use namada::types::governance::{ - OfflineProposal, OfflineVote, ProposalResult, ProposalVote, TallyResult, - VotePower, + OfflineProposal, OfflineVote, ProposalResult, VotePower, }; -use namada::types::hash::Hash; use namada::types::key::*; use namada::types::masp::{BalanceOwner, ExtendedViewingKey, PaymentAddress}; -use namada::types::storage::{ - BlockHeight, BlockResults, Epoch, Key, KeySeg, PrefixValue, TxIndex, -}; -use namada::types::token::{balance_key, Transfer}; -use namada::types::transaction::{ - process_tx, AffineCurve, DecryptedTx, EllipticCurve, PairingEngine, TxType, - WrapperTx, -}; +use namada::types::storage::{BlockHeight, BlockResults, Epoch, Key, KeySeg}; use namada::types::{address, storage, token}; use rust_decimal::Decimal; -use tokio::time::{Duration, Instant}; -use crate::cli::{self, args, Context}; -use crate::client::tendermint_rpc_types::TxResponse; -use crate::client::tx::{ - Conversions, PinnedBalanceError, TransactionDelta, TransferDelta, -}; +use crate::cli::{self, args}; use crate::facade::tendermint::merkle::proof::Proof; -use crate::facade::tendermint_config::net::Address as TendermintAddress; use crate::facade::tendermint_rpc::error::Error as TError; -use crate::facade::tendermint_rpc::query::Query; -use crate::facade::tendermint_rpc::{ - Client, HttpClient, Order, SubscriptionClient, WebSocketClient, -}; +use crate::wallet::CliWalletUtils; /// Query the status of a given transaction. /// /// If a response is not delivered until `deadline`, we exit the cli with an /// error. -pub async fn query_tx_status( - status: TxEventQuery<'_>, - address: TendermintAddress, - deadline: Instant, +pub async fn query_tx_status( + client: &C, + status: namada::ledger::rpc::TxEventQuery<'_>, + deadline: Duration, ) -> Event { - const ONE_SECOND: Duration = Duration::from_secs(1); - // sleep for the duration of `backoff`, - // and update the underlying value - async fn sleep_update(query: TxEventQuery<'_>, backoff: &mut Duration) { - tracing::debug!( - ?query, - duration = ?backoff, - "Retrying tx status query after timeout", - ); - // simple linear backoff - if an event is not available, - // increase the backoff duration by one second - tokio::time::sleep(*backoff).await; - *backoff += ONE_SECOND; - } - tokio::time::timeout_at(deadline, async move { - let client = HttpClient::new(address).unwrap(); - let mut backoff = ONE_SECOND; - - loop { - tracing::debug!(query = ?status, "Querying tx status"); - let maybe_event = match query_tx_events(&client, status).await { - Ok(response) => response, - Err(err) => { - tracing::debug!(%err, "ABCI query failed"); - sleep_update(status, &mut backoff).await; - continue; - } - }; - if let Some(e) = maybe_event { - break Ok(e); - } - sleep_update(status, &mut backoff).await; - } - }) - .await - .map_err(|_| { - eprintln!("Transaction status query deadline of {deadline:?} exceeded"); - }) - .and_then(|result| result) - .unwrap_or_else(|_| cli::safe_exit(1)) + namada::ledger::rpc::query_tx_status(client, status, deadline).await } /// Query the epoch of the last committed block -pub async fn query_epoch(args: args::Query) -> Epoch { - let client = HttpClient::new(args.ledger_address).unwrap(); - let epoch = unwrap_client_response(RPC.shell().epoch(&client).await); - println!("Last committed epoch: {}", epoch); - epoch +pub async fn query_epoch( + client: &C, +) -> Epoch { + namada::ledger::rpc::query_epoch(client).await } /// Query the last committed block -pub async fn query_block( - args: args::Query, +pub async fn query_block( + client: &C, ) -> crate::facade::tendermint_rpc::endpoint::block::Response { - let client = HttpClient::new(args.ledger_address).unwrap(); - let response = client.latest_block().await.unwrap(); - println!( - "Last committed block ID: {}, height: {}, time: {}", - response.block_id, - response.block.header.height, - response.block.header.time - ); - response + namada::ledger::rpc::query_block(client).await } /// Query the results of the last committed block -pub async fn query_results(args: args::Query) -> Vec { - let client = HttpClient::new(args.ledger_address).unwrap(); - unwrap_client_response(RPC.shell().read_results(&client).await) -} - -/// Obtain the known effects of all accepted shielded and transparent -/// transactions. If an owner is specified, then restrict the set to only -/// 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, - ledger_address: TendermintAddress, - query_owner: &Option, - query_token: &Option
, -) -> BTreeMap<(BlockHeight, TxIndex), (Epoch, TransferDelta, TransactionDelta)> -{ - const TXS_PER_PAGE: u8 = 100; - // Connect to the Tendermint server holding the transactions - let client = HttpClient::new(ledger_address.clone()).unwrap(); - // Build up the context that will be queried for transactions - let _ = ctx.shielded.load(); - let vks = ctx.wallet.get_viewing_keys(); - let fvks: Vec<_> = vks - .values() - .map(|fvk| ExtendedFullViewingKey::from(*fvk).fvk.vk) - .collect(); - ctx.shielded.fetch(&ledger_address, &[], &fvks).await; - // Save the update state so that future fetches can be short-circuited - let _ = ctx.shielded.save(); - // Required for filtering out rejected transactions from Tendermint - // responses - let block_results = query_results(args::Query { ledger_address }).await; - let mut transfers = ctx.shielded.get_tx_deltas().clone(); - // Construct the set of addresses relevant to user's query - let relevant_addrs = match &query_owner { - Some(BalanceOwner::Address(owner)) => vec![owner.clone()], - // MASP objects are dealt with outside of tx_search - Some(BalanceOwner::FullViewingKey(_viewing_key)) => vec![], - Some(BalanceOwner::PaymentAddress(_owner)) => vec![], - // Unspecified owner means all known addresses are considered relevant - None => ctx.wallet.get_addresses().into_values().collect(), - }; - // Find all transactions to or from the relevant address set - for addr in relevant_addrs { - for prop in ["transfer.source", "transfer.target"] { - // Query transactions involving the current address - let mut tx_query = Query::eq(prop, addr.encode()); - // Elaborate the query if requested by the user - if let Some(token) = &query_token { - tx_query = tx_query.and_eq("transfer.token", token.encode()); - } - for page in 1.. { - let txs = &client - .tx_search( - tx_query.clone(), - true, - page, - TXS_PER_PAGE, - Order::Ascending, - ) - .await - .expect("Unable to query for transactions") - .txs; - for response_tx in txs { - let height = BlockHeight(response_tx.height.value()); - let idx = TxIndex(response_tx.index); - // Only process yet unprocessed transactions which have been - // accepted by node VPs - let should_process = !transfers - .contains_key(&(height, idx)) - && block_results[u64::from(height) as usize] - .is_accepted(idx.0 as usize); - if !should_process { - continue; - } - let tx = Tx::try_from(response_tx.tx.as_ref()) - .expect("Ill-formed Tx"); - let mut wrapper = None; - let mut transfer = None; - extract_payload(tx, &mut wrapper, &mut transfer); - // Epoch data is not needed for transparent transactions - let epoch = wrapper.map(|x| x.epoch).unwrap_or_default(); - if let Some(transfer) = transfer { - // Skip MASP addresses as they are already handled by - // ShieldedContext - if transfer.source == masp() - || transfer.target == masp() - { - continue; - } - // Describe how a Transfer simply subtracts from one - // account and adds the same to another - let mut delta = TransferDelta::default(); - let tfer_delta = Amount::from_nonnegative( - transfer.token.clone(), - u64::from(transfer.amount), - ) - .expect("invalid value for amount"); - delta.insert( - transfer.source, - Amount::zero() - &tfer_delta, - ); - delta.insert(transfer.target, tfer_delta); - // No shielded accounts are affected by this Transfer - transfers.insert( - (height, idx), - (epoch, delta, TransactionDelta::new()), - ); - } - } - // An incomplete page signifies no more transactions - if (txs.len() as u8) < TXS_PER_PAGE { - break; - } - } - } - } - transfers +pub async fn query_results( + client: &C, +) -> Vec { + namada::ledger::rpc::query_results(client).await } /// 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)); +pub async fn query_transfers< + C: namada::ledger::queries::Client, + U: ShieldedUtils, +>( + client: &C, + wallet: &mut Wallet, + shielded: &mut ShieldedContext, + args: args::QueryTransfers, +) { + let query_token = args.token; + let query_owner = args.owner.map_or_else( + || Either::Right(wallet.get_addresses().into_values().collect()), + Either::Left, + ); + let _ = shielded.load(); // Obtain the effects of all shielded and transparent transactions - let transfers = query_tx_deltas( - &mut ctx, - args.query.ledger_address.clone(), - &query_owner, - &query_token, - ) - .await; + let transfers = shielded + .query_tx_deltas( + client, + &query_owner, + &query_token, + &wallet.get_viewing_keys(), + ) + .await; // To facilitate lookups of human-readable token names let tokens = tokens(); - let vks = ctx.wallet.get_viewing_keys(); + let vks = wallet.get_viewing_keys(); // To enable ExtendedFullViewingKeys to be displayed instead of ViewingKeys let fvk_map: HashMap<_, _> = vks .values() .map(|fvk| (ExtendedFullViewingKey::from(*fvk).fvk.vk, fvk)) .collect(); - // Connect to the Tendermint server holding the transactions - let client = HttpClient::new(args.query.ledger_address.clone()).unwrap(); // Now display historical shielded and transparent transactions for ((height, idx), (epoch, tfer_delta, tx_delta)) in transfers { // Check if this transfer pertains to the supplied owner let mut relevant = match &query_owner { - Some(BalanceOwner::FullViewingKey(fvk)) => tx_delta + Either::Left(BalanceOwner::FullViewingKey(fvk)) => tx_delta .contains_key(&ExtendedFullViewingKey::from(*fvk).fvk.vk), - Some(BalanceOwner::Address(owner)) => { + Either::Left(BalanceOwner::Address(owner)) => { tfer_delta.contains_key(owner) } - Some(BalanceOwner::PaymentAddress(_owner)) => false, - None => true, + Either::Left(BalanceOwner::PaymentAddress(_owner)) => false, + Either::Right(_) => true, }; // Realize and decode the shielded changes to enable relevance check let mut shielded_accounts = HashMap::new(); for (acc, amt) in tx_delta { // Realize the rewards that would have been attained upon the // transaction's reception - let amt = ctx - .shielded + let amt = shielded .compute_exchanged_amount( - client.clone(), + client, amt, epoch, Conversions::new(), ) .await .0; - let dec = - ctx.shielded.decode_amount(client.clone(), amt, epoch).await; + let dec = shielded.decode_amount(client, amt, epoch).await; shielded_accounts.insert(acc, dec); } // Check if this transfer pertains to the supplied token @@ -378,43 +211,14 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { } } -/// Extract the payload from the given Tx object -fn extract_payload( - tx: Tx, - wrapper: &mut Option, - transfer: &mut Option, -) { - match process_tx(tx) { - Ok(TxType::Wrapper(wrapper_tx)) => { - let privkey = ::G2Affine::prime_subgroup_generator(); - extract_payload( - Tx::from(match wrapper_tx.decrypt(privkey) { - Ok(tx) => DecryptedTx::Decrypted(tx), - _ => DecryptedTx::Undecryptable(wrapper_tx.clone()), - }), - wrapper, - transfer, - ); - *wrapper = Some(wrapper_tx); - } - Ok(TxType::Decrypted(DecryptedTx::Decrypted(tx))) => { - let empty_vec = vec![]; - let tx_data = tx.data.as_ref().unwrap_or(&empty_vec); - let _ = SignedTxData::try_from_slice(tx_data).map(|signed| { - Transfer::try_from_slice(&signed.data.unwrap()[..]) - .map(|tfer| *transfer = Some(tfer)) - }); - } - _ => {} - } -} - /// Query the raw bytes of given storage key -pub async fn query_raw_bytes(_ctx: Context, args: args::QueryRawBytes) { - let client = HttpClient::new(args.query.ledger_address).unwrap(); - let response = unwrap_client_response( +pub async fn query_raw_bytes( + client: &C, + args: args::QueryRawBytes, +) { + let response = unwrap_client_response::( RPC.shell() - .storage_value(&client, None, None, false, &args.storage_key) + .storage_value(client, None, None, false, &args.storage_key) .await, ); if !response.data.is_empty() { @@ -425,41 +229,50 @@ 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) { +pub async fn query_balance< + C: namada::ledger::queries::Client + Sync, + U: ShieldedUtils, +>( + client: &C, + wallet: &mut Wallet, + shielded: &mut ShieldedContext, + args: args::QueryBalance, +) { // 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 { Some(BalanceOwner::FullViewingKey(_viewing_key)) => { - query_shielded_balance(&mut ctx, args).await + query_shielded_balance(client, wallet, shielded, args).await } Some(BalanceOwner::Address(_owner)) => { - query_transparent_balance(&mut ctx, args).await + query_transparent_balance(client, wallet, args).await } Some(BalanceOwner::PaymentAddress(_owner)) => { - query_pinned_balance(&mut ctx, args).await + query_pinned_balance(client, wallet, shielded, args).await } None => { // Print pinned balance - query_pinned_balance(&mut ctx, args.clone()).await; + query_pinned_balance(client, wallet, shielded, args.clone()).await; // Print shielded balance - query_shielded_balance(&mut ctx, args.clone()).await; + query_shielded_balance(client, wallet, shielded, args.clone()) + .await; // Then print transparent balance - query_transparent_balance(&mut ctx, args).await; + query_transparent_balance(client, wallet, args).await; } }; } /// Query token balance(s) -pub async fn query_transparent_balance( - ctx: &mut Context, +pub async fn query_transparent_balance< + C: namada::ledger::queries::Client + Sync, +>( + client: &C, + wallet: &mut Wallet, args: args::QueryBalance, ) { - let client = HttpClient::new(args.query.ledger_address).unwrap(); let tokens = address::tokens(); match (args.token, args.owner) { (Some(token), Some(owner)) => { - let token = ctx.get(&token); - let owner = ctx.get_cached(&owner); let key = match &args.sub_prefix { Some(sub_prefix) => { let sub_prefix = Key::parse(sub_prefix).unwrap(); @@ -476,7 +289,7 @@ pub async fn query_transparent_balance( .get(&token) .map(|c| Cow::Borrowed(*c)) .unwrap_or_else(|| Cow::Owned(token.to_string())); - match query_storage_value::(&client, &key).await { + match query_storage_value::(client, &key).await { Some(balance) => match &args.sub_prefix { Some(sub_prefix) => { println!( @@ -492,15 +305,14 @@ pub async fn query_transparent_balance( } } (None, Some(owner)) => { - let owner = ctx.get_cached(&owner); for (token, _) in tokens { let prefix = token.to_db_key().into(); let balances = - query_storage_prefix::(&client, &prefix) + query_storage_prefix::(client, &prefix) .await; if let Some(balances) = balances { print_balances( - ctx, + wallet, balances, &token, owner.address().as_ref(), @@ -509,21 +321,21 @@ pub async fn query_transparent_balance( } } (Some(token), None) => { - let token = ctx.get(&token); let prefix = token.to_db_key().into(); let balances = - query_storage_prefix::(&client, &prefix).await; + query_storage_prefix::(client, &prefix).await; if let Some(balances) = balances { - print_balances(ctx, balances, &token, None); + print_balances(wallet, balances, &token, None); } } (None, None) => { for (token, _) in tokens { let key = token::balance_prefix(&token); let balances = - query_storage_prefix::(&client, &key).await; + query_storage_prefix::(client, &key) + .await; if let Some(balances) = balances { - print_balances(ctx, balances, &token, None); + print_balances(wallet, balances, &token, None); } } } @@ -531,45 +343,42 @@ 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< + C: namada::ledger::queries::Client, + U: ShieldedUtils, +>( + client: &C, + wallet: &mut Wallet, + shielded: &mut ShieldedContext, + args: args::QueryBalance, +) { // Map addresses to token names let tokens = address::tokens(); - let owners = if let Some(pa) = args - .owner - .and_then(|x| ctx.get_cached(&x).payment_address()) + let owners = if let Some(pa) = args.owner.and_then(|x| x.payment_address()) { vec![pa] } else { - ctx.wallet + wallet .get_payment_addrs() .into_values() .filter(PaymentAddress::is_pinned) .collect() }; // Get the viewing keys with which to try note decryptions - let viewing_keys: Vec = ctx - .wallet + let viewing_keys: Vec = wallet .get_viewing_keys() .values() .map(|fvk| ExtendedFullViewingKey::from(*fvk).fvk.vk) .collect(); - // Build up the context that will be queried for asset decodings - let _ = ctx.shielded.load(); - // Establish connection with which to do exchange rate queries - let client = HttpClient::new(args.query.ledger_address.clone()).unwrap(); + let _ = shielded.load(); // Print the token balances by payment address for owner in owners { let mut balance = Err(PinnedBalanceError::InvalidViewingKey); // Find the viewing key that can recognize payments the current payment // address for vk in &viewing_keys { - balance = ctx - .shielded - .compute_exchanged_pinned_balance( - &args.query.ledger_address, - owner, - vk, - ) + balance = shielded + .compute_exchanged_pinned_balance(client, owner, vk) .await; if balance != Err(PinnedBalanceError::InvalidViewingKey) { break; @@ -590,13 +399,8 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { }; let vk = ExtendedFullViewingKey::from(fvk).fvk.vk; // Use the given viewing key to decrypt pinned transaction data - balance = ctx - .shielded - .compute_exchanged_pinned_balance( - &args.query.ledger_address, - owner, - &vk, - ) + balance = shielded + .compute_exchanged_pinned_balance(client, owner, &vk) .await } // Now print out the received quantities according to CLI arguments @@ -609,12 +413,11 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { println!("Payment address {} has not yet been consumed.", owner) } (Ok((balance, epoch)), Some(token)) => { - let token = ctx.get(token); // Extract and print only the specified token from the total let (_asset_type, balance) = value_by_address(&balance, token.clone(), epoch); let currency_code = tokens - .get(&token) + .get(token) .map(|c| Cow::Borrowed(*c)) .unwrap_or_else(|| Cow::Owned(token.to_string())); if balance == 0 { @@ -635,10 +438,8 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { (Ok((balance, epoch)), None) => { let mut found_any = false; // Print balances by human-readable token names - let balance = ctx - .shielded - .decode_amount(client.clone(), balance, epoch) - .await; + let balance = + shielded.decode_amount(client, balance, epoch).await; for (addr, value) in balance.components() { let asset_value = token::Amount::from(*value as u64); if !found_any { @@ -669,7 +470,7 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { } fn print_balances( - ctx: &Context, + wallet: &Wallet, balances: impl Iterator, token: &Address, target: Option<&Address>, @@ -694,7 +495,7 @@ fn print_balances( "with {}: {}, owned by {}", sub_prefix, balance, - lookup_alias(ctx, owner) + lookup_alias(wallet, owner) ), )), None => token::is_any_token_balance_key(&key).map(|owner| { @@ -703,7 +504,7 @@ fn print_balances( format!( ": {}, owned by {}", balance, - lookup_alias(ctx, owner) + lookup_alias(wallet, owner) ), ) }), @@ -722,7 +523,7 @@ fn print_balances( if print_num == 0 { match target { Some(t) => { - writeln!(w, "No balances owned by {}", lookup_alias(ctx, t)) + writeln!(w, "No balances owned by {}", lookup_alias(wallet, t)) .unwrap() } None => { @@ -733,9 +534,12 @@ fn print_balances( } /// Query Proposals -pub async fn query_proposal(_ctx: Context, args: args::QueryProposal) { - async fn print_proposal( - client: &HttpClient, +pub async fn query_proposal( + client: &C, + args: args::QueryProposal, +) { + async fn print_proposal( + client: &C, id: u64, current_epoch: Epoch, details: bool, @@ -745,22 +549,23 @@ pub async fn query_proposal(_ctx: Context, args: args::QueryProposal) { let end_epoch_key = gov_storage::get_voting_end_epoch_key(id); let author = - query_storage_value::
(client, &author_key).await?; + query_storage_value::(client, &author_key).await?; let start_epoch = - query_storage_value::(client, &start_epoch_key).await?; + query_storage_value::(client, &start_epoch_key).await?; let end_epoch = - query_storage_value::(client, &end_epoch_key).await?; + query_storage_value::(client, &end_epoch_key).await?; if details { let content_key = gov_storage::get_content_key(id); let grace_epoch_key = gov_storage::get_grace_epoch_key(id); - let content = query_storage_value::>( + let content = query_storage_value::>( client, &content_key, ) .await?; let grace_epoch = - query_storage_value::(client, &grace_epoch_key).await?; + query_storage_value::(client, &grace_epoch_key) + .await?; println!("Proposal: {}", id); println!("{:4}Author: {}", "", author); @@ -812,11 +617,10 @@ pub async fn query_proposal(_ctx: Context, args: args::QueryProposal) { Some(()) } - let client = HttpClient::new(args.query.ledger_address.clone()).unwrap(); - let current_epoch = query_epoch(args.query.clone()).await; + let current_epoch = query_epoch(client).await; match args.proposal_id { Some(id) => { - if print_proposal(&client, id, current_epoch, true) + if print_proposal::(client, id, current_epoch, true) .await .is_none() { @@ -826,12 +630,12 @@ pub async fn query_proposal(_ctx: Context, args: args::QueryProposal) { None => { let last_proposal_id_key = gov_storage::get_counter_key(); let last_proposal_id = - query_storage_value::(&client, &last_proposal_id_key) + query_storage_value::(client, &last_proposal_id_key) .await .unwrap(); for id in 0..last_proposal_id { - if print_proposal(&client, id, current_epoch, false) + if print_proposal::(client, id, current_epoch, false) .await .is_none() { @@ -860,38 +664,36 @@ pub fn value_by_address( } /// Query token shielded balance(s) -pub async fn query_shielded_balance( - ctx: &mut Context, +pub async fn query_shielded_balance< + C: namada::ledger::queries::Client + Sync, + U: ShieldedUtils, +>( + client: &C, + wallet: &mut Wallet, + shielded: &mut ShieldedContext, args: args::QueryBalance, ) { // Used to control whether balances for all keys or a specific key are // printed - let owner = args - .owner - .and_then(|x| ctx.get_cached(&x).full_viewing_key()); + let owner = args.owner.and_then(|x| x.full_viewing_key()); // Used to control whether conversions are automatically performed let no_conversions = args.no_conversions; // Viewing keys are used to query shielded balances. If a spending key is // provided, then convert to a viewing key first. let viewing_keys = match owner { Some(viewing_key) => vec![viewing_key], - None => ctx.wallet.get_viewing_keys().values().copied().collect(), + None => wallet.get_viewing_keys().values().copied().collect(), }; - // Build up the context that will be queried for balances - let _ = ctx.shielded.load(); + let _ = shielded.load(); let fvks: Vec<_> = viewing_keys .iter() .map(|fvk| ExtendedFullViewingKey::from(*fvk).fvk.vk) .collect(); - ctx.shielded - .fetch(&args.query.ledger_address, &[], &fvks) - .await; + shielded.fetch(client, &[], &fvks).await; // Save the update state so that future fetches can be short-circuited - let _ = ctx.shielded.save(); + let _ = shielded.save(); // The epoch is required to identify timestamped tokens - let epoch = query_epoch(args.query.clone()).await; - // Establish connection with which to do exchange rate queries - let client = HttpClient::new(args.query.ledger_address.clone()).unwrap(); + let epoch = query_epoch(client).await; // Map addresses to token names let tokens = address::tokens(); match (args.token, owner.is_some()) { @@ -901,21 +703,17 @@ pub async fn query_shielded_balance( let viewing_key = ExtendedFullViewingKey::from(viewing_keys[0]).fvk.vk; let balance: Amount = if no_conversions { - ctx.shielded + shielded .compute_shielded_balance(&viewing_key) .expect("context should contain viewing key") } else { - ctx.shielded - .compute_exchanged_balance( - client.clone(), - &viewing_key, - epoch, - ) + shielded + .compute_exchanged_balance(client, &viewing_key, epoch) .await .expect("context should contain viewing key") }; // Compute the unique asset identifier from the token address - let token = ctx.get(&token); + let token = token; let asset_type = AssetType::new( (token.clone(), epoch.0) .try_to_vec() @@ -946,16 +744,12 @@ pub async fn query_shielded_balance( // Query the multi-asset balance at the given spending key let viewing_key = ExtendedFullViewingKey::from(fvk).fvk.vk; let balance = if no_conversions { - ctx.shielded + shielded .compute_shielded_balance(&viewing_key) .expect("context should contain viewing key") } else { - ctx.shielded - .compute_exchanged_balance( - client.clone(), - &viewing_key, - epoch, - ) + shielded + .compute_exchanged_balance(client, &viewing_key, epoch) .await .expect("context should contain viewing key") }; @@ -972,10 +766,8 @@ pub async fn query_shielded_balance( // Print non-zero balances whose asset types can be decoded for (asset_type, balances) in balances { // Decode the asset type - let decoded = ctx - .shielded - .decode_asset_type(client.clone(), asset_type) - .await; + let decoded = + shielded.decode_asset_type(client, asset_type).await; match decoded { Some((addr, asset_epoch)) if asset_epoch == epoch => { // Only assets with the current timestamp count @@ -1020,7 +812,7 @@ pub async fn query_shielded_balance( // users (Some(token), false) => { // Compute the unique asset identifier from the token address - let token = ctx.get(&token); + let token = token; let asset_type = AssetType::new( (token.clone(), epoch.0) .try_to_vec() @@ -1038,16 +830,12 @@ pub async fn query_shielded_balance( // Query the multi-asset balance at the given spending key let viewing_key = ExtendedFullViewingKey::from(fvk).fvk.vk; let balance = if no_conversions { - ctx.shielded + shielded .compute_shielded_balance(&viewing_key) .expect("context should contain viewing key") } else { - ctx.shielded - .compute_exchanged_balance( - client.clone(), - &viewing_key, - epoch, - ) + shielded + .compute_exchanged_balance(client, &viewing_key, epoch) .await .expect("context should contain viewing key") }; @@ -1072,31 +860,21 @@ pub async fn query_shielded_balance( ExtendedFullViewingKey::from(viewing_keys[0]).fvk.vk; let balance; if no_conversions { - balance = ctx - .shielded + balance = shielded .compute_shielded_balance(&viewing_key) .expect("context should contain viewing key"); // Print balances by human-readable token names - let decoded_balance = ctx - .shielded - .decode_all_amounts(client.clone(), balance) - .await; + let decoded_balance = + shielded.decode_all_amounts(client, balance).await; print_decoded_balance_with_epoch(decoded_balance); } else { - balance = ctx - .shielded - .compute_exchanged_balance( - client.clone(), - &viewing_key, - epoch, - ) + balance = shielded + .compute_exchanged_balance(client, &viewing_key, epoch) .await .expect("context should contain viewing key"); // Print balances by human-readable token names - let decoded_balance = ctx - .shielded - .decode_amount(client.clone(), balance, epoch) - .await; + let decoded_balance = + shielded.decode_amount(client, balance, epoch).await; print_decoded_balance(decoded_balance); } } @@ -1143,35 +921,35 @@ pub fn print_decoded_balance_with_epoch( } /// Query token amount of owner. -pub async fn get_token_balance( - client: &HttpClient, +pub async fn get_token_balance( + client: &C, token: &Address, owner: &Address, ) -> Option { - let balance_key = balance_key(token, owner); - query_storage_value(client, &balance_key).await + namada::ledger::rpc::get_token_balance(client, token, owner).await } -pub async fn query_proposal_result( - _ctx: Context, +pub async fn query_proposal_result< + C: namada::ledger::queries::Client + Sync, +>( + client: &C, args: args::QueryProposalResult, ) { - let client = HttpClient::new(args.query.ledger_address.clone()).unwrap(); - let current_epoch = query_epoch(args.query.clone()).await; + let current_epoch = query_epoch(client).await; match args.proposal_id { Some(id) => { let end_epoch_key = gov_storage::get_voting_end_epoch_key(id); let end_epoch = - query_storage_value::(&client, &end_epoch_key).await; + query_storage_value::(client, &end_epoch_key).await; match end_epoch { Some(end_epoch) => { if current_epoch > end_epoch { let votes = - get_proposal_votes(&client, end_epoch, id).await; + get_proposal_votes(client, end_epoch, id).await; let proposal_result = - compute_tally(&client, end_epoch, votes).await; + compute_tally(client, end_epoch, votes).await; println!("Proposal: {}", id); println!("{:4}Result: {}", "", proposal_result); } else { @@ -1245,12 +1023,10 @@ pub async fn query_proposal_result( "JSON was not well-formatted for proposal.", ); - let public_key = get_public_key( - &proposal.address, - args.query.ledger_address.clone(), - ) - .await - .expect("Public key should exist."); + let public_key = + get_public_key(client, &proposal.address) + .await + .expect("Public key should exist."); if !proposal.check_signature(&public_key) { eprintln!("Bad proposal signature."); @@ -1258,13 +1034,13 @@ pub async fn query_proposal_result( } let votes = get_proposal_offline_votes( - &client, + client, proposal.clone(), files, ) .await; let proposal_result = - compute_tally(&client, proposal.tally_epoch, votes) + compute_tally(client, proposal.tally_epoch, votes) .await; println!("{:4}Result: {}", "", proposal_result); @@ -1287,18 +1063,18 @@ pub async fn query_proposal_result( } } -pub async fn query_protocol_parameters( - _ctx: Context, - args: args::QueryProtocolParameters, +pub async fn query_protocol_parameters< + C: namada::ledger::queries::Client + Sync, +>( + client: &C, + _args: args::QueryProtocolParameters, ) { - let client = HttpClient::new(args.query.ledger_address).unwrap(); - - let gov_parameters = get_governance_parameters(&client).await; + let gov_parameters = get_governance_parameters(client).await; println!("Governance Parameters\n {:4}", gov_parameters); println!("Protocol parameters"); let key = param_storage::get_epoch_duration_storage_key(); - let epoch_duration = query_storage_value::(&client, &key) + let epoch_duration = query_storage_value::(client, &key) .await .expect("Parameter should be definied."); println!( @@ -1311,26 +1087,26 @@ pub async fn query_protocol_parameters( ); let key = param_storage::get_max_expected_time_per_block_key(); - let max_block_duration = query_storage_value::(&client, &key) + let max_block_duration = query_storage_value::(client, &key) .await .expect("Parameter should be definied."); println!("{:4}Max. block duration: {}", "", max_block_duration); let key = param_storage::get_tx_whitelist_storage_key(); - let vp_whitelist = query_storage_value::>(&client, &key) + let vp_whitelist = query_storage_value::>(client, &key) .await .expect("Parameter should be definied."); println!("{:4}VP whitelist: {:?}", "", vp_whitelist); let key = param_storage::get_tx_whitelist_storage_key(); - let tx_whitelist = query_storage_value::>(&client, &key) + let tx_whitelist = query_storage_value::>(client, &key) .await .expect("Parameter should be definied."); println!("{:4}Transactions whitelist: {:?}", "", tx_whitelist); println!("PoS parameters"); let key = pos::params_key(); - let pos_params = query_storage_value::(&client, &key) + let pos_params = query_storage_value::(client, &key) .await .expect("Parameter should be definied."); println!( @@ -1359,27 +1135,30 @@ pub async fn query_protocol_parameters( } /// Query PoS bond(s) -pub async fn query_bonds(ctx: Context, args: args::QueryBonds) { - let epoch = query_epoch(args.query.clone()).await; - let client = HttpClient::new(args.query.ledger_address).unwrap(); +pub async fn query_bonds( + client: &C, + args: args::QueryBonds, +) { + let epoch = query_epoch(client).await; match (args.owner, args.validator) { (Some(owner), Some(validator)) => { - let source = ctx.get(&owner); - let validator = ctx.get(&validator); + let source = owner; + let validator = validator; // Find owner's delegations to the given validator let bond_id = pos::BondId { source, validator }; let bond_key = pos::bond_key(&bond_id); let bonds = - query_storage_value::(&client, &bond_key).await; + query_storage_value::(client, &bond_key).await; // Find owner's unbonded delegations from the given // validator let unbond_key = pos::unbond_key(&bond_id); let unbonds = - query_storage_value::(&client, &unbond_key).await; + query_storage_value::(client, &unbond_key) + .await; // Find validator's slashes, if any let slashes_key = pos::validator_slashes_key(&bond_id.validator); let slashes = - query_storage_value::(&client, &slashes_key) + query_storage_value::(client, &slashes_key) .await .unwrap_or_default(); @@ -1421,7 +1200,7 @@ pub async fn query_bonds(ctx: Context, args: args::QueryBonds) { } } (None, Some(validator)) => { - let validator = ctx.get(&validator); + let validator = validator; // Find validator's self-bonds let bond_id = pos::BondId { source: validator.clone(), @@ -1429,15 +1208,16 @@ pub async fn query_bonds(ctx: Context, args: args::QueryBonds) { }; let bond_key = pos::bond_key(&bond_id); let bonds = - query_storage_value::(&client, &bond_key).await; + query_storage_value::(client, &bond_key).await; // Find validator's unbonded self-bonds let unbond_key = pos::unbond_key(&bond_id); let unbonds = - query_storage_value::(&client, &unbond_key).await; + query_storage_value::(client, &unbond_key) + .await; // Find validator's slashes, if any let slashes_key = pos::validator_slashes_key(&bond_id.validator); let slashes = - query_storage_value::(&client, &slashes_key) + query_storage_value::(client, &slashes_key) .await .unwrap_or_default(); @@ -1468,17 +1248,19 @@ pub async fn query_bonds(ctx: Context, args: args::QueryBonds) { } } (Some(owner), None) => { - let owner = ctx.get(&owner); + let owner = owner; // Find owner's bonds to any validator let bonds_prefix = pos::bonds_for_source_prefix(&owner); let bonds = - query_storage_prefix::(&client, &bonds_prefix) + query_storage_prefix::(client, &bonds_prefix) .await; // Find owner's unbonds to any validator let unbonds_prefix = pos::unbonds_for_source_prefix(&owner); - let unbonds = - query_storage_prefix::(&client, &unbonds_prefix) - .await; + let unbonds = query_storage_prefix::( + client, + &unbonds_prefix, + ) + .await; let mut total: token::Amount = 0.into(); let mut total_active: token::Amount = 0.into(); @@ -1490,12 +1272,13 @@ pub async fn query_bonds(ctx: Context, args: args::QueryBonds) { // Find validator's slashes, if any let slashes_key = pos::validator_slashes_key(&validator); - let slashes = query_storage_value::( - &client, - &slashes_key, - ) - .await - .unwrap_or_default(); + let slashes = + query_storage_value::( + client, + &slashes_key, + ) + .await + .unwrap_or_default(); let stdout = io::stdout(); let mut w = stdout.lock(); @@ -1541,12 +1324,13 @@ pub async fn query_bonds(ctx: Context, args: args::QueryBonds) { // Find validator's slashes, if any let slashes_key = pos::validator_slashes_key(&validator); - let slashes = query_storage_value::( - &client, - &slashes_key, - ) - .await - .unwrap_or_default(); + let slashes = + query_storage_value::( + client, + &slashes_key, + ) + .await + .unwrap_or_default(); let stdout = io::stdout(); let mut w = stdout.lock(); @@ -1588,13 +1372,15 @@ pub async fn query_bonds(ctx: Context, args: args::QueryBonds) { // Find all the bonds let bonds_prefix = pos::bonds_prefix(); let bonds = - query_storage_prefix::(&client, &bonds_prefix) + query_storage_prefix::(client, &bonds_prefix) .await; // Find all the unbonds let unbonds_prefix = pos::unbonds_prefix(); - let unbonds = - query_storage_prefix::(&client, &unbonds_prefix) - .await; + let unbonds = query_storage_prefix::( + client, + &unbonds_prefix, + ) + .await; let mut total: token::Amount = 0.into(); let mut total_active: token::Amount = 0.into(); @@ -1605,12 +1391,13 @@ pub async fn query_bonds(ctx: Context, args: args::QueryBonds) { // Find validator's slashes, if any let slashes_key = pos::validator_slashes_key(&validator); - let slashes = query_storage_value::( - &client, - &slashes_key, - ) - .await - .unwrap_or_default(); + let slashes = + query_storage_value::( + client, + &slashes_key, + ) + .await + .unwrap_or_default(); let stdout = io::stdout(); let mut w = stdout.lock(); @@ -1656,12 +1443,13 @@ pub async fn query_bonds(ctx: Context, args: args::QueryBonds) { // Find validator's slashes, if any let slashes_key = pos::validator_slashes_key(&validator); - let slashes = query_storage_value::( - &client, - &slashes_key, - ) - .await - .unwrap_or_default(); + let slashes = + query_storage_value::( + client, + &slashes_key, + ) + .await + .unwrap_or_default(); let stdout = io::stdout(); let mut w = stdout.lock(); @@ -1706,33 +1494,38 @@ 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) { +pub async fn query_bonded_stake( + client: &C, + args: args::QueryBondedStake, +) { let epoch = match args.epoch { Some(epoch) => epoch, - None => query_epoch(args.query.clone()).await, + None => query_epoch(client).await, }; - let client = HttpClient::new(args.query.ledger_address).unwrap(); // Find the validator set let validator_set_key = pos::validator_set_key(); - let validator_sets = - query_storage_value::(&client, &validator_set_key) - .await - .expect("Validator set should always be set"); + let validator_sets = query_storage_value::( + client, + &validator_set_key, + ) + .await + .expect("Validator set should always be set"); let validator_set = validator_sets .get(epoch) .expect("Validator set should be always set in the current epoch"); match args.validator { Some(validator) => { - let validator = ctx.get(&validator); + let validator = validator; // Find bonded stake for the given validator let validator_deltas_key = pos::validator_deltas_key(&validator); - let validator_deltas = query_storage_value::( - &client, - &validator_deltas_key, - ) - .await; + let validator_deltas = + query_storage_value::( + client, + &validator_deltas_key, + ) + .await; match validator_deltas.and_then(|data| data.get(epoch)) { Some(val_stake) => { let bonded_stake: u64 = val_stake.try_into().expect( @@ -1792,7 +1585,7 @@ pub async fn query_bonded_stake(ctx: Context, args: args::QueryBondedStake) { } let total_deltas_key = pos::total_deltas_key(); let total_deltas = - query_storage_value::(&client, &total_deltas_key) + query_storage_value::(client, &total_deltas_key) .await .expect("Total bonded stake should always be set"); let total_bonded_stake = total_deltas @@ -1806,31 +1599,31 @@ pub async fn query_bonded_stake(ctx: Context, args: args::QueryBondedStake) { } /// Query PoS validator's commission rate -pub async fn query_commission_rate( - ctx: Context, +pub async fn query_commission_rate< + C: namada::ledger::queries::Client + Sync, +>( + client: &C, args: args::QueryCommissionRate, ) { let epoch = match args.epoch { Some(epoch) => epoch, - None => query_epoch(args.query.clone()).await, + None => query_epoch(client).await, }; - let client = HttpClient::new(args.query.ledger_address.clone()).unwrap(); - let validator = ctx.get(&args.validator); - let is_validator = - is_validator(&validator, args.query.ledger_address).await; + let validator = args.validator; + let is_validator = is_validator(client, &validator).await; if is_validator { let validator_commission_key = pos::validator_commission_rate_key(&validator); let validator_max_commission_change_key = pos::validator_max_commission_rate_change_key(&validator); - let commission_rates = query_storage_value::( - &client, + let commission_rates = query_storage_value::( + client, &validator_commission_key, ) .await; - let max_rate_change = query_storage_value::( - &client, + let max_rate_change = query_storage_value::( + client, &validator_max_commission_change_key, ) .await; @@ -1862,15 +1655,17 @@ pub async fn query_commission_rate( } /// Query PoS slashes -pub async fn query_slashes(ctx: Context, args: args::QuerySlashes) { - let client = HttpClient::new(args.query.ledger_address).unwrap(); +pub async fn query_slashes( + client: &C, + args: args::QuerySlashes, +) { match args.validator { Some(validator) => { - let validator = ctx.get(&validator); + let validator = validator; // Find slashes for the given validator let slashes_key = pos::validator_slashes_key(&validator); let slashes = - query_storage_value::(&client, &slashes_key) + query_storage_value::(client, &slashes_key) .await; match slashes { Some(slashes) => { @@ -1893,9 +1688,11 @@ pub async fn query_slashes(ctx: Context, args: args::QuerySlashes) { None => { // Iterate slashes for all validators let slashes_prefix = pos::slashes_prefix(); - let slashes = - query_storage_prefix::(&client, &slashes_prefix) - .await; + let slashes = query_storage_prefix::( + client, + &slashes_prefix, + ) + .await; match slashes { Some(slashes) => { @@ -1932,77 +1729,56 @@ pub async fn query_slashes(ctx: Context, args: args::QuerySlashes) { } /// Dry run a transaction -pub async fn dry_run_tx(ledger_address: &TendermintAddress, tx_bytes: Vec) { - let client = HttpClient::new(ledger_address.clone()).unwrap(); - let (data, height, prove) = (Some(tx_bytes), None, false); - let result = unwrap_client_response( - RPC.shell().dry_run_tx(&client, data, height, prove).await, - ) - .data; - println!("Dry-run result: {}", result); +pub async fn dry_run_tx( + client: &C, + tx_bytes: Vec, +) { + println!( + "Dry-run result: {}", + namada::ledger::rpc::dry_run_tx(client, tx_bytes).await + ); } /// Get account's public key stored in its storage sub-space -pub async fn get_public_key( +pub async fn get_public_key( + client: &C, address: &Address, - ledger_address: TendermintAddress, ) -> Option { - let client = HttpClient::new(ledger_address).unwrap(); - let key = pk_key(address); - query_storage_value(&client, &key).await + namada::ledger::rpc::get_public_key(client, address).await } /// Check if the given address is a known validator. -pub async fn is_validator( +pub async fn is_validator( + client: &C, address: &Address, - ledger_address: TendermintAddress, ) -> bool { - let client = HttpClient::new(ledger_address).unwrap(); - unwrap_client_response(RPC.vp().pos().is_validator(&client, address).await) + namada::ledger::rpc::is_validator(client, address).await } /// Check if a given address is a known delegator -pub async fn is_delegator( +pub async fn is_delegator( + client: &C, address: &Address, - ledger_address: TendermintAddress, ) -> bool { - let client = HttpClient::new(ledger_address).unwrap(); - let bonds_prefix = pos::bonds_for_source_prefix(address); - let bonds = - query_storage_prefix::(&client, &bonds_prefix).await; - bonds.is_some() && bonds.unwrap().count() > 0 + namada::ledger::rpc::is_delegator(client, address).await } -pub async fn is_delegator_at( - client: &HttpClient, +pub async fn is_delegator_at( + client: &C, address: &Address, epoch: Epoch, ) -> bool { - let key = pos::bonds_for_source_prefix(address); - let bonds_iter = query_storage_prefix::(client, &key).await; - if let Some(mut bonds) = bonds_iter { - bonds.any(|(_, bond)| bond.get(epoch).is_some()) - } else { - false - } + namada::ledger::rpc::is_delegator_at(client, address, epoch).await } /// Check if the address exists on chain. Established address exists if it has a /// stored validity predicate. Implicit and internal addresses always return /// true. -pub async fn known_address( +pub async fn known_address( + client: &C, address: &Address, - ledger_address: TendermintAddress, ) -> bool { - let client = HttpClient::new(ledger_address).unwrap(); - match address { - Address::Established(_) => { - // Established account exists if it has a VP - let key = storage::Key::validity_predicate(address); - query_has_storage_key(&client, &key).await - } - Address::Implicit(_) | Address::Internal(_) => true, - } + namada::ledger::rpc::known_address(client, address).await } /// Accumulate slashes starting from `epoch_start` until (optionally) @@ -2140,19 +1916,21 @@ fn process_unbonds_query( } /// Query for all conversions. -pub async fn query_conversions(ctx: Context, args: args::QueryConversions) { +pub async fn query_conversions( + client: &C, + args: args::QueryConversions, +) { // The chosen token type of the conversions - let target_token = args.token.as_ref().map(|x| ctx.get(x)); + let target_token = args.token; // To facilitate human readable token addresses let tokens = address::tokens(); - let client = HttpClient::new(args.query.ledger_address).unwrap(); let masp_addr = masp(); let key_prefix: Key = masp_addr.to_db_key().into(); let state_key = key_prefix .push(&(token::CONVERSION_KEY_PREFIX.to_owned())) .unwrap(); let conv_state = - query_storage_value::(&client, &state_key) + query_storage_value::(client, &state_key) .await .expect("Conversions should be defined"); // Track whether any non-sentinel conversions are found @@ -2203,8 +1981,8 @@ pub async fn query_conversions(ctx: Context, args: args::QueryConversions) { } /// Query a conversion. -pub async fn query_conversion( - client: HttpClient, +pub async fn query_conversion( + client: &C, asset_type: AssetType, ) -> Option<( Address, @@ -2212,264 +1990,90 @@ pub async fn query_conversion( masp_primitives::transaction::components::Amount, MerklePath, )> { - Some(unwrap_client_response( - RPC.shell().read_conversion(&client, &asset_type).await, - )) + namada::ledger::rpc::query_conversion(client, asset_type).await } /// Query a storage value and decode it with [`BorshDeserialize`]. -pub async fn query_storage_value( - client: &HttpClient, +pub async fn query_storage_value( + client: &C, key: &storage::Key, ) -> Option where T: BorshDeserialize, { - // In case `T` is a unit (only thing that encodes to 0 bytes), we have to - // use `storage_has_key` instead of `storage_value`, because `storage_value` - // returns 0 bytes when the key is not found. - let maybe_unit = T::try_from_slice(&[]); - if let Ok(unit) = maybe_unit { - return if unwrap_client_response( - RPC.shell().storage_has_key(client, key).await, - ) { - Some(unit) - } else { - None - }; - } - - let response = unwrap_client_response( - RPC.shell() - .storage_value(client, None, None, false, key) - .await, - ); - if response.data.is_empty() { - return None; - } - T::try_from_slice(&response.data[..]) - .map(Some) - .unwrap_or_else(|err| { - eprintln!("Error decoding the value: {}", err); - cli::safe_exit(1) - }) + namada::ledger::rpc::query_storage_value(client, key).await } /// Query a storage value and the proof without decoding. -pub async fn query_storage_value_bytes( - client: &HttpClient, +pub async fn query_storage_value_bytes< + C: namada::ledger::queries::Client + Sync, +>( + client: &C, key: &storage::Key, height: Option, prove: bool, ) -> (Option>, Option) { - let data = None; - let response = unwrap_client_response( - RPC.shell() - .storage_value(client, data, height, prove, key) - .await, - ); - if response.data.is_empty() { - (None, response.proof) - } else { - (Some(response.data), response.proof) - } + namada::ledger::rpc::query_storage_value_bytes(client, key, height, prove) + .await } /// Query a range of storage values with a matching prefix and decode them with /// [`BorshDeserialize`]. Returns an iterator of the storage keys paired with /// their associated values. -pub async fn query_storage_prefix( - client: &HttpClient, +pub async fn query_storage_prefix< + C: namada::ledger::queries::Client + Sync, + T, +>( + client: &C, key: &storage::Key, ) -> Option> where T: BorshDeserialize, { - let values = unwrap_client_response( - RPC.shell() - .storage_prefix(client, None, None, false, key) - .await, - ); - let decode = - |PrefixValue { key, value }: PrefixValue| match T::try_from_slice( - &value[..], - ) { - Err(err) => { - eprintln!( - "Skipping a value for key {}. Error in decoding: {}", - key, err - ); - None - } - Ok(value) => Some((key, value)), - }; - if values.data.is_empty() { - None - } else { - Some(values.data.into_iter().filter_map(decode)) - } + namada::ledger::rpc::query_storage_prefix(client, key).await } /// Query to check if the given storage key exists. -pub async fn query_has_storage_key( - client: &HttpClient, +pub async fn query_has_storage_key< + C: namada::ledger::queries::Client + Sync, +>( + client: &C, key: &storage::Key, ) -> bool { - unwrap_client_response(RPC.shell().storage_has_key(client, key).await) -} - -/// Represents a query for an event pertaining to the specified transaction -#[derive(Debug, Copy, Clone)] -pub enum TxEventQuery<'a> { - Accepted(&'a str), - Applied(&'a str), -} - -impl<'a> TxEventQuery<'a> { - /// The event type to which this event query pertains - fn event_type(self) -> &'static str { - match self { - TxEventQuery::Accepted(_) => "accepted", - TxEventQuery::Applied(_) => "applied", - } - } - - /// The transaction to which this event query pertains - fn tx_hash(self) -> &'a str { - match self { - TxEventQuery::Accepted(tx_hash) => tx_hash, - TxEventQuery::Applied(tx_hash) => tx_hash, - } - } -} - -/// Transaction event queries are semantically a subset of general queries -impl<'a> From> for Query { - fn from(tx_query: TxEventQuery<'a>) -> Self { - match tx_query { - TxEventQuery::Accepted(tx_hash) => { - Query::default().and_eq("accepted.hash", tx_hash) - } - TxEventQuery::Applied(tx_hash) => { - Query::default().and_eq("applied.hash", tx_hash) - } - } - } + namada::ledger::rpc::query_has_storage_key(client, key).await } /// Call the corresponding `tx_event_query` RPC method, to fetch /// the current status of a transation. -pub async fn query_tx_events( - client: &HttpClient, - tx_event_query: TxEventQuery<'_>, -) -> eyre::Result> { - let tx_hash: Hash = tx_event_query.tx_hash().try_into()?; - match tx_event_query { - TxEventQuery::Accepted(_) => RPC - .shell() - .accepted(client, &tx_hash) - .await - .wrap_err_with(|| { - eyre!("Failed querying whether a transaction was accepted") - }), - TxEventQuery::Applied(_) => RPC - .shell() - .applied(client, &tx_hash) - .await - .wrap_err_with(|| { - eyre!("Error querying whether a transaction was applied") - }), - } +pub async fn query_tx_events( + client: &C, + tx_event_query: namada::ledger::rpc::TxEventQuery<'_>, +) -> std::result::Result< + Option, + ::Error, +> { + namada::ledger::rpc::query_tx_events(client, tx_event_query).await } /// Lookup the full response accompanying the specified transaction event // TODO: maybe remove this in favor of `query_tx_status` -pub async fn query_tx_response( - ledger_address: &TendermintAddress, - tx_query: TxEventQuery<'_>, +pub async fn query_tx_response( + client: &C, + tx_query: namada::ledger::rpc::TxEventQuery<'_>, ) -> Result { - // Connect to the Tendermint server holding the transactions - let (client, driver) = WebSocketClient::new(ledger_address.clone()).await?; - let driver_handle = tokio::spawn(async move { driver.run().await }); - // Find all blocks that apply a transaction with the specified hash - let blocks = &client - .block_search(tx_query.into(), 1, 255, Order::Ascending) - .await - .expect("Unable to query for transaction with given hash") - .blocks; - // Get the block results corresponding to a block to which - // the specified transaction belongs - let block = &blocks - .get(0) - .ok_or_else(|| { - TError::server( - "Unable to find a block applying the given transaction" - .to_string(), - ) - })? - .block; - let response_block_results = client - .block_results(block.header.height) - .await - .expect("Unable to retrieve block containing transaction"); - // Search for the event where the specified transaction is - // applied to the blockchain - let query_event_opt = - response_block_results.end_block_events.and_then(|events| { - events - .iter() - .find(|event| { - event.type_str == tx_query.event_type() - && event.attributes.iter().any(|tag| { - tag.key.as_ref() == "hash" - && tag.value.as_ref() == tx_query.tx_hash() - }) - }) - .cloned() - }); - let query_event = query_event_opt.ok_or_else(|| { - TError::server( - "Unable to find the event corresponding to the specified \ - transaction" - .to_string(), - ) - })?; - // Reformat the event attributes so as to ease value extraction - let event_map: std::collections::HashMap<&str, &str> = query_event - .attributes - .iter() - .map(|tag| (tag.key.as_ref(), tag.value.as_ref())) - .collect(); - // Summarize the transaction results that we were searching for - let result = TxResponse { - info: event_map["info"].to_string(), - log: event_map["log"].to_string(), - height: event_map["height"].to_string(), - hash: event_map["hash"].to_string(), - code: event_map["code"].to_string(), - gas_used: event_map["gas_used"].to_string(), - initialized_accounts: serde_json::from_str( - event_map["initialized_accounts"], - ) - .unwrap_or_default(), - }; - // Signal to the driver to terminate. - client.close()?; - // Await the driver's termination to ensure proper connection closure. - let _ = driver_handle.await.unwrap_or_else(|x| { - eprintln!("{}", x); - cli::safe_exit(1) - }); - Ok(result) + namada::ledger::rpc::query_tx_response(client, tx_query).await } /// Lookup the results of applying the specified transaction to the /// blockchain. -pub async fn query_result(_ctx: Context, args: args::QueryResult) { +pub async fn query_result( + client: &C, + args: args::QueryResult, +) { // First try looking up application event pertaining to given hash. let tx_response = query_tx_response( - &args.query.ledger_address, - TxEventQuery::Applied(&args.tx_hash), + client, + namada::ledger::rpc::TxEventQuery::Applied(&args.tx_hash), ) .await; match tx_response { @@ -2482,8 +2086,8 @@ pub async fn query_result(_ctx: Context, args: args::QueryResult) { Err(err1) => { // If this fails then instead look for an acceptance event. let tx_response = query_tx_response( - &args.query.ledger_address, - TxEventQuery::Accepted(&args.tx_hash), + client, + namada::ledger::rpc::TxEventQuery::Accepted(&args.tx_hash), ) .await; match tx_response { @@ -2501,75 +2105,18 @@ pub async fn query_result(_ctx: Context, args: args::QueryResult) { } } -pub async fn get_proposal_votes( - client: &HttpClient, +pub async fn get_proposal_votes( + client: &C, epoch: Epoch, proposal_id: u64, ) -> Votes { - let validators = get_all_validators(client, epoch).await; - - let vote_prefix_key = - gov_storage::get_proposal_vote_prefix_key(proposal_id); - let vote_iter = - query_storage_prefix::(client, &vote_prefix_key).await; - - let mut yay_validators: HashMap = HashMap::new(); - let mut yay_delegators: HashMap> = - HashMap::new(); - let mut nay_delegators: HashMap> = - HashMap::new(); - - if let Some(vote_iter) = vote_iter { - for (key, vote) in vote_iter { - let voter_address = gov_storage::get_voter_address(&key) - .expect("Vote key should contain the voting address.") - .clone(); - if vote.is_yay() && validators.contains(&voter_address) { - let amount: VotePower = - get_validator_stake(client, epoch, &voter_address) - .await - .into(); - yay_validators.insert(voter_address, amount); - } else if !validators.contains(&voter_address) { - let validator_address = - gov_storage::get_vote_delegation_address(&key) - .expect( - "Vote key should contain the delegation address.", - ) - .clone(); - let delegator_token_amount = get_bond_amount_at( - client, - &voter_address, - &validator_address, - epoch, - ) - .await; - if let Some(amount) = delegator_token_amount { - if vote.is_yay() { - let entry = - yay_delegators.entry(voter_address).or_default(); - entry - .insert(validator_address, VotePower::from(amount)); - } else { - let entry = - nay_delegators.entry(voter_address).or_default(); - entry - .insert(validator_address, VotePower::from(amount)); - } - } - } - } - } - - Votes { - yay_validators, - yay_delegators, - nay_delegators, - } + namada::ledger::rpc::get_proposal_votes(client, epoch, proposal_id).await } -pub async fn get_proposal_offline_votes( - client: &HttpClient, +pub async fn get_proposal_offline_votes< + C: namada::ledger::queries::Client + Sync, +>( + client: &C, proposal: OfflineProposal, files: HashSet, ) -> Votes { @@ -2619,7 +2166,7 @@ pub async fn get_proposal_offline_votes( { let key = pos::bonds_for_source_prefix(&proposal_vote.address); let bonds_iter = - query_storage_prefix::(client, &key).await; + query_storage_prefix::(client, &key).await; if let Some(bonds) = bonds_iter { for (key, epoched_bonds) in bonds { // Look-up slashes for the validator in this key and @@ -2629,7 +2176,7 @@ pub async fn get_proposal_offline_votes( "Delegation key should contain validator address.", ); let slashes_key = pos::validator_slashes_key(&validator); - let slashes = query_storage_value::( + let slashes = query_storage_value::( client, &slashes_key, ) @@ -2698,208 +2245,80 @@ pub async fn get_proposal_offline_votes( } // Compute the result of a proposal -pub async fn compute_tally( - client: &HttpClient, +pub async fn compute_tally( + client: &C, epoch: Epoch, votes: Votes, ) -> ProposalResult { - let total_staked_tokens: VotePower = - get_total_staked_tokens(client, epoch).await.into(); - - let Votes { - yay_validators, - yay_delegators, - nay_delegators, - } = votes; - - let mut total_yay_staked_tokens = VotePower::from(0_u64); - for (_, amount) in yay_validators.clone().into_iter() { - total_yay_staked_tokens += amount; - } - - // YAY: Add delegator amount whose validator didn't vote / voted nay - for (_, vote_map) in yay_delegators.iter() { - for (validator_address, vote_power) in vote_map.iter() { - if !yay_validators.contains_key(validator_address) { - total_yay_staked_tokens += vote_power; - } - } - } - - // NAY: Remove delegator amount whose validator validator vote yay - for (_, vote_map) in nay_delegators.iter() { - for (validator_address, vote_power) in vote_map.iter() { - if yay_validators.contains_key(validator_address) { - total_yay_staked_tokens -= vote_power; - } - } - } - - if total_yay_staked_tokens >= (total_staked_tokens / 3) * 2 { - ProposalResult { - result: TallyResult::Passed, - total_voting_power: total_staked_tokens, - total_yay_power: total_yay_staked_tokens, - total_nay_power: 0, - } - } else { - ProposalResult { - result: TallyResult::Rejected, - total_voting_power: total_staked_tokens, - total_yay_power: total_yay_staked_tokens, - total_nay_power: 0, - } - } + namada::ledger::rpc::compute_tally(client, epoch, votes).await } -pub async fn get_bond_amount_at( - client: &HttpClient, +pub async fn get_bond_amount_at( + client: &C, delegator: &Address, validator: &Address, epoch: Epoch, ) -> Option { - let slashes_key = pos::validator_slashes_key(validator); - let slashes = query_storage_value::(client, &slashes_key) + namada::ledger::rpc::get_bond_amount_at(client, delegator, validator, epoch) .await - .unwrap_or_default(); - let bond_key = pos::bond_key(&BondId { - source: delegator.clone(), - validator: validator.clone(), - }); - let epoched_bonds = query_storage_value::(client, &bond_key).await; - match epoched_bonds { - Some(epoched_bonds) => { - let mut delegated_amount: token::Amount = 0.into(); - for bond in epoched_bonds.iter() { - let mut to_deduct = bond.neg_deltas; - for (epoch_start, &(mut delta)) in - bond.pos_deltas.iter().sorted() - { - // deduct bond's neg_deltas - if to_deduct > delta { - to_deduct -= delta; - // If the whole bond was deducted, continue to - // the next one - continue; - } else { - delta -= to_deduct; - to_deduct = token::Amount::default(); - } - - delta = apply_slashes( - &slashes, - delta, - *epoch_start, - None, - None, - ); - if epoch >= *epoch_start { - delegated_amount += delta; - } - } - } - Some(delegated_amount) - } - None => None, - } } -pub async fn get_all_validators( - client: &HttpClient, +pub async fn get_all_validators( + client: &C, epoch: Epoch, ) -> HashSet
{ - unwrap_client_response( - RPC.vp() - .pos() - .validator_addresses(client, &Some(epoch)) - .await, - ) + namada::ledger::rpc::get_all_validators(client, epoch).await } -pub async fn get_total_staked_tokens( - client: &HttpClient, +pub async fn get_total_staked_tokens< + C: namada::ledger::queries::Client + Sync, +>( + client: &C, epoch: Epoch, ) -> token::Amount { - unwrap_client_response( - RPC.vp().pos().total_stake(client, &Some(epoch)).await, - ) + namada::ledger::rpc::get_total_staked_tokens(client, epoch).await } -async fn get_validator_stake( - client: &HttpClient, +async fn get_validator_stake( + client: &C, epoch: Epoch, validator: &Address, ) -> token::Amount { - unwrap_client_response( - RPC.vp() - .pos() - .validator_stake(client, validator, &Some(epoch)) - .await, - ) + namada::ledger::rpc::get_validator_stake(client, epoch, validator).await } -pub async fn get_delegators_delegation( - client: &HttpClient, +pub async fn get_delegators_delegation< + C: namada::ledger::queries::Client + Sync, +>( + client: &C, address: &Address, ) -> HashSet
{ - unwrap_client_response(RPC.vp().pos().delegations(client, address).await) + namada::ledger::rpc::get_delegators_delegation(client, address).await } -pub async fn get_governance_parameters(client: &HttpClient) -> GovParams { - use namada::types::token::Amount; - let key = gov_storage::get_max_proposal_code_size_key(); - let max_proposal_code_size = query_storage_value::(client, &key) - .await - .expect("Parameter should be definied."); - - let key = gov_storage::get_max_proposal_content_key(); - let max_proposal_content_size = query_storage_value::(client, &key) - .await - .expect("Parameter should be definied."); - - let key = gov_storage::get_min_proposal_fund_key(); - let min_proposal_fund = query_storage_value::(client, &key) - .await - .expect("Parameter should be definied."); - - let key = gov_storage::get_min_proposal_grace_epoch_key(); - let min_proposal_grace_epochs = query_storage_value::(client, &key) - .await - .expect("Parameter should be definied."); - - let key = gov_storage::get_min_proposal_period_key(); - let min_proposal_period = query_storage_value::(client, &key) - .await - .expect("Parameter should be definied."); - - let key = gov_storage::get_max_proposal_period_key(); - let max_proposal_period = query_storage_value::(client, &key) - .await - .expect("Parameter should be definied."); - - GovParams { - min_proposal_fund: u64::from(min_proposal_fund), - max_proposal_code_size, - min_proposal_period, - max_proposal_period, - max_proposal_content_size, - min_proposal_grace_epochs, - } +pub async fn get_governance_parameters< + C: namada::ledger::queries::Client + Sync, +>( + client: &C, +) -> GovParams { + namada::ledger::rpc::get_governance_parameters(client).await } /// 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 { - match ctx.wallet.find_alias(addr) { +fn lookup_alias(wallet: &Wallet, addr: &Address) -> String { + match wallet.find_alias(addr) { Some(alias) => format!("{}", alias), None => format!("{}", addr), } } /// A helper to unwrap client's response. Will shut down process on error. -fn unwrap_client_response(response: Result) -> T { - response.unwrap_or_else(|err| { - eprintln!("Error in the query {}", err); +fn unwrap_client_response( + response: Result, +) -> T { + response.unwrap_or_else(|_err| { + eprintln!("Error in the query"); cli::safe_exit(1) }) } diff --git a/apps/src/lib/client/signing.rs b/apps/src/lib/client/signing.rs index ed7ab484a9..33c1e6c69c 100644 --- a/apps/src/lib/client/signing.rs +++ b/apps/src/lib/client/signing.rs @@ -1,135 +1,46 @@ //! Helpers for making digital signatures using cryptographic keys from the //! wallet. -use borsh::BorshSerialize; +use namada::ledger::rpc::TxBroadcastData; +use namada::ledger::signing::TxSigningKey; +use namada::ledger::tx; +use namada::ledger::wallet::{Wallet, WalletUtils}; use namada::proto::Tx; -use namada::types::address::{Address, ImplicitAddress}; +use namada::types::address::Address; use namada::types::key::*; use namada::types::storage::Epoch; -use namada::types::transaction::{hash_tx, Fee, WrapperTx}; -use super::rpc; -use crate::cli::context::{WalletAddress, WalletKeypair}; -use crate::cli::{self, args, Context}; -use crate::client::tendermint_rpc_types::TxBroadcastData; -use crate::facade::tendermint_config::net::Address as TendermintAddress; -use crate::wallet::Wallet; +use crate::cli::args; /// Find the public key for the given address and try to load the keypair /// for it from the wallet. Panics if the key cannot be found or loaded. -pub async fn find_keypair( - wallet: &mut Wallet, +pub async fn find_keypair< + C: namada::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, addr: &Address, - ledger_address: TendermintAddress, -) -> common::SecretKey { - match addr { - Address::Established(_) => { - println!( - "Looking-up public key of {} from the ledger...", - addr.encode() - ); - let public_key = rpc::get_public_key(addr, ledger_address) - .await - .unwrap_or_else(|| { - eprintln!( - "No public key found for the address {}", - addr.encode() - ); - cli::safe_exit(1); - }); - wallet.find_key_by_pk(&public_key).unwrap_or_else(|err| { - eprintln!( - "Unable to load the keypair from the wallet for public \ - key {}. Failed with: {}", - public_key, err - ); - cli::safe_exit(1) - }) - } - Address::Implicit(ImplicitAddress(pkh)) => { - wallet.find_key_by_pkh(pkh).unwrap_or_else(|err| { - eprintln!( - "Unable to load the keypair from the wallet for the \ - implicit address {}. Failed with: {}", - addr.encode(), - err - ); - cli::safe_exit(1) - }) - } - Address::Internal(_) => { - eprintln!( - "Internal address {} doesn't have any signing keys.", - addr - ); - cli::safe_exit(1) - } - } -} - -/// Carries types that can be directly/indirectly used to sign a transaction. -#[allow(clippy::large_enum_variant)] -#[derive(Clone)] -pub enum TxSigningKey { - // Do not sign any transaction - None, - // Obtain the actual keypair from wallet and use that to sign - WalletKeypair(WalletKeypair), - // Obtain the keypair corresponding to given address from wallet and sign - WalletAddress(WalletAddress), - // Directly use the given secret key to sign transactions - SecretKey(common::SecretKey), +) -> Result { + namada::ledger::signing::find_keypair::(client, wallet, addr, None) + .await } /// Given CLI arguments and some defaults, determine the rightful transaction /// signer. Return the given signing key or public key of the given signer if /// possible. If no explicit signer given, use the `default`. If no `default` /// is given, panics. -pub async fn tx_signer( - ctx: &mut Context, +pub async fn tx_signer< + C: namada::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, args: &args::Tx, - mut default: TxSigningKey, -) -> common::SecretKey { - // Override the default signing key source if possible - if let Some(signing_key) = &args.signing_key { - default = TxSigningKey::WalletKeypair(signing_key.clone()); - } else if let Some(signer) = &args.signer { - default = TxSigningKey::WalletAddress(signer.clone()); - } - // Now actually fetch the signing key and apply it - match default { - TxSigningKey::WalletKeypair(signing_key) => { - ctx.get_cached(&signing_key) - } - TxSigningKey::WalletAddress(signer) => { - let signer = ctx.get(&signer); - let signing_key = find_keypair( - &mut ctx.wallet, - &signer, - args.ledger_address.clone(), - ) - .await; - // Check if the signer is implicit account that needs to reveal its - // PK first - if matches!(signer, Address::Implicit(_)) { - let pk: common::PublicKey = signing_key.ref_to(); - super::tx::reveal_pk_if_needed(ctx, &pk, args).await; - } - signing_key - } - TxSigningKey::SecretKey(signing_key) => { - // Check if the signing key needs to reveal its PK first - let pk: common::PublicKey = signing_key.ref_to(); - super::tx::reveal_pk_if_needed(ctx, &pk, args).await; - signing_key - } - TxSigningKey::None => { - panic!( - "All transactions must be signed; please either specify the \ - key or the address from which to look up the signing key." - ); - } - } + default: TxSigningKey, +) -> Result { + namada::ledger::signing::tx_signer::(client, wallet, args, default) + .await } /// Sign a transaction with a given signing key or public key of a given signer. @@ -140,62 +51,28 @@ pub async fn tx_signer( /// hashes needed for monitoring the tx on chain. /// /// 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, +pub async fn sign_tx< + C: namada::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, tx: Tx, args: &args::Tx, default: TxSigningKey, -) -> (Context, TxBroadcastData) { - let keypair = tx_signer(&mut ctx, args, default).await; - let tx = tx.sign(&keypair); - - let epoch = rpc::query_epoch(args::Query { - ledger_address: args.ledger_address.clone(), - }) - .await; - let broadcast_data = if args.dry_run { - TxBroadcastData::DryRun(tx) - } else { - sign_wrapper(&ctx, args, epoch, tx, &keypair).await - }; - (ctx, broadcast_data) +) -> Result { + namada::ledger::signing::sign_tx::(client, wallet, tx, args, default) + .await } /// Create a wrapper tx from a normal tx. Get the hash of the /// wrapper and its payload which is needed for monitoring its /// progress on chain. pub async fn sign_wrapper( - ctx: &Context, args: &args::Tx, epoch: Epoch, tx: Tx, keypair: &common::SecretKey, ) -> TxBroadcastData { - let tx = { - WrapperTx::new( - Fee { - amount: args.fee_amount, - token: ctx.get(&args.fee_token), - }, - keypair, - epoch, - args.gas_limit.clone(), - tx, - // TODO: Actually use the fetched encryption key - Default::default(), - ) - }; - - // We use this to determine when the wrapper tx makes it on-chain - let wrapper_hash = hash_tx(&tx.try_to_vec().unwrap()).to_string(); - // We use this to determine when the decrypted inner tx makes it - // on-chain - let decrypted_hash = tx.tx_hash.to_string(); - TxBroadcastData::Wrapper { - tx: tx - .sign(keypair) - .expect("Wrapper tx signing keypair should be correct"), - wrapper_hash, - decrypted_hash, - } + namada::ledger::signing::sign_wrapper(args, epoch, tx, keypair).await } diff --git a/apps/src/lib/client/tendermint_rpc_types.rs b/apps/src/lib/client/tendermint_rpc_types.rs deleted file mode 100644 index 537cca243f..0000000000 --- a/apps/src/lib/client/tendermint_rpc_types.rs +++ /dev/null @@ -1,100 +0,0 @@ -use std::convert::TryFrom; - -use namada::ledger::events::Event; -use namada::proto::Tx; -use namada::types::address::Address; -use serde::Serialize; - -use crate::cli::safe_exit; - -/// Data needed for broadcasting a tx and -/// monitoring its progress on chain -/// -/// Txs may be either a dry run or else -/// they should be encrypted and included -/// in a wrapper. -#[derive(Debug, Clone)] -pub enum TxBroadcastData { - DryRun(Tx), - Wrapper { - tx: Tx, - wrapper_hash: String, - decrypted_hash: String, - }, -} - -/// A parsed event from tendermint relating to a transaction -#[derive(Debug, Serialize)] -pub struct TxResponse { - pub info: String, - pub log: String, - pub height: String, - pub hash: String, - pub code: String, - pub gas_used: String, - pub initialized_accounts: Vec
, -} - -impl TryFrom for TxResponse { - type Error = String; - - fn try_from(event: Event) -> Result { - fn missing_field_err(field: &str) -> String { - format!("Field \"{field}\" not present in event") - } - - let hash = event - .get("hash") - .ok_or_else(|| missing_field_err("hash"))? - .clone(); - let info = event - .get("info") - .ok_or_else(|| missing_field_err("info"))? - .clone(); - let log = event - .get("log") - .ok_or_else(|| missing_field_err("log"))? - .clone(); - let height = event - .get("height") - .ok_or_else(|| missing_field_err("height"))? - .clone(); - let code = event - .get("code") - .ok_or_else(|| missing_field_err("code"))? - .clone(); - let gas_used = event - .get("gas_used") - .ok_or_else(|| missing_field_err("gas_used"))? - .clone(); - let initialized_accounts = event - .get("initialized_accounts") - .map(String::as_str) - // TODO: fix finalize block, to return initialized accounts, - // even when we reject a tx? - .map_or(Ok(vec![]), |initialized_accounts| { - serde_json::from_str(initialized_accounts) - .map_err(|err| format!("JSON decode error: {err}")) - })?; - - Ok(TxResponse { - hash, - info, - log, - height, - code, - gas_used, - initialized_accounts, - }) - } -} - -impl TxResponse { - /// Convert an [`Event`] to a [`TxResponse`], or error out. - pub fn from_event(event: Event) -> Self { - event.try_into().unwrap_or_else(|err| { - eprintln!("Error fetching TxResponse: {err}"); - safe_exit(1); - }) - } -} diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index c0cc9d37db..c136cece03 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1,200 +1,80 @@ -use std::borrow::Cow; -use std::collections::hash_map::Entry; -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::HashSet; use std::env; use std::fmt::Debug; use std::fs::{File, OpenOptions}; use std::io::{Read, Write}; -use std::ops::Deref; use std::path::PathBuf; +use async_std::io; use async_std::io::prelude::WriteExt; -use async_std::io::{self}; use borsh::{BorshDeserialize, BorshSerialize}; -use itertools::Either::*; -use masp_primitives::asset_type::AssetType; -use masp_primitives::consensus::{BranchId, TestNetwork}; -use masp_primitives::convert::AllowedConversion; -use masp_primitives::ff::PrimeField; -use masp_primitives::group::cofactor::CofactorGroup; -use masp_primitives::keys::FullViewingKey; -use masp_primitives::legacy::TransparentAddress; -use masp_primitives::merkle_tree::{ - CommitmentTree, IncrementalWitness, MerklePath, -}; -use masp_primitives::note_encryption::*; -use masp_primitives::primitives::{Diversifier, Note, ViewingKey}; -use masp_primitives::sapling::Node; -use masp_primitives::transaction::builder::{self, secp256k1, *}; -use masp_primitives::transaction::components::{Amount, OutPoint, TxOut}; -use masp_primitives::transaction::Transaction; -use masp_primitives::zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}; use masp_proofs::prover::LocalTxProver; -use namada::ibc::applications::ics20_fungible_token_transfer::msgs::transfer::MsgTransfer; -use namada::ibc::signer::Signer; -use namada::ibc::timestamp::Timestamp as IbcTimestamp; -use namada::ibc::tx_msg::Msg; -use namada::ibc::Height as IbcHeight; -use namada::ibc_proto::cosmos::base::v1beta1::Coin; use namada::ledger::governance::storage as gov_storage; -use namada::ledger::masp; -use namada::ledger::pos::{BondId, Bonds, CommissionRates, Unbonds}; +use namada::ledger::masp::{ShieldedContext, ShieldedUtils}; +use namada::ledger::rpc::{TxBroadcastData, TxResponse}; +use namada::ledger::signing::TxSigningKey; +use namada::ledger::wallet::{Wallet, WalletUtils}; +use namada::ledger::{masp, tx}; use namada::proto::Tx; -use namada::types::address::{masp, masp_tx_key, Address}; +use namada::types::address::Address; use namada::types::governance::{ OfflineProposal, OfflineVote, Proposal, ProposalVote, }; use namada::types::key::*; -use namada::types::masp::{PaymentAddress, TransferTarget}; -use namada::types::storage::{ - BlockHeight, Epoch, Key, KeySeg, TxIndex, RESERVED_ADDRESS_PREFIX, -}; -use namada::types::time::DateTimeUtc; -use namada::types::token::{ - Transfer, HEAD_TX_KEY, PIN_KEY_PREFIX, TX_KEY_PREFIX, -}; +use namada::types::storage::Epoch; +use namada::types::token; use namada::types::transaction::governance::{ InitProposalData, VoteProposalData, }; -use namada::types::transaction::{pos, InitAccount, InitValidator, UpdateVp}; -use namada::types::{storage, token}; -use namada::{ledger, vm}; -use rand_core::{CryptoRng, OsRng, RngCore}; +use namada::types::transaction::InitValidator; +use namada::vm; use rust_decimal::Decimal; -use sha2::Digest; -use tokio::time::{Duration, Instant}; use super::rpc; -use super::types::ShieldedTransferContext; use crate::cli::context::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}; -use crate::client::tendermint_rpc_types::{TxBroadcastData, TxResponse}; -use crate::client::types::ParsedTxTransferArgs; -use crate::facade::tendermint_config::net::Address as TendermintAddress; +use crate::client::signing::find_keypair; use crate::facade::tendermint_rpc::endpoint::broadcast::tx_sync::Response; -use crate::facade::tendermint_rpc::error::Error as RpcError; -use crate::facade::tendermint_rpc::{Client, HttpClient}; use crate::node::ledger::tendermint_node; - -const TX_INIT_ACCOUNT_WASM: &str = "tx_init_account.wasm"; -const TX_INIT_VALIDATOR_WASM: &str = "tx_init_validator.wasm"; -const TX_INIT_PROPOSAL: &str = "tx_init_proposal.wasm"; -const TX_VOTE_PROPOSAL: &str = "tx_vote_proposal.wasm"; -const TX_REVEAL_PK: &str = "tx_reveal_pk.wasm"; -const TX_UPDATE_VP_WASM: &str = "tx_update_vp.wasm"; -const TX_TRANSFER_WASM: &str = "tx_transfer.wasm"; -const TX_IBC_WASM: &str = "tx_ibc.wasm"; -const VP_USER_WASM: &str = "vp_user.wasm"; -const TX_BOND_WASM: &str = "tx_bond.wasm"; -const TX_UNBOND_WASM: &str = "tx_unbond.wasm"; -const TX_WITHDRAW_WASM: &str = "tx_withdraw.wasm"; -const TX_CHANGE_COMMISSION_WASM: &str = "tx_change_validator_commission.wasm"; - -/// Timeout for requests to the `/accepted` and `/applied` -/// ABCI query endpoints. -const ENV_VAR_NAMADA_EVENTS_MAX_WAIT_TIME_SECONDS: &str = - "NAMADA_EVENTS_MAX_WAIT_TIME_SECONDS"; - -/// Default timeout in seconds for requests to the `/accepted` -/// 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); - 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, &args.tx, tx, TxSigningKey::None).await; - save_initialized_accounts(ctx, &args.tx, initialized_accounts).await; -} - -pub async fn submit_update_vp(ctx: Context, args: args::TxUpdateVp) { - let addr = ctx.get(&args.addr); - - // Check that the address is established and exists on chain - match &addr { - Address::Established(_) => { - let exists = - rpc::known_address(&addr, args.tx.ledger_address.clone()).await; - if !exists { - eprintln!("The address {} doesn't exist on chain.", addr); - if !args.tx.force { - safe_exit(1) - } - } - } - Address::Implicit(_) => { - eprintln!( - "A validity predicate of an implicit address cannot be \ - directly updated. You can use an established address for \ - this purpose." - ); - if !args.tx.force { - safe_exit(1) - } - } - Address::Internal(_) => { - eprintln!( - "A validity predicate of an internal address cannot be \ - directly updated." - ); - if !args.tx.force { - safe_exit(1) - } - } - } - - let vp_code = 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); - if !args.tx.force { - safe_exit(1) - } - } - - let tx_code = 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, &args.tx, tx, TxSigningKey::WalletAddress(args.addr)).await; -} - -pub async fn submit_init_account(mut ctx: Context, args: args::TxInitAccount) { - let public_key = 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)); - // Validate the VP code - if let Err(err) = vm::validate_untrusted_wasm(&vp_code) { - eprintln!("Validity predicate code validation failed with {}", err); - if !args.tx.force { - safe_exit(1) - } - } - - let tx_code = ctx.read_wasm(TX_INIT_ACCOUNT_WASM); - let data = InitAccount { - public_key, - vp_code, - }; - 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, &args.tx, tx, TxSigningKey::WalletAddress(args.source)) - .await; - save_initialized_accounts(ctx, &args.tx, initialized_accounts).await; -} - -pub async fn submit_init_validator( +use crate::wallet::{gen_validator_keys, read_and_confirm_pwd, CliWalletUtils}; + +pub async fn submit_custom< + C: namada::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + args: args::TxCustom, +) -> Result<(), tx::Error> { + tx::submit_custom::(client, wallet, args).await +} + +pub async fn submit_update_vp< + C: namada::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + args: args::TxUpdateVp, +) -> Result<(), tx::Error> { + tx::submit_update_vp::(client, wallet, args).await +} + +pub async fn submit_init_account< + C: namada::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + args: args::TxInitAccount, +) -> Result<(), tx::Error> { + tx::submit_init_account::(client, wallet, args).await +} + +pub async fn submit_init_validator< + C: namada::ledger::queries::Client + Sync, +>( + client: &C, mut ctx: Context, args::TxInitValidator { tx: tx_args, @@ -207,6 +87,7 @@ pub async fn submit_init_validator( max_commission_rate_change, validator_vp_code_path, unsafe_dont_encrypt, + tx_code_path, }: args::TxInitValidator, ) { let alias = tx_args @@ -217,20 +98,16 @@ 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(|| { + let account_key = account_key.unwrap_or_else(|| { println!("Generating validator account key..."); + let password = read_and_confirm_pwd(unsafe_dont_encrypt); ctx.wallet - .gen_key( - scheme, - Some(validator_key_alias.clone()), - unsafe_dont_encrypt, - ) + .gen_key(scheme, Some(validator_key_alias.clone()), password) .1 .ref_to() }); - let consensus_key = ctx - .get_opt_cached(&consensus_key) + let consensus_key = consensus_key .map(|key| match key { common::SecretKey::Ed25519(_) => key, common::SecretKey::Secp256k1(_) => { @@ -240,24 +117,25 @@ pub async fn submit_init_validator( }) .unwrap_or_else(|| { println!("Generating consensus key..."); + let password = read_and_confirm_pwd(unsafe_dont_encrypt); ctx.wallet .gen_key( // Note that TM only allows ed25519 for consensus key SchemeType::Ed25519, Some(consensus_key_alias.clone()), - unsafe_dont_encrypt, + password, ) .1 }); - let protocol_key = ctx.get_opt_cached(&protocol_key); + let protocol_key = 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(); + gen_validator_keys(&mut ctx.wallet, protocol_key, scheme).unwrap(); let protocol_key = validator_keys.get_protocol_keypair().ref_to(); let dkg_key = validator_keys .dkg_keypair @@ -265,11 +143,9 @@ pub async fn submit_init_validator( .expect("DKG sessions keys should have been created") .public(); - ctx.wallet.save().unwrap_or_else(|err| eprintln!("{}", err)); + crate::wallet::save(&ctx.wallet).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)); + let validator_vp_code = validator_vp_code_path; // Validate the commission rate data if commission_rate > Decimal::ONE || commission_rate < Decimal::ZERO { @@ -302,7 +178,7 @@ pub async fn submit_init_validator( safe_exit(1) } } - let tx_code = ctx.read_wasm(TX_INIT_VALIDATOR_WASM); + let tx_code = tx_code_path; let data = InitValidator { account_key, @@ -315,9 +191,18 @@ 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, &tx_args, tx, TxSigningKey::WalletAddress(source)) - .await; + let initialized_accounts = process_tx::( + client, + &mut ctx.wallet, + &tx_args, + tx, + TxSigningKey::WalletAddress(source), + ) + .await + .unwrap_or_else(|err| { + eprintln!("Processing transaction failed with {}", err); + safe_exit(1) + }); if !tx_args.dry_run { let (validator_address_alias, validator_address) = match &initialized_accounts[..] { @@ -367,7 +252,8 @@ pub async fn submit_init_validator( // add validator address and keys to the wallet ctx.wallet .add_validator_data(validator_address, validator_keys); - ctx.wallet.save().unwrap_or_else(|err| eprintln!("{}", err)); + crate::wallet::save(&ctx.wallet) + .unwrap_or_else(|err| eprintln!("{}", err)); let tendermint_home = ctx.config.ledger.tendermint_dir(); tendermint_node::write_validator_key(&tendermint_home, &consensus_key); @@ -389,149 +275,84 @@ pub async fn submit_init_validator( } } -/// Make a ViewingKey that can view notes encrypted by given ExtendedSpendingKey -pub fn to_viewing_key(esk: &ExtendedSpendingKey) -> FullViewingKey { - ExtendedFullViewingKey::from(esk).fvk +/// Shielded context file name +const FILE_NAME: &str = "shielded.dat"; +const TMP_FILE_NAME: &str = "shielded.tmp"; + +#[derive(Debug, BorshSerialize, BorshDeserialize, Clone)] +pub struct CLIShieldedUtils { + #[borsh_skip] + context_dir: PathBuf, } -/// Generate a valid diversifier, i.e. one that has a diversified base. Return -/// also this diversified base. -pub fn find_valid_diversifier( - rng: &mut R, -) -> (Diversifier, masp_primitives::jubjub::SubgroupPoint) { - let mut diversifier; - let g_d; - // Keep generating random diversifiers until one has a diversified base - loop { - let mut d = [0; 11]; - rng.fill_bytes(&mut d); - diversifier = Diversifier(d); - if let Some(val) = diversifier.g_d() { - g_d = val; - break; +impl CLIShieldedUtils { + /// Initialize a shielded transaction context that identifies notes + /// decryptable by any viewing key in the given set + pub fn new(context_dir: PathBuf) -> masp::ShieldedContext { + // Make sure that MASP parameters are downloaded to enable MASP + // transaction building and verification later on + let params_dir = masp::get_params_dir(); + let spend_path = params_dir.join(masp::SPEND_NAME); + let convert_path = params_dir.join(masp::CONVERT_NAME); + let output_path = params_dir.join(masp::OUTPUT_NAME); + if !(spend_path.exists() + && convert_path.exists() + && output_path.exists()) + { + println!("MASP parameters not present, downloading..."); + masp_proofs::download_parameters() + .expect("MASP parameters not present or downloadable"); + println!("MASP parameter download complete, resuming execution..."); + } + // Finally initialize a shielded context with the supplied directory + let utils = Self { context_dir }; + masp::ShieldedContext { + utils, + ..Default::default() } } - (diversifier, g_d) } -/// Determine if using the current note would actually bring us closer to our -/// target -pub fn is_amount_required(src: Amount, dest: Amount, delta: Amount) -> bool { - if delta > Amount::zero() { - let gap = dest - src; - for (asset_type, value) in gap.components() { - if *value > 0 && delta[asset_type] > 0 { - return true; - } +impl Default for CLIShieldedUtils { + fn default() -> Self { + Self { + context_dir: PathBuf::from(FILE_NAME), } } - false -} - -/// An extension of Option's cloned method for pair types -fn cloned_pair((a, b): (&T, &U)) -> (T, U) { - (a.clone(), b.clone()) -} - -/// Errors that can occur when trying to retrieve pinned transaction -#[derive(PartialEq, Eq)] -pub enum PinnedBalanceError { - /// No transaction has yet been pinned to the given payment address - NoTransactionPinned, - /// The supplied viewing key does not recognize payments to given address - InvalidViewingKey, -} - -/// Represents the amount used of different conversions -pub type Conversions = - HashMap, i64)>; - -/// Represents the changes that were made to a list of transparent accounts -pub type TransferDelta = HashMap>; - -/// Represents the changes that were made to a list of shielded accounts -pub type TransactionDelta = HashMap; - -/// Represents the current state of the shielded pool from the perspective of -/// the chosen viewing keys. -#[derive(BorshSerialize, BorshDeserialize, Debug)] -pub struct ShieldedContext { - /// Location where this shielded context is saved - #[borsh_skip] - context_dir: PathBuf, - /// The last transaction index to be processed in this context - last_txidx: u64, - /// The commitment tree produced by scanning all transactions up to tx_pos - tree: CommitmentTree, - /// Maps viewing keys to applicable note positions - pos_map: HashMap>, - /// Maps a nullifier to the note position to which it applies - nf_map: HashMap<[u8; 32], usize>, - /// Maps note positions to their corresponding notes - note_map: HashMap, - /// Maps note positions to their corresponding memos - memo_map: HashMap, - /// Maps note positions to the diversifier of their payment address - div_map: HashMap, - /// Maps note positions to their witness (used to make merkle paths) - witness_map: HashMap>, - /// Tracks what each transaction does to various account balances - delta_map: BTreeMap< - (BlockHeight, TxIndex), - (Epoch, TransferDelta, TransactionDelta), - >, - /// The set of note positions that have been spent - spents: HashSet, - /// Maps asset types to their decodings - asset_types: HashMap, - /// Maps note positions to their corresponding viewing keys - vk_map: HashMap, } -/// Shielded context file name -const FILE_NAME: &str = "shielded.dat"; -const TMP_FILE_NAME: &str = "shielded.tmp"; +impl masp::ShieldedUtils for CLIShieldedUtils { + type C = tendermint_rpc::HttpClient; -/// Default implementation to ease construction of TxContexts. Derive cannot be -/// used here due to CommitmentTree not implementing Default. -impl Default for ShieldedContext { - fn default() -> ShieldedContext { - ShieldedContext { - context_dir: PathBuf::from(FILE_NAME), - last_txidx: u64::default(), - tree: CommitmentTree::empty(), - pos_map: HashMap::default(), - nf_map: HashMap::default(), - note_map: HashMap::default(), - memo_map: HashMap::default(), - div_map: HashMap::default(), - witness_map: HashMap::default(), - spents: HashSet::default(), - delta_map: BTreeMap::default(), - asset_types: HashMap::default(), - vk_map: HashMap::default(), + fn local_tx_prover(&self) -> LocalTxProver { + if let Ok(params_dir) = env::var(masp::ENV_VAR_MASP_PARAMS_DIR) { + let params_dir = PathBuf::from(params_dir); + let spend_path = params_dir.join(masp::SPEND_NAME); + let convert_path = params_dir.join(masp::CONVERT_NAME); + let output_path = params_dir.join(masp::OUTPUT_NAME); + LocalTxProver::new(&spend_path, &output_path, &convert_path) + } else { + LocalTxProver::with_default_location() + .expect("unable to load MASP Parameters") } } -} -impl ShieldedContext { /// Try to load the last saved shielded context from the given context /// directory. If this fails, then leave the current context unchanged. - pub fn load(&mut self) -> std::io::Result<()> { + fn load(self) -> std::io::Result> { // Try to load shielded context from file let mut ctx_file = File::open(self.context_dir.join(FILE_NAME))?; let mut bytes = Vec::new(); ctx_file.read_to_end(&mut bytes)?; - let mut new_ctx = Self::deserialize(&mut &bytes[..])?; + let mut new_ctx = masp::ShieldedContext::deserialize(&mut &bytes[..])?; // Associate the originating context directory with the // shielded context under construction - new_ctx.context_dir = self.context_dir.clone(); - *self = new_ctx; - Ok(()) + new_ctx.utils = self; + Ok(new_ctx) } /// Save this shielded context into its associated context directory - pub fn save(&self) -> std::io::Result<()> { + fn save(&self, ctx: &masp::ShieldedContext) -> std::io::Result<()> { // TODO: use mktemp crate? let tmp_path = self.context_dir.join(TMP_FILE_NAME); { @@ -545,7 +366,7 @@ impl ShieldedContext { .create_new(true) .open(tmp_path.clone())?; let mut bytes = Vec::new(); - self.serialize(&mut bytes) + ctx.serialize(&mut bytes) .expect("cannot serialize shielded context"); ctx_file.write_all(&bytes[..])?; } @@ -558,1224 +379,44 @@ impl ShieldedContext { std::fs::remove_file(tmp_path)?; Ok(()) } - - /// Merge data from the given shielded context into the current shielded - /// context. It must be the case that the two shielded contexts share the - /// same last transaction ID and share identical commitment trees. - pub fn merge(&mut self, new_ctx: ShieldedContext) { - debug_assert_eq!(self.last_txidx, new_ctx.last_txidx); - // Merge by simply extending maps. Identical keys should contain - // identical values, so overwriting should not be problematic. - self.pos_map.extend(new_ctx.pos_map); - self.nf_map.extend(new_ctx.nf_map); - self.note_map.extend(new_ctx.note_map); - self.memo_map.extend(new_ctx.memo_map); - self.div_map.extend(new_ctx.div_map); - self.witness_map.extend(new_ctx.witness_map); - self.spents.extend(new_ctx.spents); - self.asset_types.extend(new_ctx.asset_types); - self.vk_map.extend(new_ctx.vk_map); - // The deltas are the exception because different keys can reveal - // different parts of the same transaction. Hence each delta needs to be - // merged separately. - for ((height, idx), (ep, ntfer_delta, ntx_delta)) in new_ctx.delta_map { - let (_ep, tfer_delta, tx_delta) = self - .delta_map - .entry((height, idx)) - .or_insert((ep, TransferDelta::new(), TransactionDelta::new())); - tfer_delta.extend(ntfer_delta); - tx_delta.extend(ntx_delta); - } - } - - /// Fetch the current state of the multi-asset shielded pool into a - /// ShieldedContext - pub async fn fetch( - &mut self, - ledger_address: &TendermintAddress, - sks: &[ExtendedSpendingKey], - fvks: &[ViewingKey], - ) { - // First determine which of the keys requested to be fetched are new. - // Necessary because old transactions will need to be scanned for new - // keys. - let mut unknown_keys = Vec::new(); - for esk in sks { - let vk = to_viewing_key(esk).vk; - if !self.pos_map.contains_key(&vk) { - unknown_keys.push(vk); - } - } - for vk in fvks { - if !self.pos_map.contains_key(vk) { - unknown_keys.push(*vk); - } - } - - // If unknown keys are being used, we need to scan older transactions - // for any unspent notes - let (txs, mut tx_iter); - if !unknown_keys.is_empty() { - // Load all transactions accepted until this point - txs = Self::fetch_shielded_transfers(ledger_address, 0).await; - tx_iter = txs.iter(); - // Do this by constructing a shielding context only for unknown keys - let mut tx_ctx = ShieldedContext::new(self.context_dir.clone()); - for vk in unknown_keys { - tx_ctx.pos_map.entry(vk).or_insert_with(HashSet::new); - } - // Update this unknown shielded context until it is level with self - while tx_ctx.last_txidx != self.last_txidx { - if let Some(((height, idx), (epoch, tx))) = tx_iter.next() { - tx_ctx.scan_tx(*height, *idx, *epoch, tx); - } else { - break; - } - } - // Merge the context data originating from the unknown keys into the - // current context - self.merge(tx_ctx); - } else { - // Load only transactions accepted from last_txid until this point - txs = - Self::fetch_shielded_transfers(ledger_address, self.last_txidx) - .await; - tx_iter = txs.iter(); - } - // Now that we possess the unspent notes corresponding to both old and - // new keys up until tx_pos, proceed to scan the new transactions. - for ((height, idx), (epoch, tx)) in &mut tx_iter { - self.scan_tx(*height, *idx, *epoch, tx); - } - } - - /// Initialize a shielded transaction context that identifies notes - /// decryptable by any viewing key in the given set - pub fn new(context_dir: PathBuf) -> ShieldedContext { - // Make sure that MASP parameters are downloaded to enable MASP - // transaction building and verification later on - let params_dir = masp::get_params_dir(); - let spend_path = params_dir.join(masp::SPEND_NAME); - let convert_path = params_dir.join(masp::CONVERT_NAME); - let output_path = params_dir.join(masp::OUTPUT_NAME); - if !(spend_path.exists() - && convert_path.exists() - && output_path.exists()) - { - println!("MASP parameters not present, downloading..."); - masp_proofs::download_parameters() - .expect("MASP parameters not present or downloadable"); - println!("MASP parameter download complete, resuming execution..."); - } - // Finally initialize a shielded context with the supplied directory - Self { - context_dir, - ..Default::default() - } - } - - /// Obtain a chronologically-ordered list of all accepted shielded - /// transactions from the ledger. The ledger conceptually stores - /// transactions as a vector. More concretely, the HEAD_TX_KEY location - /// stores the index of the last accepted transaction and each transaction - /// is stored at a key derived from its index. - pub async fn fetch_shielded_transfers( - ledger_address: &TendermintAddress, - last_txidx: u64, - ) -> BTreeMap<(BlockHeight, TxIndex), (Epoch, Transfer)> { - let client = HttpClient::new(ledger_address.clone()).unwrap(); - // The address of the MASP account - let masp_addr = masp(); - // Construct the key where last transaction pointer is stored - let head_tx_key = Key::from(masp_addr.to_db_key()) - .push(&HEAD_TX_KEY.to_owned()) - .expect("Cannot obtain a storage key"); - // Query for the index of the last accepted transaction - let head_txidx = query_storage_value::(&client, &head_tx_key) - .await - .unwrap_or(0); - let mut shielded_txs = BTreeMap::new(); - // Fetch all the transactions we do not have yet - for i in last_txidx..head_txidx { - // Construct the key for where the current transaction is stored - let current_tx_key = Key::from(masp_addr.to_db_key()) - .push(&(TX_KEY_PREFIX.to_owned() + &i.to_string())) - .expect("Cannot obtain a storage key"); - // Obtain the current transaction - let (tx_epoch, tx_height, tx_index, current_tx) = - query_storage_value::<(Epoch, BlockHeight, TxIndex, Transfer)>( - &client, - ¤t_tx_key, - ) - .await - .unwrap(); - // Collect the current transaction - shielded_txs.insert((tx_height, tx_index), (tx_epoch, current_tx)); - } - shielded_txs - } - - /// Applies the given transaction to the supplied context. More precisely, - /// the shielded transaction's outputs are added to the commitment tree. - /// Newly discovered notes are associated to the supplied viewing keys. Note - /// nullifiers are mapped to their originating notes. Note positions are - /// associated to notes, memos, and diversifiers. And the set of notes that - /// we have spent are updated. The witness map is maintained to make it - /// easier to construct note merkle paths in other code. See - /// https://zips.z.cash/protocol/protocol.pdf#scan - pub fn scan_tx( - &mut self, - height: BlockHeight, - index: TxIndex, - epoch: Epoch, - tx: &Transfer, - ) { - // Ignore purely transparent transactions - let shielded = if let Some(shielded) = &tx.shielded { - shielded - } else { - return; - }; - // For tracking the account changes caused by this Transaction - let mut transaction_delta = TransactionDelta::new(); - // Listen for notes sent to our viewing keys - for so in &shielded.shielded_outputs { - // Create merkle tree leaf node from note commitment - let node = Node::new(so.cmu.to_repr()); - // Update each merkle tree in the witness map with the latest - // addition - for (_, witness) in self.witness_map.iter_mut() { - witness.append(node).expect("note commitment tree is full"); - } - let note_pos = self.tree.size(); - self.tree - .append(node) - .expect("note commitment tree is full"); - // Finally, make it easier to construct merkle paths to this new - // note - let witness = IncrementalWitness::::from_tree(&self.tree); - self.witness_map.insert(note_pos, witness); - // Let's try to see if any of our viewing keys can decrypt latest - // note - for (vk, notes) in self.pos_map.iter_mut() { - let decres = try_sapling_note_decryption::( - 0, - &vk.ivk().0, - &so.ephemeral_key.into_subgroup().unwrap(), - &so.cmu, - &so.enc_ciphertext, - ); - // So this current viewing key does decrypt this current note... - if let Some((note, pa, memo)) = decres { - // Add this note to list of notes decrypted by this viewing - // key - notes.insert(note_pos); - // Compute the nullifier now to quickly recognize when spent - let nf = note.nf(vk, note_pos.try_into().unwrap()); - self.note_map.insert(note_pos, note); - self.memo_map.insert(note_pos, memo); - // The payment address' diversifier is required to spend - // note - self.div_map.insert(note_pos, *pa.diversifier()); - self.nf_map.insert(nf.0, note_pos); - // Note the account changes - let balance = transaction_delta - .entry(*vk) - .or_insert_with(Amount::zero); - *balance += - Amount::from_nonnegative(note.asset_type, note.value) - .expect( - "found note with invalid value or asset type", - ); - self.vk_map.insert(note_pos, *vk); - break; - } - } - } - // Cancel out those of our notes that have been spent - for ss in &shielded.shielded_spends { - // If the shielded spend's nullifier is in our map, then target note - // is rendered unusable - if let Some(note_pos) = self.nf_map.get(&ss.nullifier) { - self.spents.insert(*note_pos); - // Note the account changes - let balance = transaction_delta - .entry(self.vk_map[note_pos]) - .or_insert_with(Amount::zero); - let note = self.note_map[note_pos]; - *balance -= - Amount::from_nonnegative(note.asset_type, note.value) - .expect("found note with invalid value or asset type"); - } - } - // Record the changes to the transparent accounts - let transparent_delta = - Amount::from_nonnegative(tx.token.clone(), u64::from(tx.amount)) - .expect("invalid value for amount"); - let mut transfer_delta = TransferDelta::new(); - transfer_delta - .insert(tx.source.clone(), Amount::zero() - &transparent_delta); - transfer_delta.insert(tx.target.clone(), transparent_delta); - self.delta_map.insert( - (height, index), - (epoch, transfer_delta, transaction_delta), - ); - self.last_txidx += 1; - } - - /// Summarize the effects on shielded and transparent accounts of each - /// Transfer in this context - pub fn get_tx_deltas( - &self, - ) -> &BTreeMap< - (BlockHeight, TxIndex), - (Epoch, TransferDelta, TransactionDelta), - > { - &self.delta_map - } - - /// Compute the total unspent notes associated with the viewing key in the - /// context. If the key is not in the context, then we do not know the - /// balance and hence we return None. - pub fn compute_shielded_balance(&self, vk: &ViewingKey) -> Option { - // Cannot query the balance of a key that's not in the map - if !self.pos_map.contains_key(vk) { - return None; - } - let mut val_acc = Amount::zero(); - // Retrieve the notes that can be spent by this key - if let Some(avail_notes) = self.pos_map.get(vk) { - for note_idx in avail_notes { - // Spent notes cannot contribute a new transaction's pool - if self.spents.contains(note_idx) { - continue; - } - // Get note associated with this ID - let note = self.note_map.get(note_idx).unwrap(); - // Finally add value to multi-asset accumulator - val_acc += - Amount::from_nonnegative(note.asset_type, note.value) - .expect("found note with invalid value or asset type"); - } - } - Some(val_acc) - } - - /// Query the ledger for the decoding of the given asset type and cache it - /// if it is found. - pub async fn decode_asset_type( - &mut self, - client: HttpClient, - asset_type: AssetType, - ) -> Option<(Address, Epoch)> { - // Try to find the decoding in the cache - if let decoded @ Some(_) = self.asset_types.get(&asset_type) { - return decoded.cloned(); - } - // Query for the ID of the last accepted transaction - let (addr, ep, _conv, _path): (Address, _, Amount, MerklePath) = - query_conversion(client, asset_type).await?; - self.asset_types.insert(asset_type, (addr.clone(), ep)); - Some((addr, ep)) - } - - /// Query the ledger for the conversion that is allowed for the given asset - /// type and cache it. - async fn query_allowed_conversion<'a>( - &'a mut self, - client: HttpClient, - asset_type: AssetType, - conversions: &'a mut Conversions, - ) -> Option<&'a mut (AllowedConversion, MerklePath, i64)> { - match conversions.entry(asset_type) { - Entry::Occupied(conv_entry) => Some(conv_entry.into_mut()), - Entry::Vacant(conv_entry) => { - // Query for the ID of the last accepted transaction - let (addr, ep, conv, path): (Address, _, _, _) = - query_conversion(client, asset_type).await?; - self.asset_types.insert(asset_type, (addr, ep)); - // If the conversion is 0, then we just have a pure decoding - if conv == Amount::zero() { - None - } else { - Some(conv_entry.insert((Amount::into(conv), path, 0))) - } - } - } - } - - /// Compute the total unspent notes associated with the viewing key in the - /// context and express that value in terms of the currently timestamped - /// asset types. If the key is not in the context, then we do not know the - /// balance and hence we return None. - pub async fn compute_exchanged_balance( - &mut self, - client: HttpClient, - vk: &ViewingKey, - target_epoch: Epoch, - ) -> Option { - // First get the unexchanged balance - if let Some(balance) = self.compute_shielded_balance(vk) { - // And then exchange balance into current asset types - Some( - self.compute_exchanged_amount( - client, - balance, - target_epoch, - HashMap::new(), - ) - .await - .0, - ) - } else { - None - } - } - - /// Try to convert as much of the given asset type-value pair using the - /// given allowed conversion. usage is incremented by the amount of the - /// conversion used, the conversions are applied to the given input, and - /// the trace amount that could not be converted is moved from input to - /// output. - fn apply_conversion( - conv: AllowedConversion, - asset_type: AssetType, - value: i64, - usage: &mut i64, - input: &mut Amount, - output: &mut Amount, - ) { - // If conversion if possible, accumulate the exchanged amount - let conv: Amount = conv.into(); - // The amount required of current asset to qualify for conversion - let threshold = -conv[&asset_type]; - if threshold == 0 { - eprintln!( - "Asset threshold of selected conversion for asset type {} is \ - 0, this is a bug, please report it.", - asset_type - ); - } - // We should use an amount of the AllowedConversion that almost - // cancels the original amount - let required = value / threshold; - // Forget about the trace amount left over because we cannot - // realize its value - let trace = Amount::from_pair(asset_type, value % threshold).unwrap(); - // Record how much more of the given conversion has been used - *usage += required; - // Apply the conversions to input and move the trace amount to output - *input += conv * required - &trace; - *output += trace; - } - - /// Convert the given amount into the latest asset types whilst making a - /// note of the conversions that were used. Note that this function does - /// not assume that allowed conversions from the ledger are expressed in - /// terms of the latest asset types. - pub async fn compute_exchanged_amount( - &mut self, - client: HttpClient, - mut input: Amount, - target_epoch: Epoch, - mut conversions: Conversions, - ) -> (Amount, Conversions) { - // Where we will store our exchanged value - let mut output = Amount::zero(); - // Repeatedly exchange assets until it is no longer possible - while let Some((asset_type, value)) = - input.components().next().map(cloned_pair) - { - let target_asset_type = self - .decode_asset_type(client.clone(), asset_type) - .await - .map(|(addr, _epoch)| make_asset_type(target_epoch, &addr)) - .unwrap_or(asset_type); - let at_target_asset_type = asset_type == target_asset_type; - if let (Some((conv, _wit, usage)), false) = ( - self.query_allowed_conversion( - client.clone(), - asset_type, - &mut conversions, - ) - .await, - at_target_asset_type, - ) { - println!( - "converting current asset type to latest asset type..." - ); - // Not at the target asset type, not at the latest asset type. - // Apply conversion to get from current asset type to the latest - // asset type. - Self::apply_conversion( - conv.clone(), - asset_type, - value, - usage, - &mut input, - &mut output, - ); - } else if let (Some((conv, _wit, usage)), false) = ( - self.query_allowed_conversion( - client.clone(), - target_asset_type, - &mut conversions, - ) - .await, - at_target_asset_type, - ) { - println!( - "converting latest asset type to target asset type..." - ); - // Not at the target asset type, yes at the latest asset type. - // Apply inverse conversion to get from latest asset type to - // the target asset type. - Self::apply_conversion( - conv.clone(), - asset_type, - value, - usage, - &mut input, - &mut output, - ); - } else { - // At the target asset type. Then move component over to output. - let comp = input.project(asset_type); - output += ∁ - // Strike from input to avoid repeating computation - input -= comp; - } - } - (output, conversions) - } - - /// Collect enough unspent notes in this context to exceed the given amount - /// of the specified asset type. Return the total value accumulated plus - /// notes and the corresponding diversifiers/merkle paths that were used to - /// achieve the total value. - pub async fn collect_unspent_notes( - &mut self, - ledger_address: TendermintAddress, - vk: &ViewingKey, - target: Amount, - target_epoch: Epoch, - ) -> ( - Amount, - Vec<(Diversifier, Note, MerklePath)>, - Conversions, - ) { - // Establish connection with which to do exchange rate queries - let client = HttpClient::new(ledger_address.clone()).unwrap(); - let mut conversions = HashMap::new(); - let mut val_acc = Amount::zero(); - let mut notes = Vec::new(); - // Retrieve the notes that can be spent by this key - if let Some(avail_notes) = self.pos_map.get(vk).cloned() { - for note_idx in &avail_notes { - // No more transaction inputs are required once we have met - // the target amount - if val_acc >= target { - break; - } - // Spent notes cannot contribute a new transaction's pool - if self.spents.contains(note_idx) { - continue; - } - // Get note, merkle path, diversifier associated with this ID - let note = *self.note_map.get(note_idx).unwrap(); - - // The amount contributed by this note before conversion - let pre_contr = Amount::from_pair(note.asset_type, note.value) - .expect("received note has invalid value or asset type"); - let (contr, proposed_convs) = self - .compute_exchanged_amount( - client.clone(), - pre_contr, - target_epoch, - conversions.clone(), - ) - .await; - - // Use this note only if it brings us closer to our target - if is_amount_required( - val_acc.clone(), - target.clone(), - contr.clone(), - ) { - // Be sure to record the conversions used in computing - // accumulated value - val_acc += contr; - // Commit the conversions that were used to exchange - conversions = proposed_convs; - let merkle_path = - self.witness_map.get(note_idx).unwrap().path().unwrap(); - let diversifier = self.div_map.get(note_idx).unwrap(); - // Commit this note to our transaction - notes.push((*diversifier, note, merkle_path)); - } - } - } - (val_acc, notes, conversions) - } - - /// Compute the combined value of the output notes of the transaction pinned - /// at the given payment address. This computation uses the supplied viewing - /// keys to try to decrypt the output notes. If no transaction is pinned at - /// the given payment address fails with - /// `PinnedBalanceError::NoTransactionPinned`. - pub async fn compute_pinned_balance( - ledger_address: &TendermintAddress, - owner: PaymentAddress, - viewing_key: &ViewingKey, - ) -> Result<(Amount, Epoch), PinnedBalanceError> { - // Check that the supplied viewing key corresponds to given payment - // address - let counter_owner = viewing_key.to_payment_address( - *masp_primitives::primitives::PaymentAddress::diversifier( - &owner.into(), - ), - ); - match counter_owner { - Some(counter_owner) if counter_owner == owner.into() => {} - _ => return Err(PinnedBalanceError::InvalidViewingKey), - } - let client = HttpClient::new(ledger_address.clone()).unwrap(); - // The address of the MASP account - let masp_addr = masp(); - // Construct the key for where the transaction ID would be stored - let pin_key = Key::from(masp_addr.to_db_key()) - .push(&(PIN_KEY_PREFIX.to_owned() + &owner.hash())) - .expect("Cannot obtain a storage key"); - // Obtain the transaction pointer at the key - let txidx = query_storage_value::(&client, &pin_key) - .await - .ok_or(PinnedBalanceError::NoTransactionPinned)?; - // Construct the key for where the pinned transaction is stored - let tx_key = Key::from(masp_addr.to_db_key()) - .push(&(TX_KEY_PREFIX.to_owned() + &txidx.to_string())) - .expect("Cannot obtain a storage key"); - // Obtain the pointed to transaction - let (tx_epoch, _tx_height, _tx_index, tx) = - query_storage_value::<(Epoch, BlockHeight, TxIndex, Transfer)>( - &client, &tx_key, - ) - .await - .expect("Ill-formed epoch, transaction pair"); - // Accumulate the combined output note value into this Amount - let mut val_acc = Amount::zero(); - let tx = tx - .shielded - .expect("Pinned Transfers should have shielded part"); - for so in &tx.shielded_outputs { - // Let's try to see if our viewing key can decrypt current note - let decres = try_sapling_note_decryption::( - 0, - &viewing_key.ivk().0, - &so.ephemeral_key.into_subgroup().unwrap(), - &so.cmu, - &so.enc_ciphertext, - ); - match decres { - // So the given viewing key does decrypt this current note... - Some((note, pa, _memo)) if pa == owner.into() => { - val_acc += - Amount::from_nonnegative(note.asset_type, note.value) - .expect( - "found note with invalid value or asset type", - ); - break; - } - _ => {} - } - } - Ok((val_acc, tx_epoch)) - } - - /// Compute the combined value of the output notes of the pinned transaction - /// at the given payment address if there's any. The asset types may be from - /// the epoch of the transaction or even before, so exchange all these - /// amounts to the epoch of the transaction in order to get the value that - /// would have been displayed in the epoch of the transaction. - pub async fn compute_exchanged_pinned_balance( - &mut self, - ledger_address: &TendermintAddress, - owner: PaymentAddress, - viewing_key: &ViewingKey, - ) -> Result<(Amount, Epoch), PinnedBalanceError> { - // Obtain the balance that will be exchanged - let (amt, ep) = - Self::compute_pinned_balance(ledger_address, owner, viewing_key) - .await?; - // Establish connection with which to do exchange rate queries - let client = HttpClient::new(ledger_address.clone()).unwrap(); - // Finally, exchange the balance to the transaction's epoch - Ok(( - self.compute_exchanged_amount(client, amt, ep, HashMap::new()) - .await - .0, - ep, - )) - } - - /// Convert an amount whose units are AssetTypes to one whose units are - /// Addresses that they decode to. All asset types not corresponding to - /// the given epoch are ignored. - pub async fn decode_amount( - &mut self, - client: HttpClient, - amt: Amount, - target_epoch: Epoch, - ) -> Amount
{ - let mut res = Amount::zero(); - for (asset_type, val) in amt.components() { - // Decode the asset type - let decoded = - self.decode_asset_type(client.clone(), *asset_type).await; - // Only assets with the target timestamp count - match decoded { - Some((addr, epoch)) if epoch == target_epoch => { - res += &Amount::from_pair(addr, *val).unwrap() - } - _ => {} - } - } - res - } - - /// Convert an amount whose units are AssetTypes to one whose units are - /// Addresses that they decode to. - pub async fn decode_all_amounts( - &mut self, - client: HttpClient, - amt: Amount, - ) -> Amount<(Address, Epoch)> { - let mut res = Amount::zero(); - for (asset_type, val) in amt.components() { - // Decode the asset type - let decoded = - self.decode_asset_type(client.clone(), *asset_type).await; - // Only assets with the target timestamp count - if let Some((addr, epoch)) = decoded { - res += &Amount::from_pair((addr, epoch), *val).unwrap() - } - } - res - } } -/// Make asset type corresponding to given address and epoch -fn make_asset_type(epoch: Epoch, token: &Address) -> AssetType { - // Typestamp the chosen token with the current epoch - let token_bytes = (token, epoch.0) - .try_to_vec() - .expect("token should serialize"); - // Generate the unique asset identifier from the unique token address - AssetType::new(token_bytes.as_ref()).expect("unable to create asset type") -} - -/// Convert Namada amount and token type to MASP equivalents -fn convert_amount( - epoch: Epoch, - token: &Address, - val: token::Amount, -) -> (AssetType, Amount) { - let asset_type = make_asset_type(epoch, token); - // Combine the value and unit into one amount - let amount = Amount::from_nonnegative(asset_type, u64::from(val)) - .expect("invalid value for amount"); - (asset_type, amount) -} - -/// Make shielded components to embed within a Transfer object. If no shielded -/// payment address nor spending key is specified, then no shielded components -/// are produced. Otherwise a transaction containing nullifiers and/or note -/// commitments are produced. Dummy transparent UTXOs are sometimes used to make -/// transactions balanced, but it is understood that transparent account changes -/// are effected only by the amounts and signatures specified by the containing -/// Transfer object. -async fn gen_shielded_transfer( - ctx: &mut C, - args: &ParsedTxTransferArgs, - shielded_gas: bool, -) -> Result, builder::Error> -where - C: ShieldedTransferContext, -{ - let spending_key = args.source.spending_key().map(|x| x.into()); - let payment_address = args.target.payment_address(); - // Determine epoch in which to submit potential shielded transaction - let epoch = ctx.query_epoch(args.tx.ledger_address.clone()).await; - // Context required for storing which notes are in the source's possesion - let consensus_branch_id = BranchId::Sapling; - let amt: u64 = args.amount.into(); - let memo: Option = None; - - // Now we build up the transaction within this object - let mut builder = Builder::::new(0u32); - // Convert transaction amount into MASP types - let (asset_type, amount) = convert_amount(epoch, &args.token, args.amount); - - // Transactions with transparent input and shielded output - // may be affected if constructed close to epoch boundary - let mut epoch_sensitive: bool = false; - // If there are shielded inputs - if let Some(sk) = spending_key { - // Transaction fees need to match the amount in the wrapper Transfer - // when MASP source is used - let (_, fee) = - convert_amount(epoch, &args.tx.fee_token, args.tx.fee_amount); - builder.set_fee(fee.clone())?; - // If the gas is coming from the shielded pool, then our shielded inputs - // must also cover the gas fee - let required_amt = if shielded_gas { amount + fee } else { amount }; - // Locate unspent notes that can help us meet the transaction amount - let (_, unspent_notes, used_convs) = ctx - .collect_unspent_notes( - args.tx.ledger_address.clone(), - &to_viewing_key(&sk).vk, - required_amt, - epoch, - ) - .await; - // Commit the notes found to our transaction - for (diversifier, note, merkle_path) in unspent_notes { - builder.add_sapling_spend(sk, diversifier, note, merkle_path)?; - } - // Commit the conversion notes used during summation - for (conv, wit, value) in used_convs.values() { - if *value > 0 { - builder.add_convert( - conv.clone(), - *value as u64, - wit.clone(), - )?; - } - } - } else { - // No transfer fees come from the shielded transaction for non-MASP - // sources - builder.set_fee(Amount::zero())?; - // We add a dummy UTXO to our transaction, but only the source of the - // parent Transfer object is used to validate fund availability - let secp_sk = - secp256k1::SecretKey::from_slice(&[0xcd; 32]).expect("secret key"); - let secp_ctx = secp256k1::Secp256k1::::gen_new(); - let secp_pk = - secp256k1::PublicKey::from_secret_key(&secp_ctx, &secp_sk) - .serialize(); - let hash = - ripemd160::Ripemd160::digest(&sha2::Sha256::digest(&secp_pk)); - let script = TransparentAddress::PublicKey(hash.into()).script(); - epoch_sensitive = true; - builder.add_transparent_input( - secp_sk, - OutPoint::new([0u8; 32], 0), - TxOut { - asset_type, - value: amt, - script_pubkey: script, - }, - )?; - } - // Now handle the outputs of this transaction - // If there is a shielded output - if let Some(pa) = payment_address { - let ovk_opt = spending_key.map(|x| x.expsk.ovk); - builder.add_sapling_output( - ovk_opt, - pa.into(), - asset_type, - amt, - memo.clone(), - )?; - } else { - epoch_sensitive = false; - // Embed the transparent target address into the shielded transaction so - // that it can be signed - let target_enc = args - .target - .address() - .expect("target address should be transparent") - .try_to_vec() - .expect("target address encoding"); - let hash = ripemd160::Ripemd160::digest(&sha2::Sha256::digest( - target_enc.as_ref(), - )); - builder.add_transparent_output( - &TransparentAddress::PublicKey(hash.into()), - asset_type, - amt, - )?; - } - let prover = if let Ok(params_dir) = env::var(masp::ENV_VAR_MASP_PARAMS_DIR) - { - let params_dir = PathBuf::from(params_dir); - let spend_path = params_dir.join(masp::SPEND_NAME); - let convert_path = params_dir.join(masp::CONVERT_NAME); - let output_path = params_dir.join(masp::OUTPUT_NAME); - LocalTxProver::new(&spend_path, &output_path, &convert_path) - } else { - LocalTxProver::with_default_location() - .expect("unable to load MASP Parameters") - }; - // Build and return the constructed transaction - let mut tx = builder.build(consensus_branch_id, &prover); - - if epoch_sensitive { - let new_epoch = ctx.query_epoch(args.tx.ledger_address.clone()).await; - - // If epoch has changed, recalculate shielded outputs to match new epoch - if new_epoch != epoch { - // Hack: build new shielded transfer with updated outputs - let mut replay_builder = Builder::::new(0u32); - replay_builder.set_fee(Amount::zero())?; - let ovk_opt = spending_key.map(|x| x.expsk.ovk); - let (new_asset_type, _) = - convert_amount(new_epoch, &args.token, args.amount); - replay_builder.add_sapling_output( - ovk_opt, - payment_address.unwrap().into(), - new_asset_type, - amt, - memo, - )?; - - let secp_sk = secp256k1::SecretKey::from_slice(&[0xcd; 32]) - .expect("secret key"); - let secp_ctx = - secp256k1::Secp256k1::::gen_new(); - let secp_pk = - secp256k1::PublicKey::from_secret_key(&secp_ctx, &secp_sk) - .serialize(); - let hash = - ripemd160::Ripemd160::digest(&sha2::Sha256::digest(&secp_pk)); - let script = TransparentAddress::PublicKey(hash.into()).script(); - replay_builder.add_transparent_input( - secp_sk, - OutPoint::new([0u8; 32], 0), - TxOut { - asset_type: new_asset_type, - value: amt, - script_pubkey: script, - }, - )?; - - let (replay_tx, _) = - replay_builder.build(consensus_branch_id, &prover)?; - tx = tx.map(|(t, tm)| { - let mut temp = t.deref().clone(); - temp.shielded_outputs = replay_tx.shielded_outputs.clone(); - temp.value_balance = temp.value_balance.reject(asset_type) - - Amount::from_pair(new_asset_type, amt).unwrap(); - (temp.freeze().unwrap(), tm) - }); - } - } - - tx.map(Some) +pub async fn submit_transfer< + C: namada::ledger::queries::Client + Sync, + V: WalletUtils, + U: ShieldedUtils, +>( + client: &C, + wallet: &mut Wallet, + shielded: &mut ShieldedContext, + args: args::TxTransfer, +) -> Result<(), tx::Error> { + tx::submit_transfer::(client, wallet, shielded, args).await } -pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { - let parsed_args = args.parse_from_context(&mut ctx); - let source = parsed_args.source.effective_address(); - let target = parsed_args.target.effective_address(); - // Check that the source address exists on chain - let source_exists = - rpc::known_address(&source, args.tx.ledger_address.clone()).await; - if !source_exists { - eprintln!("The source address {} doesn't exist on chain.", source); - if !args.tx.force { - safe_exit(1) - } - } - // Check that the target address exists on chain - let target_exists = - rpc::known_address(&target, args.tx.ledger_address.clone()).await; - if !target_exists { - eprintln!("The target address {} doesn't exist on chain.", target); - if !args.tx.force { - safe_exit(1) - } - } - // Check that the token address exists on chain - let token_exists = - rpc::known_address(&parsed_args.token, args.tx.ledger_address.clone()) - .await; - if !token_exists { - eprintln!( - "The token address {} doesn't exist on chain.", - parsed_args.token - ); - if !args.tx.force { - safe_exit(1) - } - } - // Check source balance - let (sub_prefix, balance_key) = match args.sub_prefix { - Some(sub_prefix) => { - let sub_prefix = storage::Key::parse(sub_prefix).unwrap(); - let prefix = token::multitoken_balance_prefix( - &parsed_args.token, - &sub_prefix, - ); - ( - Some(sub_prefix), - token::multitoken_balance_key(&prefix, &source), - ) - } - None => (None, token::balance_key(&parsed_args.token, &source)), - }; - let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); - match rpc::query_storage_value::(&client, &balance_key).await - { - Some(balance) => { - if balance < args.amount { - eprintln!( - "The balance of the source {} of token {} is lower than \ - the amount to be transferred. Amount to transfer is {} \ - and the balance is {}.", - source, parsed_args.token, args.amount, balance - ); - if !args.tx.force { - safe_exit(1) - } - } - } - None => { - eprintln!( - "No balance found for the source {} of token {}", - source, parsed_args.token - ); - if !args.tx.force { - safe_exit(1) - } - } - }; - - let tx_code = ctx.read_wasm(TX_TRANSFER_WASM); - let masp_addr = masp(); - // For MASP sources, use a special sentinel key recognized by VPs as default - // signer. Also, if the transaction is shielded, redact the amount and token - // types by setting the transparent value to 0 and token type to a constant. - // This has no side-effect because transaction is to self. - let (default_signer, amount, token) = - if source == masp_addr && target == masp_addr { - // TODO Refactor me, we shouldn't rely on any specific token here. - ( - TxSigningKey::SecretKey(masp_tx_key()), - 0.into(), - ctx.native_token.clone(), - ) - } else if source == masp_addr { - ( - TxSigningKey::SecretKey(masp_tx_key()), - args.amount, - parsed_args.token.clone(), - ) - } else { - ( - TxSigningKey::WalletAddress(args.source.to_address()), - args.amount, - parsed_args.token.clone(), - ) - }; - // 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()) - .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) { - TransferTarget::PaymentAddress(pa) if pa.is_pinned() => Some(pa.hash()), - _ => None, - }; - - let transfer = token::Transfer { - source, - target, - token, - sub_prefix, - amount, - key, - shielded: { - let spending_key = parsed_args.source.spending_key(); - let payment_address = parsed_args.target.payment_address(); - // No shielded components are needed when neither source nor - // destination are shielded - if spending_key.is_none() && payment_address.is_none() { - None - } else { - // We want to fund our transaction solely from supplied spending - // key - let spending_key = spending_key.map(|x| x.into()); - 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 - .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; - match stx_result { - Ok(stx) => stx.map(|x| x.0), - Err(builder::Error::ChangeIsNegative(_)) => { - eprintln!( - "The balance of the source {} is lower than the \ - amount to be transferred and fees. Amount to \ - transfer is {} {} and fees are {} {}.", - parsed_args.source, - args.amount, - parsed_args.token, - args.tx.fee_amount, - parsed_args.tx.fee_token, - ); - safe_exit(1) - } - Err(err) => panic!("{}", err), - } - } - }, - }; - tracing::debug!("Transfer data {:?}", transfer); - let data = transfer - .try_to_vec() - .expect("Encoding tx data shouldn't fail"); - - let tx = Tx::new(tx_code, Some(data)); - let signing_address = TxSigningKey::WalletAddress(args.source.to_address()); - process_tx(ctx, &args.tx, tx, signing_address).await; -} - -pub async fn submit_ibc_transfer(ctx: Context, args: args::TxIbcTransfer) { - let source = 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; - if !source_exists { - eprintln!("The source address {} doesn't exist on chain.", source); - if !args.tx.force { - safe_exit(1) - } - } - - // We cannot check the receiver - - let token = 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; - if !token_exists { - eprintln!("The token address {} doesn't exist on chain.", token); - if !args.tx.force { - safe_exit(1) - } - } - // Check source balance - let (sub_prefix, balance_key) = match args.sub_prefix { - Some(sub_prefix) => { - let sub_prefix = storage::Key::parse(sub_prefix).unwrap(); - let prefix = token::multitoken_balance_prefix(&token, &sub_prefix); - ( - Some(sub_prefix), - token::multitoken_balance_key(&prefix, &source), - ) - } - None => (None, token::balance_key(&token, &source)), - }; - let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); - match rpc::query_storage_value::(&client, &balance_key).await - { - Some(balance) => { - if balance < args.amount { - eprintln!( - "The balance of the source {} of token {} is lower than \ - the amount to be transferred. Amount to transfer is {} \ - and the balance is {}.", - source, token, args.amount, balance - ); - if !args.tx.force { - safe_exit(1) - } - } - } - None => { - eprintln!( - "No balance found for the source {} of token {}", - source, token - ); - if !args.tx.force { - safe_exit(1) - } - } - } - let tx_code = ctx.read_wasm(TX_IBC_WASM); - - let denom = match sub_prefix { - // To parse IbcToken address, remove the address prefix - Some(sp) => sp.to_string().replace(RESERVED_ADDRESS_PREFIX, ""), - None => token.to_string(), - }; - let token = Some(Coin { - denom, - amount: args.amount.to_string(), - }); - - // this height should be that of the destination chain, not this chain - let timeout_height = match args.timeout_height { - Some(h) => IbcHeight::new(0, h), - None => IbcHeight::zero(), - }; - - let now: namada::tendermint::Time = DateTimeUtc::now().try_into().unwrap(); - let now: IbcTimestamp = now.into(); - let timeout_timestamp = if let Some(offset) = args.timeout_sec_offset { - (now + Duration::new(offset, 0)).unwrap() - } else if timeout_height.is_zero() { - // we cannot set 0 to both the height and the timestamp - (now + Duration::new(3600, 0)).unwrap() - } else { - IbcTimestamp::none() - }; - - let msg = MsgTransfer { - source_port: args.port_id, - source_channel: args.channel_id, - token, - sender: Signer::new(source.to_string()), - receiver: Signer::new(args.receiver), - timeout_height, - timeout_timestamp, - }; - tracing::debug!("IBC transfer message {:?}", msg); - let any_msg = msg.to_any(); - let mut data = vec![]; - prost::Message::encode(&any_msg, &mut data) - .expect("Encoding tx data shouldn't fail"); - - let tx = Tx::new(tx_code, Some(data)); - process_tx(ctx, &args.tx, tx, TxSigningKey::WalletAddress(args.source)) - .await; +pub async fn submit_ibc_transfer< + C: namada::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + args: args::TxIbcTransfer, +) -> Result<(), tx::Error> { + tx::submit_ibc_transfer::(client, wallet, args).await } -pub async fn submit_init_proposal(mut ctx: Context, args: args::InitProposal) { +pub async fn submit_init_proposal( + client: &C, + mut ctx: Context, + args: args::InitProposal, +) -> Result<(), tx::Error> { 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"); - let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); - let signer = WalletAddress::new(proposal.clone().author.to_string()); - let governance_parameters = rpc::get_governance_parameters(&client).await; - let current_epoch = rpc::query_epoch(args::Query { - ledger_address: args.tx.ledger_address.clone(), - }) - .await; + let governance_parameters = rpc::get_governance_parameters(client).await; + let current_epoch = rpc::query_epoch(client).await; if proposal.voting_start_epoch <= current_epoch || proposal.voting_start_epoch.0 @@ -1833,12 +474,9 @@ pub async fn submit_init_proposal(mut ctx: Context, args: args::InitProposal) { if args.offline { let signer = ctx.get(&signer); - let signing_key = find_keypair( - &mut ctx.wallet, - &signer, - args.tx.ledger_address.clone(), - ) - .await; + let signing_key = + find_keypair::(client, &mut ctx.wallet, &signer) + .await?; let offline_proposal = OfflineProposal::new(proposal, signer, &signing_key); let proposal_filename = args @@ -1859,7 +497,9 @@ pub async fn submit_init_proposal(mut ctx: Context, args: args::InitProposal) { safe_exit(1) } } + Ok(()) } else { + let signer = ctx.get(&signer); let tx_data: Result = proposal.clone().try_into(); let init_proposal_data = if let Ok(data) = tx_data { data @@ -1869,8 +509,8 @@ pub async fn submit_init_proposal(mut ctx: Context, args: args::InitProposal) { }; let balance = rpc::get_token_balance( - &client, - &ctx.native_token, + client, + &args.native_token, &proposal.author, ) .await @@ -1895,15 +535,29 @@ 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 = args.tx_code_path; let tx = Tx::new(tx_code, Some(data)); - process_tx(ctx, &args.tx, tx, TxSigningKey::WalletAddress(signer)) - .await; + process_tx::( + client, + &mut ctx.wallet, + &args.tx, + tx, + TxSigningKey::WalletAddress(signer), + ) + .await?; + Ok(()) } } -pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { +pub async fn submit_vote_proposal< + C: namada::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + args: args::VoteProposal, +) -> Result<(), tx::Error> { let signer = if let Some(addr) = &args.tx.signer { addr } else { @@ -1912,30 +566,23 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { }; if args.offline { - let signer = ctx.get(signer); - let proposal_file_path = - args.proposal_data.expect("Proposal file should exist."); + let signer = signer; + let proposal_file_path = args.proposal_data.ok_or(tx::Error::Other( + "Proposal file should exist.".to_string(), + ))?; let file = File::open(&proposal_file_path).expect("File must exist."); let proposal: OfflineProposal = serde_json::from_reader(file).expect("JSON was not well-formatted"); - let public_key = rpc::get_public_key( - &proposal.address, - args.tx.ledger_address.clone(), - ) - .await - .expect("Public key should exist."); + let public_key = rpc::get_public_key(client, &proposal.address) + .await + .expect("Public key should exist."); if !proposal.check_signature(&public_key) { eprintln!("Proposal signature mismatch!"); safe_exit(1) } - let signing_key = find_keypair( - &mut ctx.wallet, - &signer, - args.tx.ledger_address.clone(), - ) - .await; + let signing_key = find_keypair::(client, wallet, signer).await?; let offline_vote = OfflineVote::new( &proposal, args.vote, @@ -1954,6 +601,7 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { "Proposal vote created: {}.", proposal_vote_filename.to_string_lossy() ); + Ok(()) } Err(e) => { eprintln!("Error while creating proposal vote file: {}.", e); @@ -1961,18 +609,14 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { } } } else { - let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); - let current_epoch = rpc::query_epoch(args::Query { - ledger_address: args.tx.ledger_address.clone(), - }) - .await; + let current_epoch = rpc::query_epoch(client).await; - let voter_address = ctx.get(signer); + let voter_address = signer.clone(); let proposal_id = args.proposal_id.unwrap(); let proposal_start_epoch_key = gov_storage::get_voting_start_epoch_key(proposal_id); - let proposal_start_epoch = rpc::query_storage_value::( - &client, + let proposal_start_epoch = rpc::query_storage_value::( + client, &proposal_start_epoch_key, ) .await; @@ -1991,7 +635,7 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { } } let mut delegations = - rpc::get_delegators_delegation(&client, &voter_address) + rpc::get_delegators_delegation(client, &voter_address) .await; // Optimize by quering if a vote from a validator @@ -2001,16 +645,10 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { // validator changing his vote and, effectively, invalidating // the delgator's vote if !args.tx.force - && is_safe_voting_window( - args.tx.ledger_address.clone(), - &client, - proposal_id, - epoch, - ) - .await + && is_safe_voting_window(client, proposal_id, epoch).await? { delegations = filter_delegations( - &client, + client, delegations, proposal_id, &args.vote, @@ -2028,174 +666,87 @@ 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 = args.tx_code_path; let tx = Tx::new(tx_code, Some(data)); - process_tx( - ctx, + process_tx::( + client, + wallet, &args.tx, tx, TxSigningKey::WalletAddress(signer.clone()), ) - .await; + .await?; + Ok(()) } None => { eprintln!( "Proposal start epoch for proposal id {} is not definied.", proposal_id ); - if !args.tx.force { - safe_exit(1) - } + if !args.tx.force { safe_exit(1) } else { Ok(()) } } } } } -pub async fn submit_reveal_pk(mut ctx: Context, args: args::RevealPk) { - 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 addr: Address = (&public_key).into(); - println!("PK for {addr} is already revealed, nothing to do."); - } +pub async fn submit_reveal_pk< + C: namada::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + args: args::RevealPk, +) -> Result<(), tx::Error> { + tx::submit_reveal_pk::(client, wallet, args).await } -pub async fn reveal_pk_if_needed( - ctx: &mut Context, +pub async fn reveal_pk_if_needed< + C: namada::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, public_key: &common::PublicKey, args: &args::Tx, -) -> bool { - let addr: Address = public_key.into(); - // Check if PK revealed - if args.force || !has_revealed_pk(&addr, args.ledger_address.clone()).await - { - // If not, submit it - submit_reveal_pk_aux(ctx, public_key, args).await; - true - } else { - false - } +) -> Result { + tx::reveal_pk_if_needed::(client, wallet, public_key, args).await } -pub async fn has_revealed_pk( +pub async fn has_revealed_pk( + client: &C, addr: &Address, - ledger_address: TendermintAddress, ) -> bool { - rpc::get_public_key(addr, ledger_address).await.is_some() + tx::has_revealed_pk(client, addr).await } -pub async fn submit_reveal_pk_aux( - ctx: &mut Context, +pub async fn submit_reveal_pk_aux< + C: namada::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, public_key: &common::PublicKey, args: &args::Tx, -) { - let addr: Address = public_key.into(); - println!("Submitting a tx to reveal the public key for address {addr}..."); - let tx_data = public_key - .try_to_vec() - .expect("Encoding a public key shouldn't fail"); - let tx_code = ctx.read_wasm(TX_REVEAL_PK); - let tx = Tx::new(tx_code, Some(tx_data)); - - // submit_tx without signing the inner tx - let keypair = if let Some(signing_key) = &args.signing_key { - ctx.get_cached(signing_key) - } else if let Some(signer) = args.signer.as_ref() { - let signer = ctx.get(signer); - find_keypair(&mut ctx.wallet, &signer, args.ledger_address.clone()) - .await - } else { - find_keypair(&mut ctx.wallet, &addr, args.ledger_address.clone()).await - }; - let epoch = rpc::query_epoch(args::Query { - ledger_address: args.ledger_address.clone(), - }) - .await; - let to_broadcast = if args.dry_run { - TxBroadcastData::DryRun(tx) - } else { - super::signing::sign_wrapper(ctx, args, epoch, tx, &keypair).await - }; - - if args.dry_run { - if let TxBroadcastData::DryRun(tx) = to_broadcast { - rpc::dry_run_tx(&args.ledger_address, tx.to_bytes()).await; - } else { - panic!( - "Expected a dry-run transaction, received a wrapper \ - transaction instead" - ); - } - } else { - // Either broadcast or submit transaction and collect result into - // sum type - let result = if args.broadcast_only { - Left(broadcast_tx(args.ledger_address.clone(), &to_broadcast).await) - } else { - Right(submit_tx(args.ledger_address.clone(), to_broadcast).await) - }; - // Return result based on executed operation, otherwise deal with - // the encountered errors uniformly - match result { - Right(Err(err)) => { - eprintln!( - "Encountered error while broadcasting transaction: {}", - err - ); - safe_exit(1) - } - Left(Err(err)) => { - eprintln!( - "Encountered error while broadcasting transaction: {}", - err - ); - safe_exit(1) - } - _ => {} - } - } +) -> Result<(), tx::Error> { + tx::submit_reveal_pk_aux::(client, wallet, public_key, args).await } /// Check if current epoch is in the last third of the voting period of the /// proposal. This ensures that it is safe to optimize the vote writing to /// storage. -async fn is_safe_voting_window( - ledger_address: TendermintAddress, - client: &HttpClient, +async fn is_safe_voting_window( + client: &C, proposal_id: u64, proposal_start_epoch: Epoch, -) -> bool { - let current_epoch = rpc::query_epoch(args::Query { ledger_address }).await; - - let proposal_end_epoch_key = - gov_storage::get_voting_end_epoch_key(proposal_id); - let proposal_end_epoch = - rpc::query_storage_value::(client, &proposal_end_epoch_key) - .await; - - match proposal_end_epoch { - Some(proposal_end_epoch) => { - !namada::ledger::native_vp::governance::utils::is_valid_validator_voting_period( - current_epoch, - proposal_start_epoch, - proposal_end_epoch, - ) - } - None => { - eprintln!("Proposal end epoch is not in the storage."); - safe_exit(1) - } - } +) -> Result { + tx::is_safe_voting_window(client, proposal_id, proposal_start_epoch).await } /// Removes validators whose vote corresponds to that of the delegator (needless /// vote) -async fn filter_delegations( - client: &HttpClient, +async fn filter_delegations( + client: &C, delegations: HashSet
, proposal_id: u64, delegator_vote: &ProposalVote, @@ -2214,8 +765,10 @@ async fn filter_delegations( ); if let Some(validator_vote) = - rpc::query_storage_value::(client, &vote_key) - .await + rpc::query_storage_value::( + client, &vote_key, + ) + .await { if &validator_vote == delegator_vote { return None; @@ -2229,466 +782,83 @@ 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); - // Check that the validator address exists on chain - let is_validator = - rpc::is_validator(&validator, args.tx.ledger_address.clone()).await; - if !is_validator { - eprintln!( - "The address {} doesn't belong to any known validator account.", - validator - ); - if !args.tx.force { - safe_exit(1) - } - } - let source = ctx.get_opt(&args.source); - // Check that the source address exists on chain - if let Some(source) = &source { - let source_exists = - rpc::known_address(source, args.tx.ledger_address.clone()).await; - if !source_exists { - eprintln!("The source address {} doesn't exist on chain.", source); - if !args.tx.force { - safe_exit(1) - } - } - } - // 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 client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); - match rpc::query_storage_value::(&client, &balance_key).await - { - Some(balance) => { - if balance < args.amount { - eprintln!( - "The balance of the source {} is lower than the amount to \ - be transferred. Amount to transfer is {} and the balance \ - is {}.", - bond_source, args.amount, balance - ); - if !args.tx.force { - safe_exit(1) - } - } - } - None => { - eprintln!("No balance found for the source {}", bond_source); - if !args.tx.force { - safe_exit(1) - } - } - } - let tx_code = ctx.read_wasm(TX_BOND_WASM); - let bond = pos::Bond { - validator, - amount: args.amount, - source, - }; - let data = bond.try_to_vec().expect("Encoding tx data shouldn't fail"); - - let tx = Tx::new(tx_code, Some(data)); - let default_signer = args.source.unwrap_or(args.validator); - process_tx( - ctx, - &args.tx, - tx, - TxSigningKey::WalletAddress(default_signer), - ) - .await; -} - -pub async fn submit_unbond(ctx: Context, args: args::Unbond) { - let validator = ctx.get(&args.validator); - // Check that the validator address exists on chain - let is_validator = - rpc::is_validator(&validator, args.tx.ledger_address.clone()).await; - if !is_validator { - eprintln!( - "The address {} doesn't belong to any known validator account.", - validator - ); - if !args.tx.force { - safe_exit(1) - } - } - - let source = ctx.get_opt(&args.source); - let tx_code = ctx.read_wasm(TX_UNBOND_WASM); - - // Check the source's current bond amount - let bond_source = source.clone().unwrap_or_else(|| validator.clone()); - let bond_id = BondId { - source: bond_source.clone(), - validator: validator.clone(), - }; - let bond_key = ledger::pos::bond_key(&bond_id); - let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); - let bonds = rpc::query_storage_value::(&client, &bond_key).await; - match bonds { - Some(bonds) => { - let mut bond_amount: token::Amount = 0.into(); - for bond in bonds.iter() { - for delta in bond.pos_deltas.values() { - bond_amount += *delta; - } - } - if args.amount > bond_amount { - eprintln!( - "The total bonds of the source {} is lower than the \ - amount to be unbonded. Amount to unbond is {} and the \ - total bonds is {}.", - bond_source, args.amount, bond_amount - ); - if !args.tx.force { - safe_exit(1) - } - } - } - None => { - eprintln!("No bonds found"); - if !args.tx.force { - safe_exit(1) - } - } - } - - let data = pos::Unbond { - validator, - amount: args.amount, - source, - }; - let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); - - let tx = Tx::new(tx_code, Some(data)); - let default_signer = args.source.unwrap_or(args.validator); - process_tx( - ctx, - &args.tx, - tx, - TxSigningKey::WalletAddress(default_signer), - ) - .await; -} - -pub async fn submit_withdraw(ctx: Context, args: args::Withdraw) { - let epoch = rpc::query_epoch(args::Query { - ledger_address: args.tx.ledger_address.clone(), - }) - .await; - - let validator = ctx.get(&args.validator); - // Check that the validator address exists on chain - let is_validator = - rpc::is_validator(&validator, args.tx.ledger_address.clone()).await; - if !is_validator { - eprintln!( - "The address {} doesn't belong to any known validator account.", - validator - ); - if !args.tx.force { - safe_exit(1) - } - } - - let source = ctx.get_opt(&args.source); - let tx_code = ctx.read_wasm(TX_WITHDRAW_WASM); - - // Check the source's current unbond amount - let bond_source = source.clone().unwrap_or_else(|| validator.clone()); - let bond_id = BondId { - source: bond_source.clone(), - validator: validator.clone(), - }; - let bond_key = ledger::pos::unbond_key(&bond_id); - let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); - let unbonds = rpc::query_storage_value::(&client, &bond_key).await; - match unbonds { - Some(unbonds) => { - let mut unbonded_amount: token::Amount = 0.into(); - if let Some(unbond) = unbonds.get(epoch) { - for delta in unbond.deltas.values() { - unbonded_amount += *delta; - } - } - if unbonded_amount == 0.into() { - eprintln!( - "There are no unbonded bonds ready to withdraw in the \ - current epoch {}.", - epoch - ); - if !args.tx.force { - safe_exit(1) - } - } - } - None => { - eprintln!("No unbonded bonds found"); - if !args.tx.force { - safe_exit(1) - } - } - } - - let data = pos::Withdraw { validator, source }; - let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); - - let tx = Tx::new(tx_code, Some(data)); - let default_signer = args.source.unwrap_or(args.validator); - process_tx( - ctx, - &args.tx, - tx, - TxSigningKey::WalletAddress(default_signer), - ) - .await; -} - -pub async fn submit_validator_commission_change( - ctx: Context, +pub async fn submit_bond< + C: namada::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + args: args::Bond, +) -> Result<(), tx::Error> { + tx::submit_bond::(client, wallet, args).await +} + +pub async fn submit_unbond< + C: namada::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + args: args::Unbond, +) -> Result<(), tx::Error> { + tx::submit_unbond::(client, wallet, args).await +} + +pub async fn submit_withdraw< + C: namada::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + args: args::Withdraw, +) -> Result<(), tx::Error> { + tx::submit_withdraw::(client, wallet, args).await +} + +pub async fn submit_validator_commission_change< + C: namada::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, args: args::TxCommissionRateChange, -) { - let epoch = rpc::query_epoch(args::Query { - ledger_address: args.tx.ledger_address.clone(), - }) - .await; - - let tx_code = ctx.read_wasm(TX_CHANGE_COMMISSION_WASM); - let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); - - let validator = ctx.get(&args.validator); - if rpc::is_validator(&validator, args.tx.ledger_address.clone()).await { - if args.rate < Decimal::ZERO || args.rate > Decimal::ONE { - eprintln!("Invalid new commission rate, received {}", args.rate); - if !args.tx.force { - safe_exit(1) - } - } - - let commission_rate_key = - ledger::pos::validator_commission_rate_key(&validator); - let max_commission_rate_change_key = - ledger::pos::validator_max_commission_rate_change_key(&validator); - let commission_rates = rpc::query_storage_value::( - &client, - &commission_rate_key, - ) - .await; - let max_change = rpc::query_storage_value::( - &client, - &max_commission_rate_change_key, - ) - .await; - - match (commission_rates, max_change) { - (Some(rates), Some(max_change)) => { - // Assuming that pipeline length = 2 - let rate_next_epoch = rates.get(epoch.next()).unwrap(); - if (args.rate - rate_next_epoch).abs() > max_change { - eprintln!( - "New rate is too large of a change with respect to \ - the predecessor epoch in which the rate will take \ - effect." - ); - if !args.tx.force { - safe_exit(1) - } - } - } - _ => { - eprintln!("Error retrieving from storage"); - if !args.tx.force { - safe_exit(1) - } - } - } - } else { - eprintln!("The given address {validator} is not a validator."); - if !args.tx.force { - safe_exit(1) - } - } - - let data = pos::CommissionChange { - validator: ctx.get(&args.validator), - new_rate: args.rate, - }; - let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); - - let tx = Tx::new(tx_code, Some(data)); - let default_signer = args.validator; - process_tx( - ctx, - &args.tx, - tx, - TxSigningKey::WalletAddress(default_signer), - ) - .await; +) -> Result<(), tx::Error> { + tx::submit_validator_commission_change::(client, wallet, args).await } /// 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, +async fn process_tx< + C: namada::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, args: &args::Tx, tx: Tx, default_signer: TxSigningKey, -) -> (Context, Vec
) { - let (ctx, to_broadcast) = sign_tx(ctx, tx, args, default_signer).await; - // NOTE: use this to print the request JSON body: - - // let request = - // tendermint_rpc::endpoint::broadcast::tx_commit::Request::new( - // tx_bytes.clone().into(), - // ); - // use tendermint_rpc::Request; - // let request_body = request.into_json(); - // println!("HTTP request body: {}", request_body); - - if args.dry_run { - if let TxBroadcastData::DryRun(tx) = to_broadcast { - rpc::dry_run_tx(&args.ledger_address, tx.to_bytes()).await; - (ctx, vec![]) - } else { - panic!( - "Expected a dry-run transaction, received a wrapper \ - transaction instead" - ); - } - } else { - // Either broadcast or submit transaction and collect result into - // sum type - let result = if args.broadcast_only { - Left(broadcast_tx(args.ledger_address.clone(), &to_broadcast).await) - } else { - Right(submit_tx(args.ledger_address.clone(), to_broadcast).await) - }; - // 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(Err(err)) => { - eprintln!( - "Encountered error while broadcasting transaction: {}", - err - ); - safe_exit(1) - } - Left(Err(err)) => { - eprintln!( - "Encountered error while broadcasting transaction: {}", - err - ); - safe_exit(1) - } - } - } +) -> Result, tx::Error> { + tx::process_tx::(client, wallet, args, tx, default_signer).await } /// Save accounts initialized from a tx into the wallet, if any. -async fn save_initialized_accounts( - mut ctx: Context, +pub async fn save_initialized_accounts( + wallet: &mut Wallet, args: &args::Tx, initialized_accounts: Vec
, ) { - let len = initialized_accounts.len(); - if len != 0 { - // Store newly initialized account addresses in the wallet - println!( - "The transaction initialized {} new account{}", - len, - if len == 1 { "" } else { "s" } - ); - // Store newly initialized account addresses in the wallet - let wallet = &mut ctx.wallet; - for (ix, address) in initialized_accounts.iter().enumerate() { - let encoded = address.encode(); - let alias: Cow = match &args.initialized_account_alias { - Some(initialized_account_alias) => { - if len == 1 { - // If there's only one account, use the - // alias as is - initialized_account_alias.into() - } else { - // If there're multiple accounts, use - // the alias as prefix, followed by - // index number - format!("{}{}", initialized_account_alias, ix).into() - } - } - None => { - print!("Choose an alias for {}: ", encoded); - io::stdout().flush().await.unwrap(); - let mut alias = String::new(); - io::stdin().read_line(&mut alias).await.unwrap(); - alias.trim().to_owned().into() - } - }; - let alias = alias.into_owned(); - let added = wallet.add_address(alias.clone(), address.clone()); - match added { - Some(new_alias) if new_alias != encoded => { - println!( - "Added alias {} for address {}.", - new_alias, encoded - ); - } - _ => println!("No alias added for address {}.", encoded), - }; - } - if !args.dry_run { - wallet.save().unwrap_or_else(|err| eprintln!("{}", err)); - } else { - println!("Transaction dry run. No addresses have been saved.") - } - } + tx::save_initialized_accounts::(wallet, args, initialized_accounts).await } /// Broadcast a transaction to be included in the blockchain and checks that /// the tx has been successfully included into the mempool of a validator /// /// In the case of errors in any of those stages, an error message is returned -pub async fn broadcast_tx( - address: TendermintAddress, +pub async fn broadcast_tx( + rpc_cli: &C, to_broadcast: &TxBroadcastData, -) -> Result { - let (tx, wrapper_tx_hash, decrypted_tx_hash) = match to_broadcast { - TxBroadcastData::Wrapper { - tx, - wrapper_hash, - decrypted_hash, - } => (tx, wrapper_hash, decrypted_hash), - _ => panic!("Cannot broadcast a dry-run transaction"), - }; - - tracing::debug!( - tendermint_rpc_address = ?address, - transaction = ?to_broadcast, - "Broadcasting transaction", - ); - let rpc_cli = HttpClient::new(address)?; - - // TODO: configure an explicit timeout value? we need to hack away at - // `tendermint-rs` for this, which is currently using a hard-coded 30s - // timeout. - let response = rpc_cli.broadcast_tx_sync(tx.to_bytes().into()).await?; - - if response.code == 0.into() { - println!("Transaction added to mempool: {:?}", response); - // Print the transaction identifiers to enable the extraction of - // acceptance/application results later - { - println!("Wrapper transaction hash: {:?}", wrapper_tx_hash); - println!("Inner transaction hash: {:?}", decrypted_tx_hash); - } - Ok(response) - } else { - Err(RpcError::server(serde_json::to_string(&response).unwrap())) - } +) -> Result { + tx::broadcast_tx(rpc_cli, to_broadcast).await } /// Broadcast a transaction to be included in the blockchain. @@ -2699,72 +869,9 @@ pub async fn broadcast_tx( /// 3. The decrypted payload of the tx has been included on the blockchain. /// /// In the case of errors in any of those stages, an error message is returned -pub async fn submit_tx( - address: TendermintAddress, +pub async fn submit_tx( + client: &C, to_broadcast: TxBroadcastData, -) -> Result { - let (_, wrapper_hash, decrypted_hash) = match &to_broadcast { - TxBroadcastData::Wrapper { - tx, - wrapper_hash, - decrypted_hash, - } => (tx, wrapper_hash, decrypted_hash), - _ => panic!("Cannot broadcast a dry-run transaction"), - }; - - // Broadcast the supplied transaction - broadcast_tx(address.clone(), &to_broadcast).await?; - - let max_wait_time = Duration::from_secs( - env::var(ENV_VAR_NAMADA_EVENTS_MAX_WAIT_TIME_SECONDS) - .ok() - .and_then(|val| val.parse().ok()) - .unwrap_or(DEFAULT_NAMADA_EVENTS_MAX_WAIT_TIME_SECONDS), - ); - let deadline = Instant::now() + max_wait_time; - - tracing::debug!( - tendermint_rpc_address = ?address, - transaction = ?to_broadcast, - ?deadline, - "Awaiting transaction approval", - ); - - let parsed = { - let wrapper_query = rpc::TxEventQuery::Accepted(wrapper_hash.as_str()); - let event = - rpc::query_tx_status(wrapper_query, address.clone(), deadline) - .await; - let parsed = TxResponse::from_event(event); - - println!( - "Transaction accepted with result: {}", - serde_json::to_string_pretty(&parsed).unwrap() - ); - // The transaction is now on chain. We wait for it to be decrypted - // and applied - if parsed.code == 0.to_string() { - // We also listen to the event emitted when the encrypted - // payload makes its way onto the blockchain - let decrypted_query = - rpc::TxEventQuery::Applied(decrypted_hash.as_str()); - let event = - rpc::query_tx_status(decrypted_query, address, deadline).await; - let parsed = TxResponse::from_event(event); - println!( - "Transaction applied with result: {}", - serde_json::to_string_pretty(&parsed).unwrap() - ); - Ok(parsed) - } else { - Ok(parsed) - } - }; - - tracing::debug!( - transaction = ?to_broadcast, - "Transaction approved", - ); - - parsed +) -> Result { + tx::submit_tx(client, to_broadcast).await } diff --git a/apps/src/lib/client/types.rs b/apps/src/lib/client/types.rs deleted file mode 100644 index 5a26244474..0000000000 --- a/apps/src/lib/client/types.rs +++ /dev/null @@ -1,94 +0,0 @@ -use async_trait::async_trait; -use masp_primitives::merkle_tree::MerklePath; -use masp_primitives::primitives::{Diversifier, Note, ViewingKey}; -use masp_primitives::sapling::Node; -use masp_primitives::transaction::components::Amount; -use namada::types::address::Address; -use namada::types::masp::{TransferSource, TransferTarget}; -use namada::types::storage::Epoch; -use namada::types::transaction::GasLimit; -use namada::types::{key, token}; - -use super::rpc; -use crate::cli::{args, Context}; -use crate::client::tx::Conversions; -use crate::facade::tendermint_config::net::Address as TendermintAddress; - -#[derive(Clone, Debug)] -pub struct ParsedTxArgs { - /// Simulate applying the transaction - pub dry_run: bool, - /// Submit the transaction even if it doesn't pass client checks - pub force: bool, - /// Do not wait for the transaction to be added to the blockchain - pub broadcast_only: bool, - /// The address of the ledger node as host:port - pub ledger_address: TendermintAddress, - /// If any new account is initialized by the tx, use the given alias to - /// save it in the wallet. - pub initialized_account_alias: Option, - /// The amount being payed to include the transaction - pub fee_amount: token::Amount, - /// The token in which the fee is being paid - pub fee_token: Address, - /// The max amount of gas used to process tx - pub gas_limit: GasLimit, - /// Sign the tx with the key for the given alias from your wallet - pub signing_key: Option, - /// Sign the tx with the keypair of the public key of the given address - pub signer: Option
, -} - -#[derive(Clone, Debug)] -pub struct ParsedTxTransferArgs { - /// Common tx arguments - pub tx: ParsedTxArgs, - /// Transfer source address - pub source: TransferSource, - /// Transfer target address - pub target: TransferTarget, - /// Transferred token address - pub token: Address, - /// Transferred token amount - pub amount: token::Amount, -} - -#[async_trait] -pub trait ShieldedTransferContext { - async fn collect_unspent_notes( - &mut self, - ledger_address: TendermintAddress, - vk: &ViewingKey, - target: Amount, - target_epoch: Epoch, - ) -> ( - Amount, - Vec<(Diversifier, Note, MerklePath)>, - Conversions, - ); - - async fn query_epoch(&self, ledger_address: TendermintAddress) -> Epoch; -} - -#[async_trait] -impl ShieldedTransferContext for Context { - async fn collect_unspent_notes( - &mut self, - ledger_address: TendermintAddress, - vk: &ViewingKey, - target: Amount, - target_epoch: Epoch, - ) -> ( - Amount, - Vec<(Diversifier, Note, MerklePath)>, - Conversions, - ) { - self.shielded - .collect_unspent_notes(ledger_address, vk, target, target_epoch) - .await - } - - async fn query_epoch(&self, ledger_address: TendermintAddress) -> Epoch { - rpc::query_epoch(args::Query { ledger_address }).await - } -} diff --git a/apps/src/lib/client/utils.rs b/apps/src/lib/client/utils.rs index 21fed46c11..7bc434a152 100644 --- a/apps/src/lib/client/utils.rs +++ b/apps/src/lib/client/utils.rs @@ -10,6 +10,7 @@ use borsh::BorshSerialize; use flate2::read::GzDecoder; use flate2::write::GzEncoder; use flate2::Compression; +use namada::ledger::wallet::Wallet; use namada::types::address; use namada::types::chain::ChainId; use namada::types::key::*; @@ -30,7 +31,7 @@ use crate::config::{self, Config, TendermintMode}; use crate::facade::tendermint::node::Id as TendermintNodeId; use crate::facade::tendermint_config::net::Address as TendermintAddress; use crate::node::ledger::tendermint_node; -use crate::wallet::{pre_genesis, Wallet}; +use crate::wallet::{pre_genesis, read_and_confirm_pwd, CliWalletUtils}; use crate::wasm_loader; pub const NET_ACCOUNTS_DIR: &str = "setup"; @@ -107,13 +108,12 @@ pub async fn join_network( validator_alias_and_dir.map(|(validator_alias, pre_genesis_dir)| { ( validator_alias, - pre_genesis::ValidatorWallet::load(&pre_genesis_dir) - .unwrap_or_else(|err| { - eprintln!( - "Error loading validator pre-genesis wallet {err}", - ); - cli::safe_exit(1) - }), + pre_genesis::load(&pre_genesis_dir).unwrap_or_else(|err| { + eprintln!( + "Error loading validator pre-genesis wallet {err}", + ); + cli::safe_exit(1) + }), ) }); @@ -260,7 +260,7 @@ pub async fn join_network( let genesis_file_path = base_dir.join(format!("{}.toml", chain_id.as_str())); - let mut wallet = Wallet::load_or_new_from_genesis( + let mut wallet = crate::wallet::load_or_new_from_genesis( &chain_dir, genesis_config::open_genesis_config(genesis_file_path).unwrap(), ); @@ -301,7 +301,7 @@ pub async fn join_network( pre_genesis_wallet, ); - wallet.save().unwrap(); + crate::wallet::save(&wallet).unwrap(); // Update the config from the default non-validator settings to // validator settings @@ -480,7 +480,7 @@ pub fn init_network( // Generate the consensus, account and reward keys, unless they're // pre-defined. - let mut wallet = Wallet::load_or_new(&chain_dir); + let mut wallet = crate::wallet::load_or_new(&chain_dir); let consensus_pk = try_parse_public_key( format!("validator {name} consensus key"), @@ -489,11 +489,9 @@ pub fn init_network( .unwrap_or_else(|| { let alias = format!("{}-consensus-key", name); println!("Generating validator {} consensus key...", name); - let (_alias, keypair) = wallet.gen_key( - SchemeType::Ed25519, - Some(alias), - unsafe_dont_encrypt, - ); + let password = read_and_confirm_pwd(unsafe_dont_encrypt); + let (_alias, keypair) = + wallet.gen_key(SchemeType::Ed25519, Some(alias), password); // Write consensus key for Tendermint tendermint_node::write_validator_key(&tm_home_dir, &keypair); @@ -508,11 +506,9 @@ pub fn init_network( .unwrap_or_else(|| { let alias = format!("{}-account-key", name); println!("Generating validator {} account key...", name); - let (_alias, keypair) = wallet.gen_key( - SchemeType::Ed25519, - Some(alias), - unsafe_dont_encrypt, - ); + let password = read_and_confirm_pwd(unsafe_dont_encrypt); + let (_alias, keypair) = + wallet.gen_key(SchemeType::Ed25519, Some(alias), password); keypair.ref_to() }); @@ -523,11 +519,9 @@ pub fn init_network( .unwrap_or_else(|| { let alias = format!("{}-protocol-key", name); println!("Generating validator {} protocol signing key...", name); - let (_alias, keypair) = wallet.gen_key( - SchemeType::Ed25519, - Some(alias), - unsafe_dont_encrypt, - ); + let password = read_and_confirm_pwd(unsafe_dont_encrypt); + let (_alias, keypair) = + wallet.gen_key(SchemeType::Ed25519, Some(alias), password); keypair.ref_to() }); @@ -547,12 +541,12 @@ pub fn init_network( name ); - let validator_keys = wallet - .gen_validator_keys( - Some(protocol_pk.clone()), - SchemeType::Ed25519, - ) - .expect("Generating new validator keys should not fail"); + let validator_keys = crate::wallet::gen_validator_keys( + &mut wallet, + Some(protocol_pk.clone()), + SchemeType::Ed25519, + ) + .expect("Generating new validator keys should not fail"); let pk = validator_keys.dkg_keypair.as_ref().unwrap().public(); wallet.add_validator_data(address.clone(), validator_keys); pk @@ -572,12 +566,12 @@ pub fn init_network( // Write keypairs to wallet wallet.add_address(name.clone(), address); - wallet.save().unwrap(); + crate::wallet::save(&wallet).unwrap(); }); // Create a wallet for all accounts other than validators let mut wallet = - Wallet::load_or_new(&accounts_dir.join(NET_OTHER_ACCOUNTS_DIR)); + crate::wallet::load_or_new(&accounts_dir.join(NET_OTHER_ACCOUNTS_DIR)); if let Some(established) = &mut config.established { established.iter_mut().for_each(|(name, config)| { init_established_account( @@ -607,10 +601,11 @@ pub fn init_network( "Generating implicit account {} key and address ...", name ); + let password = read_and_confirm_pwd(unsafe_dont_encrypt); let (_alias, keypair) = wallet.gen_key( SchemeType::Ed25519, Some(name.clone()), - unsafe_dont_encrypt, + password, ); let public_key = genesis_config::HexString(keypair.ref_to().to_string()); @@ -643,8 +638,8 @@ pub fn init_network( genesis_config::write_genesis_config(&config_clean, &genesis_path); // Add genesis addresses and save the wallet with other account keys - wallet.add_genesis_addresses(config_clean.clone()); - wallet.save().unwrap(); + crate::wallet::add_genesis_addresses(&mut wallet, config_clean.clone()); + crate::wallet::save(&wallet).unwrap(); // Write the global config setting the default chain ID let global_config = GlobalConfig::new(chain_id.clone()); @@ -693,9 +688,9 @@ pub fn init_network( ); global_config.write(validator_dir).unwrap(); // Add genesis addresses to the validator's wallet - let mut wallet = Wallet::load_or_new(&validator_chain_dir); - wallet.add_genesis_addresses(config_clean.clone()); - wallet.save().unwrap(); + let mut wallet = crate::wallet::load_or_new(&validator_chain_dir); + crate::wallet::add_genesis_addresses(&mut wallet, config_clean.clone()); + crate::wallet::save(&wallet).unwrap(); }); // Generate the validators' ledger config @@ -846,7 +841,7 @@ pub fn init_network( fn init_established_account( name: impl AsRef, - wallet: &mut Wallet, + wallet: &mut Wallet, config: &mut genesis_config::EstablishedAccountConfig, unsafe_dont_encrypt: bool, ) { @@ -857,10 +852,11 @@ fn init_established_account( } if config.public_key.is_none() { println!("Generating established account {} key...", name.as_ref()); + let password = read_and_confirm_pwd(unsafe_dont_encrypt); let (_alias, keypair) = wallet.gen_key( SchemeType::Ed25519, Some(format!("{}-key", name.as_ref())), - unsafe_dont_encrypt, + password, ); let public_key = genesis_config::HexString(keypair.ref_to().to_string()); @@ -904,7 +900,7 @@ pub fn init_genesis_validator( let pre_genesis_dir = validator_pre_genesis_dir(&global_args.base_dir, &alias); println!("Generating validator keys..."); - let pre_genesis = pre_genesis::ValidatorWallet::gen_and_store( + let pre_genesis = pre_genesis::gen_and_store( key_scheme, unsafe_dont_encrypt, &pre_genesis_dir, diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index 4c461568a9..16a1f42c8f 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -50,6 +50,7 @@ use num_traits::{FromPrimitive, ToPrimitive}; use thiserror::Error; use tokio::sync::mpsc::UnboundedSender; +use crate::config; use crate::config::{genesis, TendermintMode}; #[cfg(feature = "abcipp")] use crate::facade::tendermint_proto::abci::response_verify_vote_extension::VerifyStatus; @@ -63,7 +64,6 @@ use crate::node::ledger::shims::abcipp_shim_types::shim::response::TxResult; use crate::node::ledger::{storage, tendermint_node}; #[allow(unused_imports)] use crate::wallet::ValidatorData; -use crate::{config, wallet}; fn key_to_tendermint( pk: &common::PublicKey, @@ -275,7 +275,7 @@ where "{}", wallet_path.as_path().to_str().unwrap() ); - let wallet = wallet::Wallet::load_or_new_from_genesis( + let mut wallet = crate::wallet::load_or_new_from_genesis( wallet_path, genesis::genesis_config::open_genesis_config( genesis_path, @@ -285,7 +285,7 @@ where wallet .take_validator_data() .map(|data| ShellMode::Validator { - data, + data: data.clone(), broadcast_sender, }) .expect( @@ -295,11 +295,13 @@ where } #[cfg(feature = "dev")] { - let validator_keys = wallet::defaults::validator_keys(); + let validator_keys = + crate::wallet::defaults::validator_keys(); ShellMode::Validator { - data: wallet::ValidatorData { - address: wallet::defaults::validator_address(), - keys: wallet::ValidatorKeys { + data: crate::wallet::ValidatorData { + address: crate::wallet::defaults::validator_address( + ), + keys: crate::wallet::ValidatorKeys { protocol_keypair: validator_keys.0, dkg_keypair: Some(validator_keys.1), }, @@ -631,7 +633,7 @@ where let genesis_path = &self .base_dir .join(format!("{}.toml", self.chain_id.as_str())); - let mut wallet = wallet::Wallet::load_or_new_from_genesis( + let mut wallet = crate::wallet::load_or_new_from_genesis( wallet_path, genesis::genesis_config::open_genesis_config(genesis_path).unwrap(), ); @@ -651,7 +653,7 @@ where let pk = common::SecretKey::deserialize(&mut pk_bytes.as_slice()) .expect("Validator's public key should be deserializable") .ref_to(); - wallet.find_key_by_pk(&pk).expect( + wallet.find_key_by_pk(&pk, None).expect( "A validator's established keypair should be stored in its \ wallet", ) diff --git a/apps/src/lib/node/ledger/shims/abcipp_shim.rs b/apps/src/lib/node/ledger/shims/abcipp_shim.rs index 74a56a4ddc..b4421a9b97 100644 --- a/apps/src/lib/node/ledger/shims/abcipp_shim.rs +++ b/apps/src/lib/node/ledger/shims/abcipp_shim.rs @@ -5,6 +5,7 @@ use std::pin::Pin; use std::task::{Context, Poll}; use futures::future::FutureExt; +use namada::proto::Tx; use namada::types::address::Address; #[cfg(not(feature = "abcipp"))] use namada::types::hash::Hash; @@ -22,7 +23,11 @@ use super::abcipp_shim_types::shim::TxBytes; use super::abcipp_shim_types::shim::{Error, Request, Response}; use crate::config; #[cfg(not(feature = "abcipp"))] -use crate::facade::tendermint_proto::abci::RequestBeginBlock; +use crate::facade::tendermint_proto::abci::{ + RequestBeginBlock, ResponseDeliverTx, +}; +#[cfg(not(feature = "abcipp"))] +use crate::facade::tower_abci::response::DeliverTx; use crate::facade::tower_abci::{BoxError, Request as Req, Response as Resp}; /// The shim wraps the shell, which implements ABCI++. @@ -132,8 +137,14 @@ impl AbcippShim { } #[cfg(not(feature = "abcipp"))] Req::DeliverTx(tx) => { + let mut deliver: DeliverTx = Default::default(); + // Attach events to this transaction if possible + if let Ok(tx) = Tx::try_from(&tx.tx[..]) { + let resp: ResponseDeliverTx = tx.into(); + deliver.events = resp.events; + } self.delivered_txs.push(tx.tx); - Ok(Resp::DeliverTx(Default::default())) + Ok(Resp::DeliverTx(deliver)) } #[cfg(not(feature = "abcipp"))] Req::EndBlock(_) => { diff --git a/apps/src/lib/wallet/alias.rs b/apps/src/lib/wallet/alias.rs index 13d977b852..8b13789179 100644 --- a/apps/src/lib/wallet/alias.rs +++ b/apps/src/lib/wallet/alias.rs @@ -1,103 +1 @@ -//! Wallet address and key aliases. -use std::convert::Infallible; -use std::fmt::Display; -use std::hash::Hash; -use std::str::FromStr; - -use serde::{Deserialize, Serialize}; - -/// Aliases created from raw strings are kept in-memory as given, but their -/// `Serialize` and `Display` instance converts them to lowercase. Their -/// `PartialEq` instance is case-insensitive. -#[derive(Clone, Debug, Default, Deserialize, PartialOrd, Ord, Eq)] -#[serde(transparent)] -pub struct Alias(String); - -impl Alias { - /// Normalize an alias to lower-case - pub fn normalize(&self) -> String { - self.0.to_lowercase() - } - - /// Returns the length of the underlying `String`. - pub fn len(&self) -> usize { - self.0.len() - } - - /// Is the underlying `String` empty? - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } -} - -impl Serialize for Alias { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - self.normalize().serialize(serializer) - } -} - -impl PartialEq for Alias { - fn eq(&self, other: &Self) -> bool { - self.normalize() == other.normalize() - } -} - -impl Hash for Alias { - fn hash(&self, state: &mut H) { - self.normalize().hash(state); - } -} - -impl From for Alias -where - T: AsRef, -{ - fn from(raw: T) -> Self { - Self(raw.as_ref().to_owned()) - } -} - -impl From for String { - fn from(alias: Alias) -> Self { - alias.normalize() - } -} - -impl<'a> From<&'a Alias> for String { - fn from(alias: &'a Alias) -> Self { - alias.normalize() - } -} - -impl Display for Alias { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.normalize().fmt(f) - } -} - -impl FromStr for Alias { - type Err = Infallible; - - fn from_str(s: &str) -> Result { - Ok(Self(s.into())) - } -} - -/// Default alias of a validator's account key -pub fn validator_key(validator_alias: &Alias) -> Alias { - format!("{validator_alias}-validator-key").into() -} - -/// Default alias of a validator's consensus key -pub fn validator_consensus_key(validator_alias: &Alias) -> Alias { - format!("{validator_alias}-consensus-key").into() -} - -/// Default alias of a validator's Tendermint node key -pub fn validator_tendermint_node_key(validator_alias: &Alias) -> Alias { - format!("{validator_alias}-tendermint-node-key").into() -} diff --git a/apps/src/lib/wallet/defaults.rs b/apps/src/lib/wallet/defaults.rs index b0ae08ac83..643b0dd7cb 100644 --- a/apps/src/lib/wallet/defaults.rs +++ b/apps/src/lib/wallet/defaults.rs @@ -6,12 +6,12 @@ pub use dev::{ christel_address, christel_keypair, daewon_address, daewon_keypair, keys, validator_address, validator_keypair, validator_keys, }; +use namada::ledger::wallet::Alias; use namada::ledger::{eth_bridge, governance, pos}; use namada::types::address::Address; use namada::types::key::*; use crate::config::genesis::genesis_config::GenesisConfig; -use crate::wallet::alias::Alias; /// The default addresses with their aliases. pub fn addresses_from_genesis(genesis: GenesisConfig) -> Vec<(Alias, Address)> { @@ -71,13 +71,12 @@ pub fn addresses_from_genesis(genesis: GenesisConfig) -> Vec<(Alias, Address)> { #[cfg(feature = "dev")] mod dev { use borsh::BorshDeserialize; + use namada::ledger::wallet::Alias; use namada::ledger::{governance, pos}; use namada::types::address::{self, Address}; use namada::types::key::dkg_session_keys::DkgKeypair; use namada::types::key::*; - use crate::wallet::alias::Alias; - /// Generate a new protocol signing keypair and DKG session keypair pub fn validator_keys() -> (common::SecretKey, DkgKeypair) { let bytes: [u8; 33] = [ diff --git a/apps/src/lib/wallet/keys.rs b/apps/src/lib/wallet/keys.rs index 7627bd9b16..8b13789179 100644 --- a/apps/src/lib/wallet/keys.rs +++ b/apps/src/lib/wallet/keys.rs @@ -1,243 +1 @@ -//! Cryptographic keys for digital signatures support for the wallet. -use std::fmt::Display; -use std::marker::PhantomData; -use std::str::FromStr; - -use borsh::{BorshDeserialize, BorshSerialize}; -use data_encoding::HEXLOWER; -use orion::{aead, kdf}; -use serde::{Deserialize, Serialize}; -use thiserror::Error; - -use super::read_password; - -const ENCRYPTED_KEY_PREFIX: &str = "encrypted:"; -const UNENCRYPTED_KEY_PREFIX: &str = "unencrypted:"; - -/// A keypair stored in a wallet -#[derive(Debug)] -pub enum StoredKeypair -where - ::Err: Display, -{ - /// An encrypted keypair - Encrypted(EncryptedKeypair), - /// An raw (unencrypted) keypair - Raw(T), -} - -impl Serialize - for StoredKeypair -where - ::Err: Display, -{ - fn serialize( - &self, - serializer: S, - ) -> std::result::Result - where - S: serde::Serializer, - { - // String encoded, because toml doesn't support enums - match self { - StoredKeypair::Encrypted(encrypted) => { - let keypair_string = - format!("{}{}", ENCRYPTED_KEY_PREFIX, encrypted); - serde::Serialize::serialize(&keypair_string, serializer) - } - StoredKeypair::Raw(raw) => { - let keypair_string = - format!("{}{}", UNENCRYPTED_KEY_PREFIX, raw); - serde::Serialize::serialize(&keypair_string, serializer) - } - } - } -} - -impl<'de, T: BorshSerialize + BorshDeserialize + Display + FromStr> - Deserialize<'de> for StoredKeypair -where - ::Err: Display, -{ - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - use serde::de::Error; - - let keypair_string: String = - serde::Deserialize::deserialize(deserializer) - .map_err(|err| { - DeserializeStoredKeypairError::InvalidStoredKeypairString( - err.to_string(), - ) - }) - .map_err(D::Error::custom)?; - if let Some(raw) = keypair_string.strip_prefix(UNENCRYPTED_KEY_PREFIX) { - FromStr::from_str(raw) - .map(|keypair| Self::Raw(keypair)) - .map_err(|err| { - DeserializeStoredKeypairError::InvalidStoredKeypairString( - err.to_string(), - ) - }) - .map_err(D::Error::custom) - } else if let Some(encrypted) = - keypair_string.strip_prefix(ENCRYPTED_KEY_PREFIX) - { - FromStr::from_str(encrypted) - .map(Self::Encrypted) - .map_err(|err| { - DeserializeStoredKeypairError::InvalidStoredKeypairString( - err.to_string(), - ) - }) - .map_err(D::Error::custom) - } else { - Err(DeserializeStoredKeypairError::MissingPrefix) - .map_err(D::Error::custom) - } - } -} - -#[allow(missing_docs)] -#[derive(Error, Debug)] -pub enum DeserializeStoredKeypairError { - #[error("The stored keypair is not valid: {0}")] - InvalidStoredKeypairString(String), - #[error("The stored keypair is missing a prefix")] - MissingPrefix, -} - -/// An encrypted keypair stored in a wallet -#[derive(Debug)] -pub struct EncryptedKeypair( - Vec, - PhantomData, -); - -impl Display for EncryptedKeypair { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", HEXLOWER.encode(self.0.as_ref())) - } -} - -impl FromStr for EncryptedKeypair { - type Err = data_encoding::DecodeError; - - fn from_str(s: &str) -> Result { - HEXLOWER.decode(s.as_ref()).map(|x| Self(x, PhantomData)) - } -} - -#[allow(missing_docs)] -#[derive(Debug, Error)] -pub enum DecryptionError { - #[error("Unexpected encryption salt")] - BadSalt, - #[error("Unable to decrypt the keypair. Is the password correct?")] - DecryptionError, - #[error("Unable to deserialize the keypair")] - DeserializingError, - #[error("Asked not to decrypt")] - NotDecrypting, -} - -impl - StoredKeypair -where - ::Err: Display, -{ - /// Construct a keypair for storage. If no password is provided, the keypair - /// will be stored raw without encryption. Returns the key for storing and a - /// reference-counting point to the raw key. - pub fn new(keypair: T, password: Option) -> (Self, T) { - match password { - Some(password) => ( - Self::Encrypted(EncryptedKeypair::new(&keypair, password)), - keypair, - ), - None => (Self::Raw(keypair.clone()), keypair), - } - } - - /// Get a raw keypair from a stored keypair. If the keypair is encrypted and - /// no password is provided in the argument, a password will be prompted - /// from stdin. - pub fn get( - &self, - decrypt: bool, - password: Option, - ) -> Result { - match self { - StoredKeypair::Encrypted(encrypted_keypair) => { - if decrypt { - let password = password.unwrap_or_else(|| { - read_password("Enter decryption password: ") - }); - let key = encrypted_keypair.decrypt(password)?; - Ok(key) - } else { - Err(DecryptionError::NotDecrypting) - } - } - StoredKeypair::Raw(keypair) => Ok(keypair.clone()), - } - } - - pub fn is_encrypted(&self) -> bool { - match self { - StoredKeypair::Encrypted(_) => true, - StoredKeypair::Raw(_) => false, - } - } -} - -impl EncryptedKeypair { - /// Encrypt a keypair and store it with its salt. - pub fn new(keypair: &T, password: String) -> Self { - let salt = encryption_salt(); - let encryption_key = encryption_key(&salt, password); - - let data = keypair - .try_to_vec() - .expect("Serializing keypair shouldn't fail"); - - let encrypted_keypair = aead::seal(&encryption_key, &data) - .expect("Encryption of data shouldn't fail"); - - let encrypted_data = [salt.as_ref(), &encrypted_keypair].concat(); - - Self(encrypted_data, PhantomData) - } - - /// Decrypt an encrypted keypair - pub fn decrypt(&self, password: String) -> Result { - let salt_len = encryption_salt().len(); - let (raw_salt, cipher) = self.0.split_at(salt_len); - - let salt = kdf::Salt::from_slice(raw_salt) - .map_err(|_| DecryptionError::BadSalt)?; - - let encryption_key = encryption_key(&salt, password); - - let decrypted_data = aead::open(&encryption_key, cipher) - .map_err(|_| DecryptionError::DecryptionError)?; - - T::try_from_slice(&decrypted_data) - .map_err(|_| DecryptionError::DeserializingError) - } -} - -/// Keypair encryption salt -fn encryption_salt() -> kdf::Salt { - kdf::Salt::default() -} - -/// Make encryption secret key from a password. -fn encryption_key(salt: &kdf::Salt, password: String) -> kdf::SecretKey { - kdf::Password::from_slice(password.as_bytes()) - .and_then(|password| kdf::derive_key(&password, salt, 3, 1 << 17, 32)) - .expect("Generation of encryption secret key shouldn't fail") -} diff --git a/apps/src/lib/wallet/mod.rs b/apps/src/lib/wallet/mod.rs index b79ff6703b..dbe76b7b35 100644 --- a/apps/src/lib/wallet/mod.rs +++ b/apps/src/lib/wallet/mod.rs @@ -4,528 +4,201 @@ mod keys; pub mod pre_genesis; mod store; -use std::collections::HashMap; -use std::fmt::Display; +use std::io::{self, Write}; use std::path::{Path, PathBuf}; -use std::str::FromStr; use std::{env, fs}; -use borsh::{BorshDeserialize, BorshSerialize}; -use masp_primitives::zip32::ExtendedFullViewingKey; -use namada::types::address::Address; -use namada::types::key::*; -use namada::types::masp::{ - ExtendedSpendingKey, ExtendedViewingKey, PaymentAddress, +use namada::ledger::wallet::{ + Alias, ConfirmationResponse, FindKeyError, Wallet, WalletUtils, +}; +pub use namada::ledger::wallet::{ + DecryptionError, StoredKeypair, ValidatorData, ValidatorKeys, }; +use namada::types::key::*; pub use store::wallet_file; -use thiserror::Error; -use self::alias::Alias; -pub use self::keys::{DecryptionError, StoredKeypair}; -use self::store::Store; -pub use self::store::{ValidatorData, ValidatorKeys}; use crate::cli; use crate::config::genesis::genesis_config::GenesisConfig; #[derive(Debug)] -pub struct Wallet { - store_dir: PathBuf, - store: Store, - decrypted_key_cache: HashMap, - decrypted_spendkey_cache: HashMap, -} - -#[derive(Error, Debug)] -pub enum FindKeyError { - #[error("No matching key found")] - KeyNotFound, - #[error("{0}")] - KeyDecryptionError(keys::DecryptionError), -} - -impl Wallet { - /// Load a wallet from the store file. - pub fn load(store_dir: &Path) -> Option { - let store = Store::load(store_dir).unwrap_or_else(|err| { - eprintln!("Unable to load the wallet: {}", err); - cli::safe_exit(1) - }); - Some(Self { - store_dir: store_dir.to_path_buf(), - store, - decrypted_key_cache: HashMap::default(), - decrypted_spendkey_cache: HashMap::default(), - }) - } - - /// Load a wallet from the store file or create a new wallet without any - /// keys or addresses. - pub fn load_or_new(store_dir: &Path) -> Self { - let store = Store::load_or_new(store_dir).unwrap_or_else(|err| { - eprintln!("Unable to load the wallet: {}", err); - cli::safe_exit(1) - }); - Self { - store_dir: store_dir.to_path_buf(), - store, - decrypted_key_cache: HashMap::default(), - decrypted_spendkey_cache: HashMap::default(), - } - } - - /// Load a wallet from the store file or create a new one with the default - /// addresses loaded from the genesis file, if not found. - pub fn load_or_new_from_genesis( - store_dir: &Path, - genesis_cfg: GenesisConfig, - ) -> Self { - let store = Store::load_or_new_from_genesis(store_dir, genesis_cfg) - .unwrap_or_else(|err| { - eprintln!("Unable to load the wallet: {}", err); - cli::safe_exit(1) - }); - Self { - store_dir: store_dir.to_path_buf(), - store, - decrypted_key_cache: HashMap::default(), - decrypted_spendkey_cache: HashMap::default(), - } - } - - /// Add addresses from a genesis configuration. - pub fn add_genesis_addresses(&mut self, genesis: GenesisConfig) { - self.store.add_genesis_addresses(genesis) - } - - /// Save the wallet store to a file. - pub fn save(&self) -> std::io::Result<()> { - self.store.save(&self.store_dir) - } - - /// Prompt for pssword and confirm it if parameter is false - fn new_password_prompt(unsafe_dont_encrypt: bool) -> Option { - let password = if unsafe_dont_encrypt { - println!("Warning: The keypair will NOT be encrypted."); - None - } else { - Some(read_password("Enter your encryption password: ")) +pub struct CliWalletUtils; + +impl WalletUtils for CliWalletUtils { + type Storage = PathBuf; + + /// Read the password for encryption/decryption from the file/env/stdin. + /// Panics if all options are empty/invalid. + fn read_password(prompt_msg: &str) -> String { + let pwd = match env::var("ANOMA_WALLET_PASSWORD_FILE") { + Ok(path) => fs::read_to_string(path) + .expect("Something went wrong reading the file"), + Err(_) => match env::var("ANOMA_WALLET_PASSWORD") { + Ok(password) => password, + Err(_) => rpassword::read_password_from_tty(Some(prompt_msg)) + .unwrap_or_default(), + }, }; - // Bis repetita for confirmation. - let pwd = if unsafe_dont_encrypt { - None - } else { - Some(read_password( - "To confirm, please enter the same encryption password once \ - more: ", - )) - }; - if pwd != password { - eprintln!("Your two inputs do not match!"); + if pwd.is_empty() { + eprintln!("Password cannot be empty"); cli::safe_exit(1) } - password - } - - /// Generate a new keypair and derive an implicit address from its public - /// and insert them into the store with the provided alias, converted to - /// lower case. If none provided, the alias will be the public key hash (in - /// lowercase too). If the key is to be encrypted, will prompt for - /// password from stdin. Stores the key in decrypted key cache and - /// returns the alias of the key and a reference-counting pointer to the - /// key. - pub fn gen_key( - &mut self, - scheme: SchemeType, - alias: Option, - unsafe_dont_encrypt: bool, - ) -> (String, common::SecretKey) { - let password = read_and_confirm_pwd(unsafe_dont_encrypt); - let (alias, key) = self.store.gen_key(scheme, alias, password); - // Cache the newly added key - self.decrypted_key_cache.insert(alias.clone(), key.clone()); - (alias.into(), key) - } - - pub fn gen_spending_key( - &mut self, - alias: String, - unsafe_dont_encrypt: bool, - ) -> (String, ExtendedSpendingKey) { - let password = Self::new_password_prompt(unsafe_dont_encrypt); - let (alias, key) = self.store.gen_spending_key(alias, password); - // Cache the newly added key - self.decrypted_spendkey_cache.insert(alias.clone(), key); - (alias.into(), key) - } - - /// Generate keypair - /// for signing protocol txs and for the DKG (which will also be stored) - /// A protocol keypair may be optionally provided, indicating that - /// we should re-use a keypair already in the wallet - pub fn gen_validator_keys( - &mut self, - protocol_pk: Option, - scheme: SchemeType, - ) -> Result { - let protocol_keypair = protocol_pk.map(|pk| { - self.find_key_by_pkh(&PublicKeyHash::from(&pk)) - .ok() - .or_else(|| { - self.store - .validator_data - .take() - .map(|data| data.keys.protocol_keypair) - }) - .ok_or(FindKeyError::KeyNotFound) - }); - match protocol_keypair { - Some(Err(err)) => Err(err), - other => Ok(Store::gen_validator_keys( - other.map(|res| res.unwrap()), - scheme, - )), - } - } - - /// Add validator data to the store - pub fn add_validator_data( - &mut self, - address: Address, - keys: ValidatorKeys, - ) { - self.store.add_validator_data(address, keys); - } - - /// Returns the validator data, if it exists. - pub fn get_validator_data(&self) -> Option<&ValidatorData> { - self.store.get_validator_data() - } - - /// Returns the validator data, if it exists. - /// [`Wallet::save`] cannot be called after using this - /// method as it involves a partial move - pub fn take_validator_data(self) -> Option { - self.store.validator_data() - } - - /// Find the stored key by an alias, a public key hash or a public key. - /// If the key is encrypted, will prompt for password from stdin. - /// Any keys that are decrypted are stored in and read from a cache to avoid - /// prompting for password multiple times. - pub fn find_key( - &mut self, - alias_pkh_or_pk: impl AsRef, - ) -> Result { - // Try cache first - if let Some(cached_key) = self - .decrypted_key_cache - .get(&alias_pkh_or_pk.as_ref().into()) - { - return Ok(cached_key.clone()); - } - // If not cached, look-up in store - let stored_key = self - .store - .find_key(alias_pkh_or_pk.as_ref()) - .ok_or(FindKeyError::KeyNotFound)?; - Self::decrypt_stored_key( - &mut self.decrypted_key_cache, - stored_key, - alias_pkh_or_pk.into(), - ) - } - - pub fn find_spending_key( - &mut self, - alias: impl AsRef, - ) -> Result { - // Try cache first - if let Some(cached_key) = - self.decrypted_spendkey_cache.get(&alias.as_ref().into()) - { - return Ok(*cached_key); - } - // If not cached, look-up in store - let stored_spendkey = self - .store - .find_spending_key(alias.as_ref()) - .ok_or(FindKeyError::KeyNotFound)?; - Self::decrypt_stored_key( - &mut self.decrypted_spendkey_cache, - stored_spendkey, - alias.into(), - ) - } - - pub fn find_viewing_key( - &mut self, - alias: impl AsRef, - ) -> Result<&ExtendedViewingKey, FindKeyError> { - self.store - .find_viewing_key(alias.as_ref()) - .ok_or(FindKeyError::KeyNotFound) - } - - pub fn find_payment_addr( - &self, - alias: impl AsRef, - ) -> Option<&PaymentAddress> { - self.store.find_payment_addr(alias.as_ref()) - } - - /// Find the stored key by a public key. - /// If the key is encrypted, will prompt for password from stdin. - /// Any keys that are decrypted are stored in and read from a cache to avoid - /// prompting for password multiple times. - pub fn find_key_by_pk( - &mut self, - pk: &common::PublicKey, - ) -> Result { - // Try to look-up alias for the given pk. Otherwise, use the PKH string. - let pkh: PublicKeyHash = pk.into(); - let alias = self - .store - .find_alias_by_pkh(&pkh) - .unwrap_or_else(|| pkh.to_string().into()); - // Try read cache - if let Some(cached_key) = self.decrypted_key_cache.get(&alias) { - return Ok(cached_key.clone()); - } - // Look-up from store - let stored_key = self - .store - .find_key_by_pk(pk) - .ok_or(FindKeyError::KeyNotFound)?; - Self::decrypt_stored_key( - &mut self.decrypted_key_cache, - stored_key, - alias, - ) - } - - /// Find the stored key by a public key hash. - /// If the key is encrypted, will prompt for password from stdin. - /// Any keys that are decrypted are stored in and read from a cache to avoid - /// prompting for password multiple times. - pub fn find_key_by_pkh( - &mut self, - pkh: &PublicKeyHash, - ) -> Result { - // Try to look-up alias for the given pk. Otherwise, use the PKH string. - let alias = self - .store - .find_alias_by_pkh(pkh) - .unwrap_or_else(|| pkh.to_string().into()); - // Try read cache - if let Some(cached_key) = self.decrypted_key_cache.get(&alias) { - return Ok(cached_key.clone()); - } - // Look-up from store - let stored_key = self - .store - .find_key_by_pkh(pkh) - .ok_or(FindKeyError::KeyNotFound)?; - Self::decrypt_stored_key( - &mut self.decrypted_key_cache, - stored_key, - alias, - ) - } - - /// Decrypt stored key, if it's not stored un-encrypted. - /// If a given storage key needs to be decrypted, prompt for password from - /// stdin and if successfully decrypted, store it in a cache. - fn decrypt_stored_key< - T: FromStr + Display + BorshSerialize + BorshDeserialize + Clone, - >( - decrypted_key_cache: &mut HashMap, - stored_key: &StoredKeypair, - alias: Alias, - ) -> Result - where - ::Err: Display, - { - match stored_key { - StoredKeypair::Encrypted(encrypted) => { - let password = read_password("Enter decryption password: "); - let key = encrypted - .decrypt(password) - .map_err(FindKeyError::KeyDecryptionError)?; - decrypted_key_cache.insert(alias.clone(), key); - decrypted_key_cache - .get(&alias) - .cloned() - .ok_or(FindKeyError::KeyNotFound) + pwd + } + + /// Read an alias from the file/env/stdin. + fn read_alias(prompt_msg: &str) -> String { + print!("Choose an alias for {}: ", prompt_msg); + io::stdout().flush().unwrap(); + let mut alias = String::new(); + io::stdin().read_line(&mut alias).unwrap(); + alias.trim().to_owned() + } + + /// The given alias has been selected but conflicts with another alias in + /// the store. Offer the user to either replace existing mapping, alter the + /// chosen alias to a name of their chosing, or cancel the aliasing. + fn show_overwrite_confirmation( + alias: &Alias, + alias_for: &str, + ) -> ConfirmationResponse { + print!( + "You're trying to create an alias \"{}\" that already exists for \ + {} in your store.\nWould you like to replace it? \ + s(k)ip/re(p)lace/re(s)elect: ", + alias, alias_for + ); + io::stdout().flush().unwrap(); + + let mut buffer = String::new(); + // Get the user to select between 3 choices + match io::stdin().read_line(&mut buffer) { + Ok(size) if size > 0 => { + // Isolate the single character representing the choice + let byte = buffer.chars().next().unwrap(); + buffer.clear(); + match byte { + 'p' | 'P' => return ConfirmationResponse::Replace, + 's' | 'S' => { + // In the case of reselection, elicit new alias + print!("Please enter a different alias: "); + io::stdout().flush().unwrap(); + if io::stdin().read_line(&mut buffer).is_ok() { + return ConfirmationResponse::Reselect( + buffer.trim().into(), + ); + } + } + 'k' | 'K' => return ConfirmationResponse::Skip, + // Input is senseless fall through to repeat prompt + _ => {} + }; } - StoredKeypair::Raw(raw) => Ok(raw.clone()), + _ => {} } + // Input is senseless fall through to repeat prompt + println!("Invalid option, try again."); + Self::show_overwrite_confirmation(alias, alias_for) } +} - /// Get all known keys by their alias, paired with PKH, if known. - pub fn get_keys( - &self, - ) -> HashMap< - String, - (&StoredKeypair, Option<&PublicKeyHash>), - > { - self.store - .get_keys() - .into_iter() - .map(|(alias, value)| (alias.into(), value)) - .collect() - } - - /// Find the stored address by an alias. - pub fn find_address(&self, alias: impl AsRef) -> Option<&Address> { - self.store.find_address(alias) - } - - /// Find an alias by the address if it's in the wallet. - pub fn find_alias(&self, address: &Address) -> Option<&Alias> { - self.store.find_alias(address) - } - - /// Get all known addresses by their alias, paired with PKH, if known. - pub fn get_addresses(&self) -> HashMap { - self.store - .get_addresses() - .iter() - .map(|(alias, value)| (alias.into(), value.clone())) - .collect() - } - - /// Get all known payment addresses by their alias - pub fn get_payment_addrs(&self) -> HashMap { - self.store - .get_payment_addrs() - .iter() - .map(|(alias, value)| (alias.into(), *value)) - .collect() - } - - /// Get all known viewing keys by their alias - pub fn get_viewing_keys(&self) -> HashMap { - self.store - .get_viewing_keys() - .iter() - .map(|(alias, value)| (alias.into(), *value)) - .collect() - } - - /// Get all known viewing keys by their alias - pub fn get_spending_keys( - &self, - ) -> HashMap> { - self.store - .get_spending_keys() - .iter() - .map(|(alias, value)| (alias.into(), value)) - .collect() - } - - /// Add a new address with the given alias. If the alias is already used, - /// will ask whether the existing alias should be replaced, a different - /// alias is desired, or the alias creation should be cancelled. Return - /// the chosen alias if the address has been added, otherwise return - /// nothing. - pub fn add_address( - &mut self, - alias: impl AsRef, - address: Address, - ) -> Option { - self.store - .insert_address(alias.into(), address) - .map(Into::into) - } - - /// Insert a new key with the given alias. If the alias is already used, - /// will prompt for overwrite confirmation. - pub fn insert_keypair( - &mut self, - alias: String, - keypair: StoredKeypair, - pkh: PublicKeyHash, - ) -> Option { - self.store - .insert_keypair(alias.into(), keypair, pkh) - .map(Into::into) +/// Generate keypair +/// for signing protocol txs and for the DKG (which will also be stored) +/// A protocol keypair may be optionally provided, indicating that +/// we should re-use a keypair already in the wallet +pub fn gen_validator_keys( + wallet: &mut Wallet, + protocol_pk: Option, + scheme: SchemeType, +) -> Result { + let protocol_keypair = protocol_pk.map(|pk| { + wallet + .find_key_by_pkh(&PublicKeyHash::from(&pk), None) + .ok() + .or_else(|| { + wallet + .store_mut() + .validator_data() + .take() + .map(|data| data.keys.protocol_keypair.clone()) + }) + .ok_or(FindKeyError::KeyNotFound) + }); + match protocol_keypair { + Some(Err(err)) => Err(err), + other => Ok(store::gen_validator_keys( + other.map(|res| res.unwrap()), + scheme, + )), } +} - pub fn insert_viewing_key( - &mut self, - alias: String, - view_key: ExtendedViewingKey, - ) -> Option { - self.store - .insert_viewing_key(alias.into(), view_key) - .map(Into::into) +/// Add addresses from a genesis configuration. +pub fn add_genesis_addresses( + wallet: &mut Wallet, + genesis: GenesisConfig, +) { + for (alias, addr) in defaults::addresses_from_genesis(genesis) { + wallet.add_address(alias.normalize(), addr); } +} - pub fn insert_spending_key( - &mut self, - alias: String, - spend_key: StoredKeypair, - viewkey: ExtendedViewingKey, - ) -> Option { - self.store - .insert_spending_key(alias.into(), spend_key, viewkey) - .map(Into::into) - } +/// Save the wallet store to a file. +pub fn save(wallet: &Wallet) -> std::io::Result<()> { + self::store::save(wallet.store(), wallet.store_dir()) +} - pub fn encrypt_insert_spending_key( - &mut self, - alias: String, - spend_key: ExtendedSpendingKey, - unsafe_dont_encrypt: bool, - ) -> Option { - let password = Self::new_password_prompt(unsafe_dont_encrypt); - self.store - .insert_spending_key( - alias.into(), - StoredKeypair::new(spend_key, password).0, - ExtendedFullViewingKey::from(&spend_key.into()).into(), - ) - .map(Into::into) - } +/// Load a wallet from the store file. +pub fn load(store_dir: &Path) -> Option> { + let store = self::store::load(store_dir).unwrap_or_else(|err| { + eprintln!("Unable to load the wallet: {}", err); + cli::safe_exit(1) + }); + Some(Wallet::::new( + store_dir.to_path_buf(), + store, + )) +} - pub fn insert_payment_addr( - &mut self, - alias: String, - payment_addr: PaymentAddress, - ) -> Option { - self.store - .insert_payment_addr(alias.into(), payment_addr) - .map(Into::into) - } +/// Load a wallet from the store file or create a new wallet without any +/// keys or addresses. +pub fn load_or_new(store_dir: &Path) -> Wallet { + let store = self::store::load_or_new(store_dir).unwrap_or_else(|err| { + eprintln!("Unable to load the wallet: {}", err); + cli::safe_exit(1) + }); + Wallet::::new(store_dir.to_path_buf(), store) +} - /// Extend this wallet from pre-genesis validator wallet. - pub fn extend_from_pre_genesis_validator( - &mut self, - validator_address: Address, - validator_alias: Alias, - other: pre_genesis::ValidatorWallet, - ) { - self.store.extend_from_pre_genesis_validator( - validator_address, - validator_alias, - other, - ) - } +/// Load a wallet from the store file or create a new one with the default +/// addresses loaded from the genesis file, if not found. +pub fn load_or_new_from_genesis( + store_dir: &Path, + genesis_cfg: GenesisConfig, +) -> Wallet { + let store = self::store::load_or_new_from_genesis(store_dir, genesis_cfg) + .unwrap_or_else(|err| { + eprintln!("Unable to load the wallet: {}", err); + cli::safe_exit(1) + }); + Wallet::::new(store_dir.to_path_buf(), store) } -/// Read the password for encryption from the file/env/stdin with confirmation. +/// Read the password for encryption from the file/env/stdin with +/// confirmation. pub fn read_and_confirm_pwd(unsafe_dont_encrypt: bool) -> Option { let password = if unsafe_dont_encrypt { println!("Warning: The keypair will NOT be encrypted."); None } else { - Some(read_password("Enter your encryption password: ")) + Some(CliWalletUtils::read_password( + "Enter your encryption password: ", + )) }; // Bis repetita for confirmation. let to_confirm = if unsafe_dont_encrypt { None } else { - Some(read_password( + Some(CliWalletUtils::read_password( "To confirm, please enter the same encryption password once more: ", )) }; @@ -535,22 +208,3 @@ pub fn read_and_confirm_pwd(unsafe_dont_encrypt: bool) -> Option { } password } - -/// Read the password for encryption/decryption from the file/env/stdin. Panics -/// if all options are empty/invalid. -pub fn read_password(prompt_msg: &str) -> String { - let pwd = match env::var("NAMADA_WALLET_PASSWORD_FILE") { - Ok(path) => fs::read_to_string(path) - .expect("Something went wrong reading the file"), - Err(_) => match env::var("NAMADA_WALLET_PASSWORD") { - Ok(password) => password, - Err(_) => rpassword::read_password_from_tty(Some(prompt_msg)) - .unwrap_or_default(), - }, - }; - if pwd.is_empty() { - eprintln!("Password cannot be empty"); - cli::safe_exit(1) - } - pwd -} diff --git a/apps/src/lib/wallet/pre_genesis.rs b/apps/src/lib/wallet/pre_genesis.rs index d3c5fa14c5..12209d5674 100644 --- a/apps/src/lib/wallet/pre_genesis.rs +++ b/apps/src/lib/wallet/pre_genesis.rs @@ -3,188 +3,125 @@ use std::path::{Path, PathBuf}; use ark_serialize::{Read, Write}; use file_lock::{FileLock, FileOptions}; -use namada::types::key::{common, SchemeType}; -use serde::{Deserialize, Serialize}; -use thiserror::Error; +use namada::ledger::wallet::pre_genesis::{ + ReadError, ValidatorStore, ValidatorWallet, +}; +use namada::ledger::wallet::{gen_key_to_store, WalletUtils}; +use namada::types::key::SchemeType; -use crate::wallet; -use crate::wallet::{store, StoredKeypair}; +use crate::wallet::store::gen_validator_keys; +use crate::wallet::{read_and_confirm_pwd, CliWalletUtils}; /// Validator pre-genesis wallet file name const VALIDATOR_FILE_NAME: &str = "wallet.toml"; -#[derive(Error, Debug)] -pub enum ReadError { - #[error("Failed decoding the wallet store: {0}")] - Decode(toml::de::Error), - #[error("Failed to read the wallet store from {0}: {1}")] - ReadWallet(String, String), - #[error("Failed to write the wallet store: {0}")] - StoreNewWallet(String), - #[error("Failed to decode a key: {0}")] - Decryption(wallet::keys::DecryptionError), -} - /// Get the path to the validator pre-genesis wallet store. pub fn validator_file_name(store_dir: impl AsRef) -> PathBuf { store_dir.as_ref().join(VALIDATOR_FILE_NAME) } -/// Validator pre-genesis wallet includes all the required keys for genesis -/// setup and a cache of decrypted keys. -pub struct ValidatorWallet { - /// The wallet store that can be written/read to/from TOML - pub store: ValidatorStore, - /// Cryptographic keypair for validator account key - pub account_key: common::SecretKey, - /// Cryptographic keypair for consensus key - pub consensus_key: common::SecretKey, - /// Cryptographic keypair for Tendermint node key - pub tendermint_node_key: common::SecretKey, -} - -/// Validator pre-genesis wallet store includes all the required keys for -/// genesis setup. -#[derive(Serialize, Deserialize, Debug)] -pub struct ValidatorStore { - /// Cryptographic keypair for validator account key - pub account_key: wallet::StoredKeypair, - /// Cryptographic keypair for consensus key - pub consensus_key: wallet::StoredKeypair, - /// Cryptographic keypair for Tendermint node key - pub tendermint_node_key: wallet::StoredKeypair, - /// Special validator keys - pub validator_keys: wallet::ValidatorKeys, +/// Generate a new [`ValidatorWallet`] with required pre-genesis keys and +/// store it as TOML at the given path. +pub fn gen_and_store( + scheme: SchemeType, + unsafe_dont_encrypt: bool, + store_dir: &Path, +) -> std::io::Result { + let password = read_and_confirm_pwd(unsafe_dont_encrypt); + let validator = gen(scheme, password); + let data = validator.store.encode(); + let wallet_path = validator_file_name(store_dir); + // Make sure the dir exists + let wallet_dir = wallet_path.parent().unwrap(); + fs::create_dir_all(wallet_dir)?; + // Write the file + let options = FileOptions::new().create(true).write(true).truncate(true); + let mut filelock = + FileLock::lock(wallet_path.to_str().unwrap(), true, options)?; + filelock.file.write_all(&data)?; + Ok(validator) } -impl ValidatorWallet { - /// Generate a new [`ValidatorWallet`] with required pre-genesis keys and - /// store it as TOML at the given path. - pub fn gen_and_store( - scheme: SchemeType, - unsafe_dont_encrypt: bool, - store_dir: &Path, - ) -> std::io::Result { - let validator = Self::gen(scheme, unsafe_dont_encrypt); - let data = validator.store.encode(); - let wallet_path = validator_file_name(store_dir); - // Make sure the dir exists - let wallet_dir = wallet_path.parent().unwrap(); - fs::create_dir_all(wallet_dir)?; - // Write the file - let options = - FileOptions::new().create(true).write(true).truncate(true); - let mut filelock = - FileLock::lock(wallet_path.to_str().unwrap(), true, options)?; - filelock.file.write_all(&data)?; - Ok(validator) - } - - /// Try to load and decrypt keys, if encrypted, in a [`ValidatorWallet`] - /// from a TOML file. - pub fn load(store_dir: &Path) -> Result { - let wallet_file = validator_file_name(store_dir); - match FileLock::lock( - wallet_file.to_str().unwrap(), - true, - FileOptions::new().read(true).write(false), - ) { - Ok(mut filelock) => { - let mut store = Vec::::new(); - filelock.file.read_to_end(&mut store).map_err(|err| { - ReadError::ReadWallet( - store_dir.to_str().unwrap().into(), - err.to_string(), - ) - })?; - let store = - ValidatorStore::decode(store).map_err(ReadError::Decode)?; - - let password = if store.account_key.is_encrypted() - || store.consensus_key.is_encrypted() - || store.account_key.is_encrypted() - { - Some(wallet::read_password("Enter decryption password: ")) - } else { - None - }; - - let account_key = - store.account_key.get(true, password.clone())?; - let consensus_key = - store.consensus_key.get(true, password.clone())?; - let tendermint_node_key = - store.tendermint_node_key.get(true, password)?; - - Ok(Self { - store, - account_key, - consensus_key, - tendermint_node_key, - }) - } - Err(err) => Err(ReadError::ReadWallet( - wallet_file.to_string_lossy().into_owned(), - err.to_string(), - )), +/// Try to load and decrypt keys, if encrypted, in a [`ValidatorWallet`] +/// from a TOML file. +pub fn load(store_dir: &Path) -> Result { + let wallet_file = validator_file_name(store_dir); + match FileLock::lock( + wallet_file.to_str().unwrap(), + true, + FileOptions::new().read(true).write(false), + ) { + Ok(mut filelock) => { + let mut store = Vec::::new(); + filelock.file.read_to_end(&mut store).map_err(|err| { + ReadError::ReadWallet( + store_dir.to_str().unwrap().into(), + err.to_string(), + ) + })?; + let store = + ValidatorStore::decode(store).map_err(ReadError::Decode)?; + + let password = if store.account_key.is_encrypted() + || store.consensus_key.is_encrypted() + || store.account_key.is_encrypted() + { + Some(CliWalletUtils::read_password( + "Enter decryption password: ", + )) + } else { + None + }; + + let account_key = store + .account_key + .get::(true, password.clone())?; + let consensus_key = store + .consensus_key + .get::(true, password.clone())?; + let tendermint_node_key = store + .tendermint_node_key + .get::(true, password)?; + + Ok(ValidatorWallet { + store, + account_key, + consensus_key, + tendermint_node_key, + }) } + Err(err) => Err(ReadError::ReadWallet( + wallet_file.to_string_lossy().into_owned(), + err.to_string(), + )), } - - /// Generate a new [`ValidatorWallet`] with required pre-genesis keys. Will - /// prompt for password when `!unsafe_dont_encrypt`. - fn gen(scheme: SchemeType, unsafe_dont_encrypt: bool) -> Self { - let password = wallet::read_and_confirm_pwd(unsafe_dont_encrypt); - let (account_key, account_sk) = gen_key_to_store(scheme, &password); - let (consensus_key, consensus_sk) = gen_key_to_store( - // Note that TM only allows ed25519 for consensus key - SchemeType::Ed25519, - &password, - ); - let (tendermint_node_key, tendermint_node_sk) = gen_key_to_store( - // Note that TM only allows ed25519 for node IDs - SchemeType::Ed25519, - &password, - ); - let validator_keys = store::Store::gen_validator_keys(None, scheme); - let store = ValidatorStore { - account_key, - consensus_key, - tendermint_node_key, - validator_keys, - }; - Self { - store, - account_key: account_sk, - consensus_key: consensus_sk, - tendermint_node_key: tendermint_node_sk, - } - } -} - -impl ValidatorStore { - /// Decode from TOML string bytes - pub fn decode(data: Vec) -> Result { - toml::from_slice(&data) - } - - /// Encode in TOML string bytes - pub fn encode(&self) -> Vec { - toml::to_vec(self).expect( - "Serializing of validator pre-genesis wallet shouldn't fail", - ) - } -} - -fn gen_key_to_store( - scheme: SchemeType, - password: &Option, -) -> (StoredKeypair, common::SecretKey) { - let sk = store::gen_sk(scheme); - StoredKeypair::new(sk, password.clone()) } -impl From for ReadError { - fn from(err: wallet::keys::DecryptionError) -> Self { - ReadError::Decryption(err) +/// Generate a new [`ValidatorWallet`] with required pre-genesis keys. Will +/// prompt for password when `!unsafe_dont_encrypt`. +fn gen(scheme: SchemeType, password: Option) -> ValidatorWallet { + let (account_key, account_sk) = gen_key_to_store(scheme, &password); + let (consensus_key, consensus_sk) = gen_key_to_store( + // Note that TM only allows ed25519 for consensus key + SchemeType::Ed25519, + &password, + ); + let (tendermint_node_key, tendermint_node_sk) = gen_key_to_store( + // Note that TM only allows ed25519 for node IDs + SchemeType::Ed25519, + &password, + ); + let validator_keys = gen_validator_keys(None, scheme); + let store = ValidatorStore { + account_key, + consensus_key, + tendermint_node_key, + validator_keys, + }; + ValidatorWallet { + store, + account_key: account_sk, + consensus_key: consensus_sk, + tendermint_node_key: tendermint_node_sk, } } diff --git a/apps/src/lib/wallet/store.rs b/apps/src/lib/wallet/store.rs index aa1ae1ca88..d43b2c283a 100644 --- a/apps/src/lib/wallet/store.rs +++ b/apps/src/lib/wallet/store.rs @@ -1,75 +1,20 @@ -use std::collections::HashMap; use std::fs; use std::io::prelude::*; -use std::io::{self, Write}; +use std::io::Write; use std::path::{Path, PathBuf}; -use std::str::FromStr; use ark_std::rand::prelude::*; use ark_std::rand::SeedableRng; -use bimap::BiHashMap; use file_lock::{FileLock, FileOptions}; -use masp_primitives::zip32::ExtendedFullViewingKey; -use namada::types::address::{Address, ImplicitAddress}; -use namada::types::key::dkg_session_keys::DkgKeypair; +#[cfg(feature = "dev")] +use namada::ledger::wallet::StoredKeypair; +use namada::ledger::wallet::{gen_sk, Store, ValidatorKeys}; use namada::types::key::*; -use namada::types::masp::{ - ExtendedSpendingKey, ExtendedViewingKey, PaymentAddress, -}; use namada::types::transaction::EllipticCurve; -use serde::{Deserialize, Serialize}; use thiserror::Error; -use super::alias::{self, Alias}; -use super::keys::StoredKeypair; -use super::pre_genesis; -use crate::cli; use crate::config::genesis::genesis_config::GenesisConfig; - -/// Special keys for a validator -#[derive(Serialize, Deserialize, Debug)] -pub struct ValidatorKeys { - /// Special keypair for signing protocol txs - pub protocol_keypair: common::SecretKey, - /// Special session keypair needed by validators for participating - /// in the DKG protocol - pub dkg_keypair: Option, -} - -impl ValidatorKeys { - /// Get the protocol keypair - pub fn get_protocol_keypair(&self) -> &common::SecretKey { - &self.protocol_keypair - } -} - -/// Special data associated with a validator -#[derive(Serialize, Deserialize, Debug)] -pub struct ValidatorData { - /// The address associated to a validator - pub address: Address, - /// special keys for a validator - pub keys: ValidatorKeys, -} - -#[derive(Serialize, Deserialize, Debug, Default)] -pub struct Store { - /// Known viewing keys - view_keys: HashMap, - /// Known spending keys - spend_keys: HashMap>, - /// Known payment addresses - payment_addrs: HashMap, - /// Cryptographic keypairs - keys: HashMap>, - /// Namada address book - addresses: BiHashMap, - /// Known mappings of public key hashes to their aliases in the `keys` - /// field. Used for look-up by a public key. - pkhs: HashMap, - /// Special keys if the wallet belongs to a validator - pub(crate) validator_data: Option, -} +use crate::wallet::CliWalletUtils; #[derive(Error, Debug)] pub enum LoadStoreError { @@ -81,690 +26,146 @@ pub enum LoadStoreError { StoreNewWallet(String), } -impl Store { - #[cfg(not(feature = "dev"))] - fn new(genesis: GenesisConfig) -> Self { - let mut store = Self::default(); - store.add_genesis_addresses(genesis); - store - } +/// Wallet file name +const FILE_NAME: &str = "wallet.toml"; - #[cfg(feature = "dev")] - fn new() -> Self { - let mut store = Self::default(); - // Pre-load the default keys without encryption - let no_password = None; - for (alias, keypair) in super::defaults::keys() { - let pkh: PublicKeyHash = (&keypair.ref_to()).into(); - store.keys.insert( - alias.clone(), - StoredKeypair::new(keypair, no_password.clone()).0, - ); - store.pkhs.insert(pkh, alias); - } - store - .addresses - .extend(super::defaults::addresses().into_iter()); - store - } +/// Get the path to the wallet store. +pub fn wallet_file(store_dir: impl AsRef) -> PathBuf { + store_dir.as_ref().join(FILE_NAME) +} - /// Add addresses from a genesis configuration. - pub fn add_genesis_addresses(&mut self, genesis: GenesisConfig) { - self.addresses.extend( - super::defaults::addresses_from_genesis(genesis).into_iter(), - ); - } +/// Save the wallet store to a file. +pub fn save(store: &Store, store_dir: &Path) -> std::io::Result<()> { + let data = store.encode(); + let wallet_path = wallet_file(store_dir); + // Make sure the dir exists + let wallet_dir = wallet_path.parent().unwrap(); + fs::create_dir_all(wallet_dir)?; + // Write the file + let options = FileOptions::new().create(true).write(true).truncate(true); + let mut filelock = + FileLock::lock(wallet_path.to_str().unwrap(), true, options)?; + filelock.file.write_all(&data) +} - /// Save the wallet store to a file. - pub fn save(&self, store_dir: &Path) -> std::io::Result<()> { - let data = self.encode(); - let wallet_path = wallet_file(store_dir); - // Make sure the dir exists - let wallet_dir = wallet_path.parent().unwrap(); - fs::create_dir_all(wallet_dir)?; - // Write the file - let options = - FileOptions::new().create(true).write(true).truncate(true); - let mut filelock = - FileLock::lock(wallet_path.to_str().unwrap(), true, options)?; - filelock.file.write_all(&data) - } +/// Load the store file or create a new one without any keys or addresses. +pub fn load_or_new(store_dir: &Path) -> Result { + load(store_dir).or_else(|_| { + let store = Store::default(); + save(&store, store_dir) + .map_err(|err| LoadStoreError::StoreNewWallet(err.to_string()))?; + Ok(store) + }) +} - /// Load the store file or create a new one without any keys or addresses. - pub fn load_or_new(store_dir: &Path) -> Result { - Self::load(store_dir).or_else(|_| { - let store = Self::default(); - store.save(store_dir).map_err(|err| { - LoadStoreError::StoreNewWallet(err.to_string()) - })?; - Ok(store) - }) - } +/// Load the store file or create a new one with the default addresses from +/// the genesis file, if not found. +pub fn load_or_new_from_genesis( + store_dir: &Path, + genesis_cfg: GenesisConfig, +) -> Result { + load(store_dir).or_else(|_| { + #[cfg(not(feature = "dev"))] + let store = new(genesis_cfg); + #[cfg(feature = "dev")] + let store = { + // The function is unused in dev + let _ = genesis_cfg; + new() + }; + save(&store, store_dir) + .map_err(|err| LoadStoreError::StoreNewWallet(err.to_string()))?; + Ok(store) + }) +} - /// Load the store file or create a new one with the default addresses from - /// the genesis file, if not found. - pub fn load_or_new_from_genesis( - store_dir: &Path, - genesis_cfg: GenesisConfig, - ) -> Result { - Self::load(store_dir).or_else(|_| { - #[cfg(not(feature = "dev"))] - let store = Self::new(genesis_cfg); - #[cfg(feature = "dev")] - let store = { - // The function is unused in dev - let _ = genesis_cfg; - Self::new() - }; - store.save(store_dir).map_err(|err| { - LoadStoreError::StoreNewWallet(err.to_string()) +/// Attempt to load the store file. +pub fn load(store_dir: &Path) -> Result { + let wallet_file = wallet_file(store_dir); + match FileLock::lock( + wallet_file.to_str().unwrap(), + true, + FileOptions::new().read(true).write(false), + ) { + Ok(mut filelock) => { + let mut store = Vec::::new(); + filelock.file.read_to_end(&mut store).map_err(|err| { + LoadStoreError::ReadWallet( + store_dir.to_str().unwrap().into(), + err.to_string(), + ) })?; - Ok(store) - }) - } - - /// Attempt to load the store file. - pub fn load(store_dir: &Path) -> Result { - let wallet_file = wallet_file(store_dir); - match FileLock::lock( - wallet_file.to_str().unwrap(), - true, - FileOptions::new().read(true).write(false), - ) { - Ok(mut filelock) => { - let mut store = Vec::::new(); - filelock.file.read_to_end(&mut store).map_err(|err| { - LoadStoreError::ReadWallet( - store_dir.to_str().unwrap().into(), - err.to_string(), - ) - })?; - Store::decode(store).map_err(LoadStoreError::Decode) - } - Err(err) => Err(LoadStoreError::ReadWallet( - wallet_file.to_string_lossy().into_owned(), - err.to_string(), - )), + Store::decode(store).map_err(LoadStoreError::Decode) } + Err(err) => Err(LoadStoreError::ReadWallet( + wallet_file.to_string_lossy().into_owned(), + err.to_string(), + )), } +} - /// Find the stored key by an alias, a public key hash or a public key. - pub fn find_key( - &self, - alias_pkh_or_pk: impl AsRef, - ) -> Option<&StoredKeypair> { - let alias_pkh_or_pk = alias_pkh_or_pk.as_ref(); - // Try to find by alias - self.keys - .get(&alias_pkh_or_pk.into()) - // Try to find by PKH - .or_else(|| { - let pkh = PublicKeyHash::from_str(alias_pkh_or_pk).ok()?; - self.find_key_by_pkh(&pkh) - }) - // Try to find by PK - .or_else(|| { - let pk = common::PublicKey::from_str(alias_pkh_or_pk).ok()?; - self.find_key_by_pk(&pk) - }) - } - - pub fn find_spending_key( - &self, - alias: impl AsRef, - ) -> Option<&StoredKeypair> { - self.spend_keys.get(&alias.into()) - } - - pub fn find_viewing_key( - &self, - alias: impl AsRef, - ) -> Option<&ExtendedViewingKey> { - self.view_keys.get(&alias.into()) - } - - pub fn find_payment_addr( - &self, - alias: impl AsRef, - ) -> Option<&PaymentAddress> { - self.payment_addrs.get(&alias.into()) - } - - /// Find the stored key by a public key. - pub fn find_key_by_pk( - &self, - pk: &common::PublicKey, - ) -> Option<&StoredKeypair> { - let pkh = PublicKeyHash::from(pk); - self.find_key_by_pkh(&pkh) - } - - /// Find the stored key by a public key hash. - pub fn find_key_by_pkh( - &self, - pkh: &PublicKeyHash, - ) -> Option<&StoredKeypair> { - let alias = self.pkhs.get(pkh)?; - self.keys.get(alias) - } - - /// Find the stored alias for a public key hash. - pub fn find_alias_by_pkh(&self, pkh: &PublicKeyHash) -> Option { - self.pkhs.get(pkh).cloned() - } - - /// Find the stored address by an alias. - pub fn find_address(&self, alias: impl AsRef) -> Option<&Address> { - self.addresses.get_by_left(&alias.into()) - } - - /// Find an alias by the address if it's in the wallet. - pub fn find_alias(&self, address: &Address) -> Option<&Alias> { - self.addresses.get_by_right(address) - } - - /// Get all known keys by their alias, paired with PKH, if known. - pub fn get_keys( - &self, - ) -> HashMap< - Alias, - (&StoredKeypair, Option<&PublicKeyHash>), - > { - let mut keys: HashMap< - Alias, - (&StoredKeypair, Option<&PublicKeyHash>), - > = self - .pkhs - .iter() - .filter_map(|(pkh, alias)| { - let key = &self.keys.get(alias)?; - Some((alias.clone(), (*key, Some(pkh)))) - }) - .collect(); - self.keys.iter().for_each(|(alias, key)| { - if !keys.contains_key(alias) { - keys.insert(alias.clone(), (key, None)); - } - }); - keys - } - - /// Get all known addresses by their alias, paired with PKH, if known. - pub fn get_addresses(&self) -> &BiHashMap { - &self.addresses - } - - /// Get all known payment addresses by their alias. - pub fn get_payment_addrs(&self) -> &HashMap { - &self.payment_addrs - } - - /// Get all known viewing keys by their alias. - pub fn get_viewing_keys(&self) -> &HashMap { - &self.view_keys - } - - /// Get all known spending keys by their alias. - pub fn get_spending_keys( - &self, - ) -> &HashMap> { - &self.spend_keys - } - - fn generate_spending_key() -> ExtendedSpendingKey { - use rand::rngs::OsRng; - let mut spend_key = [0; 32]; - OsRng.fill_bytes(&mut spend_key); - masp_primitives::zip32::ExtendedSpendingKey::master(spend_key.as_ref()) - .into() - } - - /// Generate a new keypair and insert it into the store with the provided - /// alias. If none provided, the alias will be the public key hash. - /// If no password is provided, the keypair will be stored raw without - /// encryption. Returns the alias of the key and a reference-counting - /// pointer to the key. - pub fn gen_key( - &mut self, - scheme: SchemeType, - alias: Option, - password: Option, - ) -> (Alias, common::SecretKey) { - let sk = gen_sk(scheme); - let pkh: PublicKeyHash = PublicKeyHash::from(&sk.ref_to()); - let (keypair_to_store, raw_keypair) = StoredKeypair::new(sk, password); - let address = Address::Implicit(ImplicitAddress(pkh.clone())); - let alias: Alias = alias.unwrap_or_else(|| pkh.clone().into()).into(); - if self - .insert_keypair(alias.clone(), keypair_to_store, pkh) - .is_none() - { - eprintln!("Action cancelled, no changes persisted."); - cli::safe_exit(1); - } - if self.insert_address(alias.clone(), address).is_none() { - eprintln!("Action cancelled, no changes persisted."); - cli::safe_exit(1); - } - (alias, raw_keypair) +/// Add addresses from a genesis configuration. +#[cfg(not(feature = "dev"))] +pub fn add_genesis_addresses(store: &mut Store, genesis: GenesisConfig) { + for (alias, addr) in super::defaults::addresses_from_genesis(genesis) { + store.insert_address::(alias, addr); } +} - /// Generate a spending key similarly to how it's done for keypairs - pub fn gen_spending_key( - &mut self, - alias: String, - password: Option, - ) -> (Alias, ExtendedSpendingKey) { - let spendkey = Self::generate_spending_key(); - let viewkey = ExtendedFullViewingKey::from(&spendkey.into()).into(); - let (spendkey_to_store, _raw_spendkey) = - StoredKeypair::new(spendkey, password); - let alias = Alias::from(alias); - if self - .insert_spending_key(alias.clone(), spendkey_to_store, viewkey) - .is_none() - { - eprintln!("Action cancelled, no changes persisted."); - cli::safe_exit(1); - } - (alias, spendkey) - } +#[cfg(not(feature = "dev"))] +fn new(genesis: GenesisConfig) -> Store { + let mut store = Store::default(); + add_genesis_addresses(&mut store, genesis); + store +} - /// Generate keypair for signing protocol txs and for the DKG - /// A protocol keypair may be optionally provided - /// - /// Note that this removes the validator data. - pub fn gen_validator_keys( - protocol_keypair: Option, - scheme: SchemeType, - ) -> ValidatorKeys { - let protocol_keypair = - protocol_keypair.unwrap_or_else(|| gen_sk(scheme)); - let dkg_keypair = ferveo_common::Keypair::::new( - &mut StdRng::from_entropy(), +#[cfg(feature = "dev")] +fn new() -> Store { + let mut store = Store::default(); + // Pre-load the default keys without encryption + let no_password = None; + for (alias, keypair) in super::defaults::keys() { + let pkh: PublicKeyHash = (&keypair.ref_to()).into(); + store.insert_keypair::( + alias, + StoredKeypair::new(keypair, no_password.clone()).0, + pkh, ); - ValidatorKeys { - protocol_keypair, - dkg_keypair: Some(dkg_keypair.into()), - } - } - - /// Add validator data to the store - pub fn add_validator_data( - &mut self, - address: Address, - keys: ValidatorKeys, - ) { - self.validator_data = Some(ValidatorData { address, keys }); - } - - /// Returns the validator data, if it exists - pub fn get_validator_data(&self) -> Option<&ValidatorData> { - self.validator_data.as_ref() - } - - /// Returns the validator data, if it exists - pub fn validator_data(self) -> Option { - self.validator_data - } - - /// Insert a new key with the given alias. If the alias is already used, - /// will prompt for overwrite/reselection confirmation. If declined, then - /// keypair is not inserted and nothing is returned, otherwise selected - /// alias is returned. - pub(super) fn insert_keypair( - &mut self, - alias: Alias, - keypair: StoredKeypair, - pkh: PublicKeyHash, - ) -> Option { - if alias.is_empty() { - println!( - "Empty alias given, defaulting to {}.", - Into::::into(pkh.to_string()) - ); - } - // Addresses and keypairs can share aliases, so first remove any - // addresses sharing the same namesake before checking if alias has been - // used. - let counterpart_address = self.addresses.remove_by_left(&alias); - if self.contains_alias(&alias) { - match show_overwrite_confirmation(&alias, "a key") { - ConfirmationResponse::Replace => {} - ConfirmationResponse::Reselect(new_alias) => { - // Restore the removed address in case the recursive prompt - // terminates with a cancellation - counterpart_address - .map(|x| self.addresses.insert(alias.clone(), x.1)); - return self.insert_keypair(new_alias, keypair, pkh); - } - ConfirmationResponse::Skip => { - // Restore the removed address since this insertion action - // has now been cancelled - counterpart_address - .map(|x| self.addresses.insert(alias.clone(), x.1)); - return None; - } - } - } - self.remove_alias(&alias); - self.keys.insert(alias.clone(), keypair); - self.pkhs.insert(pkh, alias.clone()); - // Since it is intended for the inserted keypair to share its namesake - // with the pre-existing address - counterpart_address.map(|x| self.addresses.insert(alias.clone(), x.1)); - Some(alias) - } - - /// Insert spending keys similarly to how it's done for keypairs - pub fn insert_spending_key( - &mut self, - alias: Alias, - spendkey: StoredKeypair, - viewkey: ExtendedViewingKey, - ) -> Option { - if alias.is_empty() { - eprintln!("Empty alias given."); - return None; - } - if self.contains_alias(&alias) { - match show_overwrite_confirmation(&alias, "a spending key") { - ConfirmationResponse::Replace => {} - ConfirmationResponse::Reselect(new_alias) => { - return self - .insert_spending_key(new_alias, spendkey, viewkey); - } - ConfirmationResponse::Skip => return None, - } - } - self.remove_alias(&alias); - self.spend_keys.insert(alias.clone(), spendkey); - // Simultaneously add the derived viewing key to ease balance viewing - self.view_keys.insert(alias.clone(), viewkey); - Some(alias) - } - - /// Insert viewing keys similarly to how it's done for keypairs - pub fn insert_viewing_key( - &mut self, - alias: Alias, - viewkey: ExtendedViewingKey, - ) -> Option { - if alias.is_empty() { - eprintln!("Empty alias given."); - return None; - } - if self.contains_alias(&alias) { - match show_overwrite_confirmation(&alias, "a viewing key") { - ConfirmationResponse::Replace => {} - ConfirmationResponse::Reselect(new_alias) => { - return self.insert_viewing_key(new_alias, viewkey); - } - ConfirmationResponse::Skip => return None, - } - } - self.remove_alias(&alias); - self.view_keys.insert(alias.clone(), viewkey); - Some(alias) - } - - /// Check if any map of the wallet contains the given alias - fn contains_alias(&self, alias: &Alias) -> bool { - self.payment_addrs.contains_key(alias) - || self.view_keys.contains_key(alias) - || self.spend_keys.contains_key(alias) - || self.keys.contains_key(alias) - || self.addresses.contains_left(alias) - } - - /// Completely remove the given alias from all maps in the wallet - fn remove_alias(&mut self, alias: &Alias) { - self.payment_addrs.remove(alias); - self.view_keys.remove(alias); - self.spend_keys.remove(alias); - self.keys.remove(alias); - self.addresses.remove_by_left(alias); - self.pkhs.retain(|_key, val| val != alias); - } - - /// Insert payment addresses similarly to how it's done for keypairs - pub fn insert_payment_addr( - &mut self, - alias: Alias, - payment_addr: PaymentAddress, - ) -> Option { - if alias.is_empty() { - eprintln!("Empty alias given."); - return None; - } - if self.contains_alias(&alias) { - match show_overwrite_confirmation(&alias, "a payment address") { - ConfirmationResponse::Replace => {} - ConfirmationResponse::Reselect(new_alias) => { - return self.insert_payment_addr(new_alias, payment_addr); - } - ConfirmationResponse::Skip => return None, - } - } - self.remove_alias(&alias); - self.payment_addrs.insert(alias.clone(), payment_addr); - Some(alias) - } - - /// Helper function to restore keypair given alias-keypair mapping and the - /// pkhs-alias mapping. - fn restore_keypair( - &mut self, - alias: Alias, - key: Option>, - pkh: Option, - ) { - key.map(|x| self.keys.insert(alias.clone(), x)); - pkh.map(|x| self.pkhs.insert(x, alias.clone())); - } - - /// Insert a new address with the given alias. If the alias is already used, - /// will prompt for overwrite/reselection confirmation, which when declined, - /// the address won't be added. Return the selected alias if the address has - /// been added. - pub fn insert_address( - &mut self, - alias: Alias, - address: Address, - ) -> Option { - if alias.is_empty() { - println!("Empty alias given, defaulting to {}.", address.encode()); - } - // Addresses and keypairs can share aliases, so first remove any keys - // sharing the same namesake before checking if alias has been used. - let counterpart_key = self.keys.remove(&alias); - let mut counterpart_pkh = None; - self.pkhs.retain(|k, v| { - if v == &alias { - counterpart_pkh = Some(k.clone()); - false - } else { - true - } - }); - if self.addresses.contains_left(&alias) { - match show_overwrite_confirmation(&alias, "an address") { - ConfirmationResponse::Replace => {} - ConfirmationResponse::Reselect(new_alias) => { - // Restore the removed keypair in case the recursive prompt - // terminates with a cancellation - self.restore_keypair( - alias, - counterpart_key, - counterpart_pkh, - ); - return self.insert_address(new_alias, address); - } - ConfirmationResponse::Skip => { - // Restore the removed keypair since this insertion action - // has now been cancelled - self.restore_keypair( - alias, - counterpart_key, - counterpart_pkh, - ); - return None; - } - } - } - self.remove_alias(&alias); - self.addresses.insert(alias.clone(), address); - // Since it is intended for the inserted address to share its namesake - // with the pre-existing keypair - self.restore_keypair(alias.clone(), counterpart_key, counterpart_pkh); - Some(alias) - } - - /// Extend this store from pre-genesis validator wallet. - pub fn extend_from_pre_genesis_validator( - &mut self, - validator_address: Address, - validator_alias: Alias, - other: pre_genesis::ValidatorWallet, - ) { - let account_key_alias = alias::validator_key(&validator_alias); - let consensus_key_alias = - alias::validator_consensus_key(&validator_alias); - let tendermint_node_key_alias = - alias::validator_tendermint_node_key(&validator_alias); - - let keys = [ - (account_key_alias.clone(), other.store.account_key), - (consensus_key_alias.clone(), other.store.consensus_key), - ( - tendermint_node_key_alias.clone(), - other.store.tendermint_node_key, - ), - ]; - self.keys.extend(keys.into_iter()); - - let account_pk = other.account_key.ref_to(); - let consensus_pk = other.consensus_key.ref_to(); - let tendermint_node_pk = other.tendermint_node_key.ref_to(); - let addresses = [ - (account_key_alias.clone(), (&account_pk).into()), - (consensus_key_alias.clone(), (&consensus_pk).into()), - ( - tendermint_node_key_alias.clone(), - (&tendermint_node_pk).into(), - ), - ]; - self.addresses.extend(addresses.into_iter()); - - let pkhs = [ - ((&account_pk).into(), account_key_alias), - ((&consensus_pk).into(), consensus_key_alias), - ((&tendermint_node_pk).into(), tendermint_node_key_alias), - ]; - self.pkhs.extend(pkhs.into_iter()); - - self.validator_data = Some(ValidatorData { - address: validator_address, - keys: other.store.validator_keys, - }); } - - fn decode(data: Vec) -> Result { - toml::from_slice(&data) - } - - fn encode(&self) -> Vec { - toml::to_vec(self).expect("Serializing of store shouldn't fail") + for (alias, addr) in super::defaults::addresses() { + store.insert_address::(alias, addr); } + store } -enum ConfirmationResponse { - Replace, - Reselect(Alias), - Skip, -} - -/// The given alias has been selected but conflicts with another alias in -/// the store. Offer the user to either replace existing mapping, alter the -/// chosen alias to a name of their chosing, or cancel the aliasing. - -fn show_overwrite_confirmation( - alias: &Alias, - alias_for: &str, -) -> ConfirmationResponse { - print!( - "You're trying to create an alias \"{}\" that already exists for {} \ - in your store.\nWould you like to replace it? \ - s(k)ip/re(p)lace/re(s)elect: ", - alias, alias_for +/// Generate keypair for signing protocol txs and for the DKG +/// A protocol keypair may be optionally provided +/// +/// Note that this removes the validator data. +pub fn gen_validator_keys( + protocol_keypair: Option, + scheme: SchemeType, +) -> ValidatorKeys { + let protocol_keypair = protocol_keypair.unwrap_or_else(|| gen_sk(scheme)); + let dkg_keypair = ferveo_common::Keypair::::new( + &mut StdRng::from_entropy(), ); - io::stdout().flush().unwrap(); - - let mut buffer = String::new(); - // Get the user to select between 3 choices - match io::stdin().read_line(&mut buffer) { - Ok(size) if size > 0 => { - // Isolate the single character representing the choice - let byte = buffer.chars().next().unwrap(); - buffer.clear(); - match byte { - 'p' | 'P' => return ConfirmationResponse::Replace, - 's' | 'S' => { - // In the case of reselection, elicit new alias - print!("Please enter a different alias: "); - io::stdout().flush().unwrap(); - if io::stdin().read_line(&mut buffer).is_ok() { - return ConfirmationResponse::Reselect( - buffer.trim().into(), - ); - } - } - 'k' | 'K' => return ConfirmationResponse::Skip, - // Input is senseless fall through to repeat prompt - _ => {} - }; - } - _ => {} - } - // Input is senseless fall through to repeat prompt - println!("Invalid option, try again."); - show_overwrite_confirmation(alias, alias_for) -} - -/// Wallet file name -const FILE_NAME: &str = "wallet.toml"; - -/// Get the path to the wallet store. -pub fn wallet_file(store_dir: impl AsRef) -> PathBuf { - store_dir.as_ref().join(FILE_NAME) -} - -/// Generate a new secret key. -pub fn gen_sk(scheme: SchemeType) -> common::SecretKey { - use rand::rngs::OsRng; - let mut csprng = OsRng {}; - match scheme { - SchemeType::Ed25519 => ed25519::SigScheme::generate(&mut csprng) - .try_to_sk() - .unwrap(), - SchemeType::Secp256k1 => secp256k1::SigScheme::generate(&mut csprng) - .try_to_sk() - .unwrap(), - SchemeType::Common => common::SigScheme::generate(&mut csprng) - .try_to_sk() - .unwrap(), + ValidatorKeys { + protocol_keypair, + dkg_keypair: Some(dkg_keypair.into()), } } #[cfg(all(test, feature = "dev"))] mod test_wallet { + use namada::types::address::Address; + use super::*; #[test] fn test_toml_roundtrip_ed25519() { - let mut store = Store::new(); - let validator_keys = - Store::gen_validator_keys(None, SchemeType::Ed25519); + let mut store = new(); + let validator_keys = gen_validator_keys(None, SchemeType::Ed25519); store.add_validator_data( Address::decode("atest1v4ehgw36x3prswzxggunzv6pxqmnvdj9xvcyzvpsggeyvs3cg9qnywf589qnwvfsg5erg3fkl09rg5").unwrap(), validator_keys @@ -775,9 +176,8 @@ mod test_wallet { #[test] fn test_toml_roundtrip_secp256k1() { - let mut store = Store::new(); - let validator_keys = - Store::gen_validator_keys(None, SchemeType::Secp256k1); + let mut store = new(); + let validator_keys = gen_validator_keys(None, SchemeType::Secp256k1); store.add_validator_data( Address::decode("atest1v4ehgw36x3prswzxggunzv6pxqmnvdj9xvcyzvpsggeyvs3cg9qnywf589qnwvfsg5erg3fkl09rg5").unwrap(), validator_keys diff --git a/shared/Cargo.toml b/shared/Cargo.toml index efcd0e81a0..5ea9249699 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -9,7 +9,7 @@ version = "0.12.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["abciplus"] +default = ["abciplus", "namada-sdk"] # NOTE "dev" features that shouldn't be used in live networks are enabled by default for now dev = [] ferveo-tpke = [ @@ -67,6 +67,11 @@ ibc-mocks-abcipp = [ "namada_core/ibc-mocks-abcipp", ] +masp-tx-gen = [ + "rand", + "rand_core", +] + # for integration tests and test utilies testing = [ "namada_core/testing", @@ -76,7 +81,15 @@ testing = [ "tempfile", ] +namada-sdk = [ + "tendermint-rpc", + "masp-tx-gen", + "ferveo-tpke", + "masp_primitives/transparent-inputs", +] + [dependencies] +async-std = "1.11.0" namada_core = {path = "../core", default-features = false, features = ["secp256k1-sign-verify"]} namada_proof_of_stake = {path = "../proof_of_stake", default-features = false} async-trait = {version = "0.1.51", optional = true} @@ -103,16 +116,17 @@ prost = "0.9.0" pwasm-utils = {git = "https://github.com/heliaxdev/wasm-utils", tag = "v0.20.0", features = ["sign_ext"], optional = true} rayon = {version = "=1.5.3", optional = true} rust_decimal = "1.26.1" +serde = {version = "1.0.125", features = ["derive"]} serde_json = "1.0.62" sha2 = "0.9.3" # We switch off "blake2b" because it cannot be compiled to wasm tempfile = {version = "3.2.0", optional = true} -tendermint-abcipp = {package = "tendermint", git = "https://github.com/heliaxdev/tendermint-rs", rev = "95c52476bc37927218374f94ac8e2a19bd35bec9", optional = true} -tendermint-rpc-abcipp = {package = "tendermint-rpc", git = "https://github.com/heliaxdev/tendermint-rs", rev = "95c52476bc37927218374f94ac8e2a19bd35bec9", features = ["http-client"], optional = true} -tendermint-proto-abcipp = {package = "tendermint-proto", git = "https://github.com/heliaxdev/tendermint-rs", rev = "95c52476bc37927218374f94ac8e2a19bd35bec9", optional = true} -tendermint = {version = "0.23.6", optional = true} -tendermint-rpc = {version = "0.23.6", features = ["http-client"], optional = true} -tendermint-proto = {version = "0.23.6", optional = true} +tendermint-abcipp = {package = "tendermint", git = "https://github.com/heliaxdev/tendermint-rs", optional = true, rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} +tendermint-rpc-abcipp = {package = "tendermint-rpc", git = "https://github.com/heliaxdev/tendermint-rs", features = ["trait-client"], default-features = false, optional = true, rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} +tendermint-proto-abcipp = {package = "tendermint-proto", git = "https://github.com/heliaxdev/tendermint-rs", optional = true, rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} +tendermint = {version = "0.23.6", optional = true, git = "https://github.com/heliaxdev/tendermint-rs", rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} +tendermint-rpc = {version = "0.23.6", features = ["trait-client"], default-features = false, optional = true, git = "https://github.com/heliaxdev/tendermint-rs", rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} +tendermint-proto = {version = "0.23.6", optional = true, git = "https://github.com/heliaxdev/tendermint-rs", rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} thiserror = "1.0.30" tracing = "0.1.30" wasmer = {version = "=2.2.0", optional = true} @@ -125,7 +139,13 @@ wasmparser = "0.83.0" #libmasp = { git = "https://github.com/anoma/masp", branch = "murisi/masp-incentive" } masp_primitives = { git = "https://github.com/anoma/masp", rev = "bee40fc465f6afbd10558d12fe96eb1742eee45c" } masp_proofs = { git = "https://github.com/anoma/masp", rev = "bee40fc465f6afbd10558d12fe96eb1742eee45c" } +rand = {version = "0.8", default-features = false, optional = true} +rand_core = {version = "0.6", default-features = false, optional = true} zeroize = "1.5.5" +toml = "0.5.8" +bimap = {version = "0.6.2", features = ["serde"]} +orion = "0.16.0" +tokio = {version = "1.8.2", default-features = false} [dev-dependencies] assert_matches = "1.5.0" diff --git a/shared/src/ledger/args.rs b/shared/src/ledger/args.rs new file mode 100644 index 0000000000..f4906b42c8 --- /dev/null +++ b/shared/src/ledger/args.rs @@ -0,0 +1,492 @@ +//! Structures encapsulating SDK arguments +use rust_decimal::Decimal; + +use crate::ibc::core::ics24_host::identifier::{ChannelId, PortId}; +use crate::types::address::Address; +use crate::types::key::{common, SchemeType}; +use crate::types::masp::MaspValue; +use crate::types::storage::Epoch; +use crate::types::transaction::GasLimit; +use crate::types::{storage, token}; + +/// Abstraction of types being used in Namada +pub trait NamadaTypes: Clone + std::fmt::Debug { + /// Represents an address on the ledger + type Address: Clone + std::fmt::Debug; + /// Represents the address of a native token + type NativeAddress: Clone + std::fmt::Debug; + /// Represents a key pair + type Keypair: Clone + std::fmt::Debug; + /// Represents the address of a Tendermint endpoint + type TendermintAddress: Clone + std::fmt::Debug; + /// Represents a viewing key + type ViewingKey: Clone + std::fmt::Debug; + /// Represents the owner of a balance + type BalanceOwner: Clone + std::fmt::Debug; + /// Represents a public key + type PublicKey: Clone + std::fmt::Debug; + /// Represents the source of a Transfer + type TransferSource: Clone + std::fmt::Debug; + /// Represents the target of a Transfer + type TransferTarget: Clone + std::fmt::Debug; + /// Represents some data that is used in a transaction + type Data: Clone + std::fmt::Debug; +} + +/// The concrete types being used in Namada SDK +#[derive(Clone, Debug)] +pub struct SdkTypes; + +impl NamadaTypes for SdkTypes { + type Address = Address; + type BalanceOwner = namada_core::types::masp::BalanceOwner; + type Data = Vec; + type Keypair = namada_core::types::key::common::SecretKey; + type NativeAddress = Address; + type PublicKey = namada_core::types::key::common::PublicKey; + type TendermintAddress = (); + type TransferSource = namada_core::types::masp::TransferSource; + type TransferTarget = namada_core::types::masp::TransferTarget; + type ViewingKey = namada_core::types::masp::ExtendedViewingKey; +} + +/// Common query arguments +#[derive(Clone, Debug)] +pub struct Query { + /// The address of the ledger node as host:port + pub ledger_address: C::TendermintAddress, +} + +/// Transaction associated results arguments +#[derive(Clone, Debug)] +pub struct QueryResult { + /// Common query args + pub query: Query, + /// Hash of transaction to lookup + pub tx_hash: String, +} + +/// Custom transaction arguments +#[derive(Clone, Debug)] +pub struct TxCustom { + /// Common tx arguments + pub tx: Tx, + /// Path to the tx WASM code file + pub code_path: C::Data, + /// Path to the data file + pub data_path: Option, +} + +/// Transfer transaction arguments +#[derive(Clone, Debug)] +pub struct TxTransfer { + /// Common tx arguments + pub tx: Tx, + /// Transfer source address + pub source: C::TransferSource, + /// Transfer target address + pub target: C::TransferTarget, + /// Transferred token address + pub token: C::Address, + /// Transferred token address + pub sub_prefix: Option, + /// Transferred token amount + pub amount: token::Amount, + /// Native token address + pub native_token: C::NativeAddress, + /// Path to the TX WASM code file + pub tx_code_path: C::Data, +} + +/// IBC transfer transaction arguments +#[derive(Clone, Debug)] +pub struct TxIbcTransfer { + /// Common tx arguments + pub tx: Tx, + /// Transfer source address + pub source: C::Address, + /// Transfer target address + pub receiver: String, + /// Transferred token address + pub token: C::Address, + /// Transferred token address + pub sub_prefix: Option, + /// Transferred token amount + pub amount: token::Amount, + /// Port ID + pub port_id: PortId, + /// Channel ID + pub channel_id: ChannelId, + /// Timeout height of the destination chain + pub timeout_height: Option, + /// Timeout timestamp offset + pub timeout_sec_offset: Option, + /// Path to the TX WASM code file + pub tx_code_path: C::Data, +} + +/// Transaction to initialize a new account +#[derive(Clone, Debug)] +pub struct TxInitAccount { + /// Common tx arguments + pub tx: Tx, + /// Address of the source account + pub source: C::Address, + /// Path to the VP WASM code file for the new account + pub vp_code_path: C::Data, + /// Path to the TX WASM code file + pub tx_code_path: C::Data, + /// Public key for the new account + pub public_key: C::PublicKey, +} + +/// Transaction to initialize a new account +#[derive(Clone, Debug)] +pub struct TxInitValidator { + /// Common tx arguments + pub tx: Tx, + /// Source + pub source: C::Address, + /// Signature scheme + pub scheme: SchemeType, + /// Account key + pub account_key: Option, + /// Consensus key + pub consensus_key: Option, + /// Protocol key + pub protocol_key: Option, + /// Commission rate + pub commission_rate: Decimal, + /// Maximum commission rate change + pub max_commission_rate_change: Decimal, + /// Path to the VP WASM code file + pub validator_vp_code_path: C::Data, + /// Path to the TX WASM code file + pub tx_code_path: C::Data, + /// Don't encrypt the keypair + pub unsafe_dont_encrypt: bool, +} + +/// Transaction to update a VP arguments +#[derive(Clone, Debug)] +pub struct TxUpdateVp { + /// Common tx arguments + pub tx: Tx, + /// Path to the VP WASM code file + pub vp_code_path: C::Data, + /// Path to the TX WASM code file + pub tx_code_path: C::Data, + /// Address of the account whose VP is to be updated + pub addr: C::Address, +} + +/// Bond arguments +#[derive(Clone, Debug)] +pub struct Bond { + /// Common tx arguments + pub tx: Tx, + /// Validator address + pub validator: C::Address, + /// Amount of tokens to stake in a bond + pub amount: token::Amount, + /// Source address for delegations. For self-bonds, the validator is + /// also the source. + pub source: Option, + /// Native token address + pub native_token: C::NativeAddress, + /// Path to the TX WASM code file + pub tx_code_path: C::Data, +} + +/// Unbond arguments +#[derive(Clone, Debug)] +pub struct Unbond { + /// Common tx arguments + pub tx: Tx, + /// Validator address + pub validator: C::Address, + /// Amount of tokens to unbond from a bond + pub amount: token::Amount, + /// Source address for unbonding from delegations. For unbonding from + /// self-bonds, the validator is also the source + pub source: Option, + /// Path to the TX WASM code file + pub tx_code_path: C::Data, +} + +/// Reveal public key +#[derive(Clone, Debug)] +pub struct RevealPk { + /// Common tx arguments + pub tx: Tx, + /// A public key to be revealed on-chain + pub public_key: C::PublicKey, +} + +/// Query proposal +#[derive(Clone, Debug)] +pub struct QueryProposal { + /// Common query args + pub query: Query, + /// Proposal id + pub proposal_id: Option, +} + +/// Query protocol parameters +#[derive(Clone, Debug)] +pub struct QueryProtocolParameters { + /// Common query args + pub query: Query, +} + +/// Withdraw arguments +#[derive(Clone, Debug)] +pub struct Withdraw { + /// Common tx arguments + pub tx: Tx, + /// Validator address + pub validator: C::Address, + /// Source address for withdrawing from delegations. For withdrawing + /// from self-bonds, the validator is also the source + pub source: Option, + /// Path to the TX WASM code file + pub tx_code_path: C::Data, +} + +/// Query asset conversions +#[derive(Clone, Debug)] +pub struct QueryConversions { + /// Common query args + pub query: Query, + /// Address of a token + pub token: Option, + /// Epoch of the asset + pub epoch: Option, +} + +/// Query token balance(s) +#[derive(Clone, Debug)] +pub struct QueryBalance { + /// Common query args + pub query: Query, + /// Address of an owner + pub owner: Option, + /// Address of a token + pub token: Option, + /// Whether not to convert balances + pub no_conversions: bool, + /// Sub prefix of an account + pub sub_prefix: Option, +} + +/// Query historical transfer(s) +#[derive(Clone, Debug)] +pub struct QueryTransfers { + /// Common query args + pub query: Query, + /// Address of an owner + pub owner: Option, + /// Address of a token + pub token: Option, +} + +/// Query PoS bond(s) +#[derive(Clone, Debug)] +pub struct QueryBonds { + /// Common query args + pub query: Query, + /// Address of an owner + pub owner: Option, + /// Address of a validator + pub validator: Option, +} + +/// Query PoS bonded stake +#[derive(Clone, Debug)] +pub struct QueryBondedStake { + /// Common query args + pub query: Query, + /// Address of a validator + pub validator: Option, + /// Epoch in which to find bonded stake + pub epoch: Option, +} + +#[derive(Clone, Debug)] +/// Commission rate change args +pub struct TxCommissionRateChange { + /// Common tx arguments + pub tx: Tx, + /// Validator address (should be self) + pub validator: C::Address, + /// Value to which the tx changes the commission rate + pub rate: Decimal, + /// Path to the TX WASM code file + pub tx_code_path: C::Data, +} + +/// Query PoS commission rate +#[derive(Clone, Debug)] +pub struct QueryCommissionRate { + /// Common query args + pub query: Query, + /// Address of a validator + pub validator: C::Address, + /// Epoch in which to find commission rate + pub epoch: Option, +} + +/// Query PoS slashes +#[derive(Clone, Debug)] +pub struct QuerySlashes { + /// Common query args + pub query: Query, + /// Address of a validator + pub validator: Option, +} + +/// Query the raw bytes of given storage key +#[derive(Clone, Debug)] +pub struct QueryRawBytes { + /// The storage key to query + pub storage_key: storage::Key, + /// Common query args + pub query: Query, +} + +/// Common transaction arguments +#[derive(Clone, Debug)] +pub struct Tx { + /// Simulate applying the transaction + pub dry_run: bool, + /// Submit the transaction even if it doesn't pass client checks + pub force: bool, + /// Do not wait for the transaction to be added to the blockchain + pub broadcast_only: bool, + /// The address of the ledger node as host:port + pub ledger_address: C::TendermintAddress, + /// If any new account is initialized by the tx, use the given alias to + /// save it in the wallet. + pub initialized_account_alias: Option, + /// The amount being payed to include the transaction + pub fee_amount: token::Amount, + /// The token in which the fee is being paid + pub fee_token: C::Address, + /// The max amount of gas used to process tx + pub gas_limit: GasLimit, + /// Sign the tx with the key for the given alias from your wallet + pub signing_key: Option, + /// Sign the tx with the keypair of the public key of the given address + pub signer: Option, + /// Path to the TX WASM code file + pub tx_code_path: C::Data, + /// Password to decrypt key + pub password: Option, +} + +/// MASP add key or address arguments +#[derive(Clone, Debug)] +pub struct MaspAddrKeyAdd { + /// Key alias + pub alias: String, + /// Any MASP value + pub value: MaspValue, + /// Don't encrypt the keypair + pub unsafe_dont_encrypt: bool, +} + +/// MASP generate spending key arguments +#[derive(Clone, Debug)] +pub struct MaspSpendKeyGen { + /// Key alias + pub alias: String, + /// Don't encrypt the keypair + pub unsafe_dont_encrypt: bool, +} + +/// MASP generate payment address arguments +#[derive(Clone, Debug)] +pub struct MaspPayAddrGen { + /// Key alias + pub alias: String, + /// Viewing key + pub viewing_key: C::ViewingKey, + /// Pin + pub pin: bool, +} + +/// Wallet generate key and implicit address arguments +#[derive(Clone, Debug)] +pub struct KeyAndAddressGen { + /// Scheme type + pub scheme: SchemeType, + /// Key alias + pub alias: Option, + /// Don't encrypt the keypair + pub unsafe_dont_encrypt: bool, +} + +/// Wallet key lookup arguments +#[derive(Clone, Debug)] +pub struct KeyFind { + /// Public key to lookup keypair with + pub public_key: Option, + /// Key alias to lookup keypair with + pub alias: Option, + /// Public key hash to lookup keypair with + pub value: Option, + /// Show secret keys to user + pub unsafe_show_secret: bool, +} + +/// Wallet find shielded address or key arguments +#[derive(Clone, Debug)] +pub struct AddrKeyFind { + /// Address/key alias + pub alias: String, + /// Show secret keys to user + pub unsafe_show_secret: bool, +} + +/// Wallet list shielded keys arguments +#[derive(Clone, Debug)] +pub struct MaspKeysList { + /// Don't decrypt spending keys + pub decrypt: bool, + /// Show secret keys to user + pub unsafe_show_secret: bool, +} + +/// Wallet list keys arguments +#[derive(Clone, Debug)] +pub struct KeyList { + /// Don't decrypt keypairs + pub decrypt: bool, + /// Show secret keys to user + pub unsafe_show_secret: bool, +} + +/// Wallet key export arguments +#[derive(Clone, Debug)] +pub struct KeyExport { + /// Key alias + pub alias: String, +} + +/// Wallet address lookup arguments +#[derive(Clone, Debug)] +pub struct AddressOrAliasFind { + /// Alias to find + pub alias: Option, + /// Address to find + pub address: Option
, +} + +/// Wallet address add arguments +#[derive(Clone, Debug)] +pub struct AddressAdd { + /// Address alias + pub alias: String, + /// Address to add + pub address: Address, +} diff --git a/shared/src/ledger/masp.rs b/shared/src/ledger/masp.rs index 10c63db4cb..7183f82ee7 100644 --- a/shared/src/ledger/masp.rs +++ b/shared/src/ledger/masp.rs @@ -1,22 +1,71 @@ //! MASP verification wrappers. +use std::collections::hash_map::Entry; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::env; +use std::fmt::Debug; use std::fs::File; +#[cfg(feature = "masp-tx-gen")] use std::ops::Deref; use std::path::PathBuf; +use async_trait::async_trait; use bellman::groth16::{prepare_verifying_key, PreparedVerifyingKey}; use bls12_381::Bls12; +// use async_std::io::prelude::WriteExt; +// use async_std::io::{self}; +use borsh::{BorshDeserialize, BorshSerialize}; +use itertools::Either; use masp_primitives::asset_type::AssetType; use masp_primitives::consensus::BranchId::Sapling; +use masp_primitives::consensus::{BranchId, TestNetwork}; +use masp_primitives::convert::AllowedConversion; +use masp_primitives::ff::PrimeField; +use masp_primitives::group::cofactor::CofactorGroup; +use masp_primitives::keys::FullViewingKey; +#[cfg(feature = "masp-tx-gen")] +use masp_primitives::legacy::TransparentAddress; +use masp_primitives::merkle_tree::{ + CommitmentTree, IncrementalWitness, MerklePath, +}; +use masp_primitives::note_encryption::*; +use masp_primitives::primitives::{Diversifier, Note, ViewingKey}; use masp_primitives::redjubjub::PublicKey; +use masp_primitives::sapling::Node; +#[cfg(feature = "masp-tx-gen")] +use masp_primitives::transaction::builder::{self, secp256k1, *}; use masp_primitives::transaction::components::{ - ConvertDescription, OutputDescription, SpendDescription, + Amount, ConvertDescription, OutputDescription, SpendDescription, }; +#[cfg(feature = "masp-tx-gen")] +use masp_primitives::transaction::components::{OutPoint, TxOut}; use masp_primitives::transaction::{ signature_hash_data, Transaction, SIGHASH_ALL, }; +use masp_primitives::zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}; +use masp_proofs::prover::LocalTxProver; use masp_proofs::sapling::SaplingVerificationContext; +use namada_core::types::transaction::AffineCurve; +#[cfg(feature = "masp-tx-gen")] +use rand_core::{CryptoRng, OsRng, RngCore}; +#[cfg(feature = "masp-tx-gen")] +use sha2::Digest; + +use crate::ledger::queries::Client; +use crate::ledger::{args, rpc}; +use crate::proto::{SignedTxData, Tx}; +use crate::tendermint_rpc::query::Query; +use crate::tendermint_rpc::Order; +use crate::types::address::{masp, Address}; +use crate::types::masp::{BalanceOwner, ExtendedViewingKey, PaymentAddress}; +use crate::types::storage::{BlockHeight, Epoch, Key, KeySeg, TxIndex}; +use crate::types::token; +use crate::types::token::{ + Transfer, HEAD_TX_KEY, PIN_KEY_PREFIX, TX_KEY_PREFIX, +}; +use crate::types::transaction::{ + process_tx, DecryptedTx, EllipticCurve, PairingEngine, TxType, WrapperTx, +}; /// Env var to point to a dir with MASP parameters. When not specified, /// the default OS specific path is used. @@ -200,3 +249,1209 @@ pub fn get_params_dir() -> PathBuf { masp_proofs::default_params_folder().unwrap() } } + +/// Abstracts platform specific details away from the logic of shielded pool +/// operations. +#[async_trait] +pub trait ShieldedUtils: + Sized + BorshDeserialize + BorshSerialize + Default + Clone +{ + /// The type of the Tendermint client to make queries with + type C: crate::ledger::queries::Client + std::marker::Sync; + + /// Get a MASP transaction prover + fn local_tx_prover(&self) -> LocalTxProver; + + /// Load up the currently saved ShieldedContext + fn load(self) -> std::io::Result>; + + /// Sace the given ShieldedContext for future loads + fn save(&self, ctx: &ShieldedContext) -> std::io::Result<()>; +} + +/// Make a ViewingKey that can view notes encrypted by given ExtendedSpendingKey +pub fn to_viewing_key(esk: &ExtendedSpendingKey) -> FullViewingKey { + ExtendedFullViewingKey::from(esk).fvk +} + +/// Generate a valid diversifier, i.e. one that has a diversified base. Return +/// also this diversified base. +#[cfg(feature = "masp-tx-gen")] +pub fn find_valid_diversifier( + rng: &mut R, +) -> (Diversifier, masp_primitives::jubjub::SubgroupPoint) { + let mut diversifier; + let g_d; + // Keep generating random diversifiers until one has a diversified base + loop { + let mut d = [0; 11]; + rng.fill_bytes(&mut d); + diversifier = Diversifier(d); + if let Some(val) = diversifier.g_d() { + g_d = val; + break; + } + } + (diversifier, g_d) +} + +/// Determine if using the current note would actually bring us closer to our +/// target +pub fn is_amount_required(src: Amount, dest: Amount, delta: Amount) -> bool { + if delta > Amount::zero() { + let gap = dest - src; + for (asset_type, value) in gap.components() { + if *value > 0 && delta[asset_type] > 0 { + return true; + } + } + } + false +} + +/// An extension of Option's cloned method for pair types +fn cloned_pair((a, b): (&T, &U)) -> (T, U) { + (a.clone(), b.clone()) +} + +/// Errors that can occur when trying to retrieve pinned transaction +#[derive(PartialEq, Eq)] +pub enum PinnedBalanceError { + /// No transaction has yet been pinned to the given payment address + NoTransactionPinned, + /// The supplied viewing key does not recognize payments to given address + InvalidViewingKey, +} + +/// Represents the amount used of different conversions +pub type Conversions = + HashMap, i64)>; + +/// Represents the changes that were made to a list of transparent accounts +pub type TransferDelta = HashMap>; + +/// Represents the changes that were made to a list of shielded accounts +pub type TransactionDelta = HashMap; + +/// Represents the current state of the shielded pool from the perspective of +/// the chosen viewing keys. +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub struct ShieldedContext { + /// Location where this shielded context is saved + #[borsh_skip] + pub utils: U, + /// The last transaction index to be processed in this context + pub last_txidx: u64, + /// The commitment tree produced by scanning all transactions up to tx_pos + pub tree: CommitmentTree, + /// Maps viewing keys to applicable note positions + pub pos_map: HashMap>, + /// Maps a nullifier to the note position to which it applies + pub nf_map: HashMap<[u8; 32], usize>, + /// Maps note positions to their corresponding notes + pub note_map: HashMap, + /// Maps note positions to their corresponding memos + pub memo_map: HashMap, + /// Maps note positions to the diversifier of their payment address + pub div_map: HashMap, + /// Maps note positions to their witness (used to make merkle paths) + pub witness_map: HashMap>, + /// Tracks what each transaction does to various account balances + pub delta_map: BTreeMap< + (BlockHeight, TxIndex), + (Epoch, TransferDelta, TransactionDelta), + >, + /// The set of note positions that have been spent + pub spents: HashSet, + /// Maps asset types to their decodings + pub asset_types: HashMap, + /// Maps note positions to their corresponding viewing keys + pub vk_map: HashMap, +} + +/// Default implementation to ease construction of TxContexts. Derive cannot be +/// used here due to CommitmentTree not implementing Default. +impl Default for ShieldedContext { + fn default() -> ShieldedContext { + ShieldedContext:: { + utils: U::default(), + last_txidx: u64::default(), + tree: CommitmentTree::empty(), + pos_map: HashMap::default(), + nf_map: HashMap::default(), + note_map: HashMap::default(), + memo_map: HashMap::default(), + div_map: HashMap::default(), + witness_map: HashMap::default(), + spents: HashSet::default(), + delta_map: BTreeMap::default(), + asset_types: HashMap::default(), + vk_map: HashMap::default(), + } + } +} + +impl ShieldedContext { + /// Try to load the last saved shielded context from the given context + /// directory. If this fails, then leave the current context unchanged. + pub fn load(&mut self) -> std::io::Result<()> { + let new_ctx = self.utils.clone().load()?; + *self = new_ctx; + Ok(()) + } + + /// Save this shielded context into its associated context directory + pub fn save(&self) -> std::io::Result<()> { + self.utils.save(self) + } + + /// Merge data from the given shielded context into the current shielded + /// context. It must be the case that the two shielded contexts share the + /// same last transaction ID and share identical commitment trees. + pub fn merge(&mut self, new_ctx: ShieldedContext) { + debug_assert_eq!(self.last_txidx, new_ctx.last_txidx); + // Merge by simply extending maps. Identical keys should contain + // identical values, so overwriting should not be problematic. + self.pos_map.extend(new_ctx.pos_map); + self.nf_map.extend(new_ctx.nf_map); + self.note_map.extend(new_ctx.note_map); + self.memo_map.extend(new_ctx.memo_map); + self.div_map.extend(new_ctx.div_map); + self.witness_map.extend(new_ctx.witness_map); + self.spents.extend(new_ctx.spents); + self.asset_types.extend(new_ctx.asset_types); + self.vk_map.extend(new_ctx.vk_map); + // The deltas are the exception because different keys can reveal + // different parts of the same transaction. Hence each delta needs to be + // merged separately. + for ((height, idx), (ep, ntfer_delta, ntx_delta)) in new_ctx.delta_map { + let (_ep, tfer_delta, tx_delta) = self + .delta_map + .entry((height, idx)) + .or_insert((ep, TransferDelta::new(), TransactionDelta::new())); + tfer_delta.extend(ntfer_delta); + tx_delta.extend(ntx_delta); + } + } + + /// Fetch the current state of the multi-asset shielded pool into a + /// ShieldedContext + pub async fn fetch( + &mut self, + client: &U::C, + sks: &[ExtendedSpendingKey], + fvks: &[ViewingKey], + ) { + // First determine which of the keys requested to be fetched are new. + // Necessary because old transactions will need to be scanned for new + // keys. + let mut unknown_keys = Vec::new(); + for esk in sks { + let vk = to_viewing_key(esk).vk; + if !self.pos_map.contains_key(&vk) { + unknown_keys.push(vk); + } + } + for vk in fvks { + if !self.pos_map.contains_key(vk) { + unknown_keys.push(*vk); + } + } + + // If unknown keys are being used, we need to scan older transactions + // for any unspent notes + let (txs, mut tx_iter); + if !unknown_keys.is_empty() { + // Load all transactions accepted until this point + txs = Self::fetch_shielded_transfers(client, 0).await; + tx_iter = txs.iter(); + // Do this by constructing a shielding context only for unknown keys + let mut tx_ctx = Self { + utils: self.utils.clone(), + ..Default::default() + }; + for vk in unknown_keys { + tx_ctx.pos_map.entry(vk).or_insert_with(HashSet::new); + } + // Update this unknown shielded context until it is level with self + while tx_ctx.last_txidx != self.last_txidx { + if let Some(((height, idx), (epoch, tx))) = tx_iter.next() { + tx_ctx.scan_tx(*height, *idx, *epoch, tx); + } else { + break; + } + } + // Merge the context data originating from the unknown keys into the + // current context + self.merge(tx_ctx); + } else { + // Load only transactions accepted from last_txid until this point + txs = Self::fetch_shielded_transfers(client, self.last_txidx).await; + tx_iter = txs.iter(); + } + // Now that we possess the unspent notes corresponding to both old and + // new keys up until tx_pos, proceed to scan the new transactions. + for ((height, idx), (epoch, tx)) in &mut tx_iter { + self.scan_tx(*height, *idx, *epoch, tx); + } + } + + /// Obtain a chronologically-ordered list of all accepted shielded + /// transactions from the ledger. The ledger conceptually stores + /// transactions as a vector. More concretely, the HEAD_TX_KEY location + /// stores the index of the last accepted transaction and each transaction + /// is stored at a key derived from its index. + pub async fn fetch_shielded_transfers( + client: &U::C, + last_txidx: u64, + ) -> BTreeMap<(BlockHeight, TxIndex), (Epoch, Transfer)> { + // The address of the MASP account + let masp_addr = masp(); + // Construct the key where last transaction pointer is stored + let head_tx_key = Key::from(masp_addr.to_db_key()) + .push(&HEAD_TX_KEY.to_owned()) + .expect("Cannot obtain a storage key"); + // Query for the index of the last accepted transaction + let head_txidx = + rpc::query_storage_value::(client, &head_tx_key) + .await + .unwrap_or(0); + let mut shielded_txs = BTreeMap::new(); + // Fetch all the transactions we do not have yet + for i in last_txidx..head_txidx { + // Construct the key for where the current transaction is stored + let current_tx_key = Key::from(masp_addr.to_db_key()) + .push(&(TX_KEY_PREFIX.to_owned() + &i.to_string())) + .expect("Cannot obtain a storage key"); + // Obtain the current transaction + let (tx_epoch, tx_height, tx_index, current_tx) = + rpc::query_storage_value::< + U::C, + (Epoch, BlockHeight, TxIndex, Transfer), + >(client, ¤t_tx_key) + .await + .unwrap(); + // Collect the current transaction + shielded_txs.insert((tx_height, tx_index), (tx_epoch, current_tx)); + } + shielded_txs + } + + /// Applies the given transaction to the supplied context. More precisely, + /// the shielded transaction's outputs are added to the commitment tree. + /// Newly discovered notes are associated to the supplied viewing keys. Note + /// nullifiers are mapped to their originating notes. Note positions are + /// associated to notes, memos, and diversifiers. And the set of notes that + /// we have spent are updated. The witness map is maintained to make it + /// easier to construct note merkle paths in other code. See + /// https://zips.z.cash/protocol/protocol.pdf#scan + pub fn scan_tx( + &mut self, + height: BlockHeight, + index: TxIndex, + epoch: Epoch, + tx: &Transfer, + ) { + // Ignore purely transparent transactions + let shielded = if let Some(shielded) = &tx.shielded { + shielded + } else { + return; + }; + // For tracking the account changes caused by this Transaction + let mut transaction_delta = TransactionDelta::new(); + // Listen for notes sent to our viewing keys + for so in &shielded.shielded_outputs { + // Create merkle tree leaf node from note commitment + let node = Node::new(so.cmu.to_repr()); + // Update each merkle tree in the witness map with the latest + // addition + for (_, witness) in self.witness_map.iter_mut() { + witness.append(node).expect("note commitment tree is full"); + } + let note_pos = self.tree.size(); + self.tree + .append(node) + .expect("note commitment tree is full"); + // Finally, make it easier to construct merkle paths to this new + // note + let witness = IncrementalWitness::::from_tree(&self.tree); + self.witness_map.insert(note_pos, witness); + // Let's try to see if any of our viewing keys can decrypt latest + // note + for (vk, notes) in self.pos_map.iter_mut() { + let decres = try_sapling_note_decryption::( + 0, + &vk.ivk().0, + &so.ephemeral_key.into_subgroup().unwrap(), + &so.cmu, + &so.enc_ciphertext, + ); + // So this current viewing key does decrypt this current note... + if let Some((note, pa, memo)) = decres { + // Add this note to list of notes decrypted by this viewing + // key + notes.insert(note_pos); + // Compute the nullifier now to quickly recognize when spent + let nf = note.nf(vk, note_pos.try_into().unwrap()); + self.note_map.insert(note_pos, note); + self.memo_map.insert(note_pos, memo); + // The payment address' diversifier is required to spend + // note + self.div_map.insert(note_pos, *pa.diversifier()); + self.nf_map.insert(nf.0, note_pos); + // Note the account changes + let balance = transaction_delta + .entry(*vk) + .or_insert_with(Amount::zero); + *balance += + Amount::from_nonnegative(note.asset_type, note.value) + .expect( + "found note with invalid value or asset type", + ); + self.vk_map.insert(note_pos, *vk); + break; + } + } + } + // Cancel out those of our notes that have been spent + for ss in &shielded.shielded_spends { + // If the shielded spend's nullifier is in our map, then target note + // is rendered unusable + if let Some(note_pos) = self.nf_map.get(&ss.nullifier) { + self.spents.insert(*note_pos); + // Note the account changes + let balance = transaction_delta + .entry(self.vk_map[note_pos]) + .or_insert_with(Amount::zero); + let note = self.note_map[note_pos]; + *balance -= + Amount::from_nonnegative(note.asset_type, note.value) + .expect("found note with invalid value or asset type"); + } + } + // Record the changes to the transparent accounts + let transparent_delta = + Amount::from_nonnegative(tx.token.clone(), u64::from(tx.amount)) + .expect("invalid value for amount"); + let mut transfer_delta = TransferDelta::new(); + transfer_delta + .insert(tx.source.clone(), Amount::zero() - &transparent_delta); + transfer_delta.insert(tx.target.clone(), transparent_delta); + self.delta_map.insert( + (height, index), + (epoch, transfer_delta, transaction_delta), + ); + self.last_txidx += 1; + } + + /// Summarize the effects on shielded and transparent accounts of each + /// Transfer in this context + pub fn get_tx_deltas( + &self, + ) -> &BTreeMap< + (BlockHeight, TxIndex), + (Epoch, TransferDelta, TransactionDelta), + > { + &self.delta_map + } + + /// Compute the total unspent notes associated with the viewing key in the + /// context. If the key is not in the context, then we do not know the + /// balance and hence we return None. + pub fn compute_shielded_balance(&self, vk: &ViewingKey) -> Option { + // Cannot query the balance of a key that's not in the map + if !self.pos_map.contains_key(vk) { + return None; + } + let mut val_acc = Amount::zero(); + // Retrieve the notes that can be spent by this key + if let Some(avail_notes) = self.pos_map.get(vk) { + for note_idx in avail_notes { + // Spent notes cannot contribute a new transaction's pool + if self.spents.contains(note_idx) { + continue; + } + // Get note associated with this ID + let note = self.note_map.get(note_idx).unwrap(); + // Finally add value to multi-asset accumulator + val_acc += + Amount::from_nonnegative(note.asset_type, note.value) + .expect("found note with invalid value or asset type"); + } + } + Some(val_acc) + } + + /// Query the ledger for the decoding of the given asset type and cache it + /// if it is found. + pub async fn decode_asset_type( + &mut self, + client: &U::C, + asset_type: AssetType, + ) -> Option<(Address, Epoch)> { + // Try to find the decoding in the cache + if let decoded @ Some(_) = self.asset_types.get(&asset_type) { + return decoded.cloned(); + } + // Query for the ID of the last accepted transaction + let (addr, ep, _conv, _path): (Address, _, Amount, MerklePath) = + rpc::query_conversion(client, asset_type).await?; + self.asset_types.insert(asset_type, (addr.clone(), ep)); + Some((addr, ep)) + } + + /// Query the ledger for the conversion that is allowed for the given asset + /// type and cache it. + async fn query_allowed_conversion<'a>( + &'a mut self, + client: &U::C, + asset_type: AssetType, + conversions: &'a mut Conversions, + ) -> Option<&'a mut (AllowedConversion, MerklePath, i64)> { + match conversions.entry(asset_type) { + Entry::Occupied(conv_entry) => Some(conv_entry.into_mut()), + Entry::Vacant(conv_entry) => { + // Query for the ID of the last accepted transaction + let (addr, ep, conv, path): (Address, _, _, _) = + rpc::query_conversion(client, asset_type).await?; + self.asset_types.insert(asset_type, (addr, ep)); + // If the conversion is 0, then we just have a pure decoding + if conv == Amount::zero() { + None + } else { + Some(conv_entry.insert((Amount::into(conv), path, 0))) + } + } + } + } + + /// Compute the total unspent notes associated with the viewing key in the + /// context and express that value in terms of the currently timestamped + /// asset types. If the key is not in the context, then we do not know the + /// balance and hence we return None. + pub async fn compute_exchanged_balance( + &mut self, + client: &U::C, + vk: &ViewingKey, + target_epoch: Epoch, + ) -> Option { + // First get the unexchanged balance + if let Some(balance) = self.compute_shielded_balance(vk) { + // And then exchange balance into current asset types + Some( + self.compute_exchanged_amount( + client, + balance, + target_epoch, + HashMap::new(), + ) + .await + .0, + ) + } else { + None + } + } + + /// Try to convert as much of the given asset type-value pair using the + /// given allowed conversion. usage is incremented by the amount of the + /// conversion used, the conversions are applied to the given input, and + /// the trace amount that could not be converted is moved from input to + /// output. + fn apply_conversion( + conv: AllowedConversion, + asset_type: AssetType, + value: i64, + usage: &mut i64, + input: &mut Amount, + output: &mut Amount, + ) { + // If conversion if possible, accumulate the exchanged amount + let conv: Amount = conv.into(); + // The amount required of current asset to qualify for conversion + let threshold = -conv[&asset_type]; + if threshold == 0 { + eprintln!( + "Asset threshold of selected conversion for asset type {} is \ + 0, this is a bug, please report it.", + asset_type + ); + } + // We should use an amount of the AllowedConversion that almost + // cancels the original amount + let required = value / threshold; + // Forget about the trace amount left over because we cannot + // realize its value + let trace = Amount::from_pair(asset_type, value % threshold).unwrap(); + // Record how much more of the given conversion has been used + *usage += required; + // Apply the conversions to input and move the trace amount to output + *input += conv * required - &trace; + *output += trace; + } + + /// Convert the given amount into the latest asset types whilst making a + /// note of the conversions that were used. Note that this function does + /// not assume that allowed conversions from the ledger are expressed in + /// terms of the latest asset types. + pub async fn compute_exchanged_amount( + &mut self, + client: &U::C, + mut input: Amount, + target_epoch: Epoch, + mut conversions: Conversions, + ) -> (Amount, Conversions) { + // Where we will store our exchanged value + let mut output = Amount::zero(); + // Repeatedly exchange assets until it is no longer possible + while let Some((asset_type, value)) = + input.components().next().map(cloned_pair) + { + let target_asset_type = self + .decode_asset_type(client, asset_type) + .await + .map(|(addr, _epoch)| make_asset_type(target_epoch, &addr)) + .unwrap_or(asset_type); + let at_target_asset_type = asset_type == target_asset_type; + if let (Some((conv, _wit, usage)), false) = ( + self.query_allowed_conversion( + client, + asset_type, + &mut conversions, + ) + .await, + at_target_asset_type, + ) { + println!( + "converting current asset type to latest asset type..." + ); + // Not at the target asset type, not at the latest asset type. + // Apply conversion to get from current asset type to the latest + // asset type. + Self::apply_conversion( + conv.clone(), + asset_type, + value, + usage, + &mut input, + &mut output, + ); + } else if let (Some((conv, _wit, usage)), false) = ( + self.query_allowed_conversion( + client, + target_asset_type, + &mut conversions, + ) + .await, + at_target_asset_type, + ) { + println!( + "converting latest asset type to target asset type..." + ); + // Not at the target asset type, yes at the latest asset type. + // Apply inverse conversion to get from latest asset type to + // the target asset type. + Self::apply_conversion( + conv.clone(), + asset_type, + value, + usage, + &mut input, + &mut output, + ); + } else { + // At the target asset type. Then move component over to output. + let comp = input.project(asset_type); + output += ∁ + // Strike from input to avoid repeating computation + input -= comp; + } + } + (output, conversions) + } + + /// Collect enough unspent notes in this context to exceed the given amount + /// of the specified asset type. Return the total value accumulated plus + /// notes and the corresponding diversifiers/merkle paths that were used to + /// achieve the total value. + pub async fn collect_unspent_notes( + &mut self, + client: &U::C, + vk: &ViewingKey, + target: Amount, + target_epoch: Epoch, + ) -> ( + Amount, + Vec<(Diversifier, Note, MerklePath)>, + Conversions, + ) { + // Establish connection with which to do exchange rate queries + let mut conversions = HashMap::new(); + let mut val_acc = Amount::zero(); + let mut notes = Vec::new(); + // Retrieve the notes that can be spent by this key + if let Some(avail_notes) = self.pos_map.get(vk).cloned() { + for note_idx in &avail_notes { + // No more transaction inputs are required once we have met + // the target amount + if val_acc >= target { + break; + } + // Spent notes cannot contribute a new transaction's pool + if self.spents.contains(note_idx) { + continue; + } + // Get note, merkle path, diversifier associated with this ID + let note = *self.note_map.get(note_idx).unwrap(); + + // The amount contributed by this note before conversion + let pre_contr = Amount::from_pair(note.asset_type, note.value) + .expect("received note has invalid value or asset type"); + let (contr, proposed_convs) = self + .compute_exchanged_amount( + client, + pre_contr, + target_epoch, + conversions.clone(), + ) + .await; + + // Use this note only if it brings us closer to our target + if is_amount_required( + val_acc.clone(), + target.clone(), + contr.clone(), + ) { + // Be sure to record the conversions used in computing + // accumulated value + val_acc += contr; + // Commit the conversions that were used to exchange + conversions = proposed_convs; + let merkle_path = + self.witness_map.get(note_idx).unwrap().path().unwrap(); + let diversifier = self.div_map.get(note_idx).unwrap(); + // Commit this note to our transaction + notes.push((*diversifier, note, merkle_path)); + } + } + } + (val_acc, notes, conversions) + } + + /// Compute the combined value of the output notes of the transaction pinned + /// at the given payment address. This computation uses the supplied viewing + /// keys to try to decrypt the output notes. If no transaction is pinned at + /// the given payment address fails with + /// `PinnedBalanceError::NoTransactionPinned`. + pub async fn compute_pinned_balance( + client: &U::C, + owner: PaymentAddress, + viewing_key: &ViewingKey, + ) -> Result<(Amount, Epoch), PinnedBalanceError> { + // Check that the supplied viewing key corresponds to given payment + // address + let counter_owner = viewing_key.to_payment_address( + *masp_primitives::primitives::PaymentAddress::diversifier( + &owner.into(), + ), + ); + match counter_owner { + Some(counter_owner) if counter_owner == owner.into() => {} + _ => return Err(PinnedBalanceError::InvalidViewingKey), + } + // The address of the MASP account + let masp_addr = masp(); + // Construct the key for where the transaction ID would be stored + let pin_key = Key::from(masp_addr.to_db_key()) + .push(&(PIN_KEY_PREFIX.to_owned() + &owner.hash())) + .expect("Cannot obtain a storage key"); + // Obtain the transaction pointer at the key + let txidx = rpc::query_storage_value::(client, &pin_key) + .await + .ok_or(PinnedBalanceError::NoTransactionPinned)?; + // Construct the key for where the pinned transaction is stored + let tx_key = Key::from(masp_addr.to_db_key()) + .push(&(TX_KEY_PREFIX.to_owned() + &txidx.to_string())) + .expect("Cannot obtain a storage key"); + // Obtain the pointed to transaction + let (tx_epoch, _tx_height, _tx_index, tx) = rpc::query_storage_value::< + U::C, + (Epoch, BlockHeight, TxIndex, Transfer), + >(client, &tx_key) + .await + .expect("Ill-formed epoch, transaction pair"); + // Accumulate the combined output note value into this Amount + let mut val_acc = Amount::zero(); + let tx = tx + .shielded + .expect("Pinned Transfers should have shielded part"); + for so in &tx.shielded_outputs { + // Let's try to see if our viewing key can decrypt current note + let decres = try_sapling_note_decryption::( + 0, + &viewing_key.ivk().0, + &so.ephemeral_key.into_subgroup().unwrap(), + &so.cmu, + &so.enc_ciphertext, + ); + match decres { + // So the given viewing key does decrypt this current note... + Some((note, pa, _memo)) if pa == owner.into() => { + val_acc += + Amount::from_nonnegative(note.asset_type, note.value) + .expect( + "found note with invalid value or asset type", + ); + break; + } + _ => {} + } + } + Ok((val_acc, tx_epoch)) + } + + /// Compute the combined value of the output notes of the pinned transaction + /// at the given payment address if there's any. The asset types may be from + /// the epoch of the transaction or even before, so exchange all these + /// amounts to the epoch of the transaction in order to get the value that + /// would have been displayed in the epoch of the transaction. + pub async fn compute_exchanged_pinned_balance( + &mut self, + client: &U::C, + owner: PaymentAddress, + viewing_key: &ViewingKey, + ) -> Result<(Amount, Epoch), PinnedBalanceError> { + // Obtain the balance that will be exchanged + let (amt, ep) = + Self::compute_pinned_balance(client, owner, viewing_key).await?; + // Finally, exchange the balance to the transaction's epoch + Ok(( + self.compute_exchanged_amount(client, amt, ep, HashMap::new()) + .await + .0, + ep, + )) + } + + /// Convert an amount whose units are AssetTypes to one whose units are + /// Addresses that they decode to. All asset types not corresponding to + /// the given epoch are ignored. + pub async fn decode_amount( + &mut self, + client: &U::C, + amt: Amount, + target_epoch: Epoch, + ) -> Amount
{ + let mut res = Amount::zero(); + for (asset_type, val) in amt.components() { + // Decode the asset type + let decoded = self.decode_asset_type(client, *asset_type).await; + // Only assets with the target timestamp count + match decoded { + Some((addr, epoch)) if epoch == target_epoch => { + res += &Amount::from_pair(addr, *val).unwrap() + } + _ => {} + } + } + res + } + + /// Convert an amount whose units are AssetTypes to one whose units are + /// Addresses that they decode to. + pub async fn decode_all_amounts( + &mut self, + client: &U::C, + amt: Amount, + ) -> Amount<(Address, Epoch)> { + let mut res = Amount::zero(); + for (asset_type, val) in amt.components() { + // Decode the asset type + let decoded = self.decode_asset_type(client, *asset_type).await; + // Only assets with the target timestamp count + if let Some((addr, epoch)) = decoded { + res += &Amount::from_pair((addr, epoch), *val).unwrap() + } + } + res + } + + /// Make shielded components to embed within a Transfer object. If no + /// shielded payment address nor spending key is specified, then no + /// shielded components are produced. Otherwise a transaction containing + /// nullifiers and/or note commitments are produced. Dummy transparent + /// UTXOs are sometimes used to make transactions balanced, but it is + /// understood that transparent account changes are effected only by the + /// amounts and signatures specified by the containing Transfer object. + #[cfg(feature = "masp-tx-gen")] + pub async fn gen_shielded_transfer( + &mut self, + client: &U::C, + args: args::TxTransfer, + shielded_gas: bool, + ) -> Result, builder::Error> + { + // No shielded components are needed when neither source nor destination + // are shielded + let spending_key = args.source.spending_key(); + let payment_address = args.target.payment_address(); + if spending_key.is_none() && payment_address.is_none() { + return Ok(None); + } + // We want to fund our transaction solely from supplied spending key + let spending_key = spending_key.map(|x| x.into()); + let spending_keys: Vec<_> = spending_key.into_iter().collect(); + // Load the current shielded context given the spending key we possess + let _ = self.load(); + self.fetch(client, &spending_keys, &[]).await; + // Save the update state so that future fetches can be short-circuited + let _ = self.save(); + // Determine epoch in which to submit potential shielded transaction + let epoch = rpc::query_epoch(client).await; + // Context required for storing which notes are in the source's + // possesion + let consensus_branch_id = BranchId::Sapling; + let amt: u64 = args.amount.into(); + let memo: Option = None; + + // Now we build up the transaction within this object + let mut builder = Builder::::new(0u32); + // Convert transaction amount into MASP types + let (asset_type, amount) = + convert_amount(epoch, &args.token, args.amount); + + // Transactions with transparent input and shielded output + // may be affected if constructed close to epoch boundary + let mut epoch_sensitive: bool = false; + // If there are shielded inputs + if let Some(sk) = spending_key { + // Transaction fees need to match the amount in the wrapper Transfer + // when MASP source is used + let (_, fee) = + convert_amount(epoch, &args.tx.fee_token, args.tx.fee_amount); + builder.set_fee(fee.clone())?; + // If the gas is coming from the shielded pool, then our shielded + // inputs must also cover the gas fee + let required_amt = if shielded_gas { amount + fee } else { amount }; + // Locate unspent notes that can help us meet the transaction amount + let (_, unspent_notes, used_convs) = self + .collect_unspent_notes( + client, + &to_viewing_key(&sk).vk, + required_amt, + epoch, + ) + .await; + // Commit the notes found to our transaction + for (diversifier, note, merkle_path) in unspent_notes { + builder.add_sapling_spend( + sk, + diversifier, + note, + merkle_path, + )?; + } + // Commit the conversion notes used during summation + for (conv, wit, value) in used_convs.values() { + if *value > 0 { + builder.add_convert( + conv.clone(), + *value as u64, + wit.clone(), + )?; + } + } + } else { + // No transfer fees come from the shielded transaction for non-MASP + // sources + builder.set_fee(Amount::zero())?; + // We add a dummy UTXO to our transaction, but only the source of + // the parent Transfer object is used to validate fund + // availability + let secp_sk = secp256k1::SecretKey::from_slice(&[0xcd; 32]) + .expect("secret key"); + let secp_ctx = + secp256k1::Secp256k1::::gen_new(); + let secp_pk = + secp256k1::PublicKey::from_secret_key(&secp_ctx, &secp_sk) + .serialize(); + let hash = + ripemd160::Ripemd160::digest(&sha2::Sha256::digest(&secp_pk)); + let script = TransparentAddress::PublicKey(hash.into()).script(); + epoch_sensitive = true; + builder.add_transparent_input( + secp_sk, + OutPoint::new([0u8; 32], 0), + TxOut { + asset_type, + value: amt, + script_pubkey: script, + }, + )?; + } + // Now handle the outputs of this transaction + // If there is a shielded output + if let Some(pa) = payment_address { + let ovk_opt = spending_key.map(|x| x.expsk.ovk); + builder.add_sapling_output( + ovk_opt, + pa.into(), + asset_type, + amt, + memo.clone(), + )?; + } else { + epoch_sensitive = false; + // Embed the transparent target address into the shielded + // transaction so that it can be signed + let target_enc = args + .target + .address() + .expect("target address should be transparent") + .try_to_vec() + .expect("target address encoding"); + let hash = ripemd160::Ripemd160::digest(&sha2::Sha256::digest( + target_enc.as_ref(), + )); + builder.add_transparent_output( + &TransparentAddress::PublicKey(hash.into()), + asset_type, + amt, + )?; + } + let prover = self.utils.local_tx_prover(); + // Build and return the constructed transaction + let mut tx = builder.build(consensus_branch_id, &prover); + + if epoch_sensitive { + let new_epoch = rpc::query_epoch(client).await; + + // If epoch has changed, recalculate shielded outputs to match new + // epoch + if new_epoch != epoch { + // Hack: build new shielded transfer with updated outputs + let mut replay_builder = + Builder::::new(0u32); + replay_builder.set_fee(Amount::zero())?; + let ovk_opt = spending_key.map(|x| x.expsk.ovk); + let (new_asset_type, _) = + convert_amount(new_epoch, &args.token, args.amount); + replay_builder.add_sapling_output( + ovk_opt, + payment_address.unwrap().into(), + new_asset_type, + amt, + memo, + )?; + + let secp_sk = secp256k1::SecretKey::from_slice(&[0xcd; 32]) + .expect("secret key"); + let secp_ctx = + secp256k1::Secp256k1::::gen_new(); + let secp_pk = + secp256k1::PublicKey::from_secret_key(&secp_ctx, &secp_sk) + .serialize(); + let hash = ripemd160::Ripemd160::digest(&sha2::Sha256::digest( + &secp_pk, + )); + let script = + TransparentAddress::PublicKey(hash.into()).script(); + replay_builder.add_transparent_input( + secp_sk, + OutPoint::new([0u8; 32], 0), + TxOut { + asset_type: new_asset_type, + value: amt, + script_pubkey: script, + }, + )?; + + let (replay_tx, _) = + replay_builder.build(consensus_branch_id, &prover)?; + tx = tx.map(|(t, tm)| { + let mut temp = t.deref().clone(); + temp.shielded_outputs = replay_tx.shielded_outputs.clone(); + temp.value_balance = temp.value_balance.reject(asset_type) + - Amount::from_pair(new_asset_type, amt).unwrap(); + (temp.freeze().unwrap(), tm) + }); + } + } + + tx.map(Some) + } + + /// Obtain the known effects of all accepted shielded and transparent + /// transactions. If an owner is specified, then restrict the set to only + /// 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( + &mut self, + client: &U::C, + query_owner: &Either>, + query_token: &Option
, + viewing_keys: &HashMap, + ) -> BTreeMap< + (BlockHeight, TxIndex), + (Epoch, TransferDelta, TransactionDelta), + > { + const TXS_PER_PAGE: u8 = 100; + let _ = self.load(); + let vks = viewing_keys; + let fvks: Vec<_> = vks + .values() + .map(|fvk| ExtendedFullViewingKey::from(*fvk).fvk.vk) + .collect(); + self.fetch(client, &[], &fvks).await; + // Save the update state so that future fetches can be short-circuited + let _ = self.save(); + // Required for filtering out rejected transactions from Tendermint + // responses + let block_results = rpc::query_results(client).await; + let mut transfers = self.get_tx_deltas().clone(); + // Construct the set of addresses relevant to user's query + let relevant_addrs = match &query_owner { + Either::Left(BalanceOwner::Address(owner)) => vec![owner.clone()], + // MASP objects are dealt with outside of tx_search + Either::Left(BalanceOwner::FullViewingKey(_viewing_key)) => vec![], + Either::Left(BalanceOwner::PaymentAddress(_owner)) => vec![], + // Unspecified owner means all known addresses are considered + // relevant + Either::Right(addrs) => addrs.clone(), + }; + // Find all transactions to or from the relevant address set + for addr in relevant_addrs { + for prop in ["transfer.source", "transfer.target"] { + // Query transactions involving the current address + let mut tx_query = Query::eq(prop, addr.encode()); + // Elaborate the query if requested by the user + if let Some(token) = &query_token { + tx_query = + tx_query.and_eq("transfer.token", token.encode()); + } + for page in 1.. { + let txs = &client + .tx_search( + tx_query.clone(), + true, + page, + TXS_PER_PAGE, + Order::Ascending, + ) + .await + .expect("Unable to query for transactions") + .txs; + for response_tx in txs { + let height = BlockHeight(response_tx.height.value()); + let idx = TxIndex(response_tx.index); + // Only process yet unprocessed transactions which have + // been accepted by node VPs + let should_process = !transfers + .contains_key(&(height, idx)) + && block_results[u64::from(height) as usize] + .is_accepted(idx.0 as usize); + if !should_process { + continue; + } + let tx = Tx::try_from(response_tx.tx.as_ref()) + .expect("Ill-formed Tx"); + let mut wrapper = None; + let mut transfer = None; + extract_payload(tx, &mut wrapper, &mut transfer); + // Epoch data is not needed for transparent transactions + let epoch = + wrapper.map(|x| x.epoch).unwrap_or_default(); + if let Some(transfer) = transfer { + // Skip MASP addresses as they are already handled + // by ShieldedContext + if transfer.source == masp() + || transfer.target == masp() + { + continue; + } + // Describe how a Transfer simply subtracts from one + // account and adds the same to another + let mut delta = TransferDelta::default(); + let tfer_delta = Amount::from_nonnegative( + transfer.token.clone(), + u64::from(transfer.amount), + ) + .expect("invalid value for amount"); + delta.insert( + transfer.source, + Amount::zero() - &tfer_delta, + ); + delta.insert(transfer.target, tfer_delta); + // No shielded accounts are affected by this + // Transfer + transfers.insert( + (height, idx), + (epoch, delta, TransactionDelta::new()), + ); + } + } + // An incomplete page signifies no more transactions + if (txs.len() as u8) < TXS_PER_PAGE { + break; + } + } + } + } + transfers + } +} + +/// Extract the payload from the given Tx object +fn extract_payload( + tx: Tx, + wrapper: &mut Option, + transfer: &mut Option, +) { + match process_tx(tx) { + Ok(TxType::Wrapper(wrapper_tx)) => { + let privkey = ::G2Affine::prime_subgroup_generator(); + extract_payload( + Tx::from(match wrapper_tx.decrypt(privkey) { + Ok(tx) => DecryptedTx::Decrypted(tx), + _ => DecryptedTx::Undecryptable(wrapper_tx.clone()), + }), + wrapper, + transfer, + ); + *wrapper = Some(wrapper_tx); + } + Ok(TxType::Decrypted(DecryptedTx::Decrypted(tx))) => { + let empty_vec = vec![]; + let tx_data = tx.data.as_ref().unwrap_or(&empty_vec); + let _ = SignedTxData::try_from_slice(tx_data).map(|signed| { + Transfer::try_from_slice(&signed.data.unwrap()[..]) + .map(|tfer| *transfer = Some(tfer)) + }); + } + _ => {} + } +} + +/// Make asset type corresponding to given address and epoch +fn make_asset_type(epoch: Epoch, token: &Address) -> AssetType { + // Typestamp the chosen token with the current epoch + let token_bytes = (token, epoch.0) + .try_to_vec() + .expect("token should serialize"); + // Generate the unique asset identifier from the unique token address + AssetType::new(token_bytes.as_ref()).expect("unable to create asset type") +} + +/// Convert Anoma amount and token type to MASP equivalents +fn convert_amount( + epoch: Epoch, + token: &Address, + val: token::Amount, +) -> (AssetType, Amount) { + let asset_type = make_asset_type(epoch, token); + // Combine the value and unit into one amount + let amount = Amount::from_nonnegative(asset_type, u64::from(val)) + .expect("invalid value for amount"); + (asset_type, amount) +} diff --git a/shared/src/ledger/mod.rs b/shared/src/ledger/mod.rs index 73f39dda05..e7ea6e403b 100644 --- a/shared/src/ledger/mod.rs +++ b/shared/src/ledger/mod.rs @@ -1,5 +1,6 @@ //! The ledger modules +pub mod args; pub mod eth_bridge; pub mod events; pub mod ibc; @@ -9,8 +10,12 @@ pub mod pos; #[cfg(all(feature = "wasm-runtime", feature = "ferveo-tpke"))] pub mod protocol; pub mod queries; +pub mod rpc; +pub mod signing; pub mod storage; +pub mod tx; pub mod vp_host_fns; +pub mod wallet; pub use namada_core::ledger::{ gas, governance, parameters, storage_api, tx_env, vp_env, diff --git a/shared/src/ledger/queries/mod.rs b/shared/src/ledger/queries/mod.rs index 4644909b1a..891cc2d420 100644 --- a/shared/src/ledger/queries/mod.rs +++ b/shared/src/ledger/queries/mod.rs @@ -11,6 +11,7 @@ use vp::{Vp, VP}; use super::storage::{DBIter, StorageHasher, DB}; use super::storage_api; +use crate::tendermint_rpc::error::Error as RpcError; use crate::types::storage::BlockHeight; #[macro_use] @@ -87,71 +88,11 @@ pub fn require_no_data(request: &RequestQuery) -> storage_api::Result<()> { Ok(()) } -#[cfg(any(feature = "tendermint-rpc", feature = "tendermint-rpc-abcipp",))] -/// Provides [`Client`] implementation for Tendermint RPC client -pub mod tm { - use thiserror::Error; - - use super::*; - use crate::types::storage::BlockHeight; - - #[allow(missing_docs)] - #[derive(Error, Debug)] - pub enum Error { - #[error("{0}")] - Tendermint(#[from] crate::tendermint_rpc::Error), - #[error("Decoding error: {0}")] - Decoding(#[from] std::io::Error), - #[error("Info log: {0}, error code: {1}")] - Query(String, u32), - #[error("Invalid block height: {0} (overflown i64)")] - InvalidHeight(BlockHeight), - } - - #[async_trait::async_trait] - impl Client for crate::tendermint_rpc::HttpClient { - type Error = Error; - - async fn request( - &self, - path: String, - data: Option>, - height: Option, - prove: bool, - ) -> Result { - let data = data.unwrap_or_default(); - let height = height - .map(|height| { - crate::tendermint::block::Height::try_from(height.0) - .map_err(|_err| Error::InvalidHeight(height)) - }) - .transpose()?; - let response = crate::tendermint_rpc::Client::abci_query( - self, - // TODO open the private Path constructor in tendermint-rpc - Some(std::str::FromStr::from_str(&path).unwrap()), - data, - height, - prove, - ) - .await?; - use crate::tendermint::abci::Code; - match response.code { - Code::Ok => Ok(EncodedResponseQuery { - data: response.value, - info: response.info, - proof: response.proof, - }), - Code::Err(code) => Err(Error::Query(response.info, code)), - } - } - } -} - /// Queries testing helpers #[cfg(any(test, feature = "testing"))] mod testing { use tempfile::TempDir; + use tendermint_rpc::Response; use super::*; use crate::ledger::events::log::EventLog; @@ -207,7 +148,7 @@ mod testing { } } - #[async_trait::async_trait] + #[async_trait::async_trait(?Send)] impl Client for TestClient where RPC: Router + Sync, @@ -241,5 +182,12 @@ mod testing { let response = self.rpc.handle(ctx, &request).unwrap(); Ok(response) } + + async fn perform(&self, _request: R) -> Result + where + R: tendermint_rpc::SimpleRequest, + { + Response::from_string("TODO") + } } } diff --git a/shared/src/ledger/queries/shell.rs b/shared/src/ledger/queries/shell.rs index 4883294c78..aa16ef08b2 100644 --- a/shared/src/ledger/queries/shell.rs +++ b/shared/src/ledger/queries/shell.rs @@ -4,7 +4,7 @@ use masp_primitives::merkle_tree::MerklePath; use masp_primitives::sapling::Node; use namada_core::types::address::Address; use namada_core::types::hash::Hash; -use namada_core::types::storage::BlockResults; +use namada_core::types::storage::{BlockResults, KeySeg}; use crate::ledger::events::log::dumb_queries; use crate::ledger::events::Event; @@ -111,12 +111,13 @@ where let mut results = vec![BlockResults::default(); ctx.storage.block.height.0 as usize + 1]; iter.for_each(|(key, value, _gas)| { - let key = key - .parse::() - .expect("expected integer for block height"); + let key = u64::parse(key).expect("expected integer for block height"); let value = BlockResults::try_from_slice(&value) .expect("expected BlockResults bytes"); - results[key] = value; + let idx: usize = key + .try_into() + .expect("expected block height to fit into usize"); + results[idx] = value; }); Ok(results) } diff --git a/shared/src/ledger/queries/types.rs b/shared/src/ledger/queries/types.rs index 35a3424822..155944a898 100644 --- a/shared/src/ledger/queries/types.rs +++ b/shared/src/ledger/queries/types.rs @@ -1,13 +1,22 @@ +use tendermint::block::Height; +use tendermint_rpc::endpoint::{ + abci_info, block, block_results, blockchain, commit, consensus_params, + consensus_state, health, net_info, status, +}; +use tendermint_rpc::query::Query; +use tendermint_rpc::Order; +use thiserror::Error; + use crate::ledger::events::log::EventLog; use crate::ledger::storage::{DBIter, Storage, StorageHasher, DB}; use crate::ledger::storage_api; use crate::tendermint::merkle::proof::Proof; +use crate::tendermint_rpc::error::Error as RpcError; use crate::types::storage::BlockHeight; #[cfg(feature = "wasm-runtime")] use crate::vm::wasm::{TxCache, VpCache}; #[cfg(feature = "wasm-runtime")] use crate::vm::WasmCacheRoAccess; - /// A request context provides read-only access to storage and WASM compilation /// caches to request handlers. #[derive(Debug, Clone)] @@ -69,7 +78,7 @@ pub trait Router { /// A client with async request dispatcher method, which can be used to invoke /// type-safe methods from a root [`Router`], generated via `router!` macro. #[cfg(any(test, feature = "async-client"))] -#[async_trait::async_trait] +#[async_trait::async_trait(?Send)] pub trait Client { /// `std::io::Error` can happen in decoding with /// `BorshDeserialize::try_from_slice` @@ -94,6 +103,250 @@ pub trait Client { height: Option, prove: bool, ) -> Result; + + /// `/abci_info`: get information about the ABCI application. + async fn abci_info(&self) -> Result { + Ok(self.perform(abci_info::Request).await?.response) + } + + /// `/broadcast_tx_sync`: broadcast a transaction, returning the response + /// from `CheckTx`. + async fn broadcast_tx_sync( + &self, + tx: tendermint::abci::Transaction, + ) -> Result + { + self.perform( + tendermint_rpc::endpoint::broadcast::tx_sync::Request::new(tx), + ) + .await + } + + /// `/block`: get the latest block. + async fn latest_block(&self) -> Result { + self.perform(block::Request::default()).await + } + + /// `/block`: get block at a given height. + async fn block(&self, height: H) -> Result + where + H: Into + Send, + { + self.perform(block::Request::new(height.into())).await + } + + /// `/block_search`: search for blocks by BeginBlock and EndBlock events. + async fn block_search( + &self, + query: Query, + page: u32, + per_page: u8, + order: Order, + ) -> Result + { + self.perform(tendermint_rpc::endpoint::block_search::Request::new( + query, page, per_page, order, + )) + .await + } + + /// `/block_results`: get ABCI results for a block at a particular height. + async fn block_results( + &self, + height: H, + ) -> Result + where + H: Into + Send, + { + self.perform(tendermint_rpc::endpoint::block_results::Request::new( + height.into(), + )) + .await + } + + /// `/tx_search`: search for transactions with their results. + async fn tx_search( + &self, + query: Query, + prove: bool, + page: u32, + per_page: u8, + order: Order, + ) -> Result { + self.perform(tendermint_rpc::endpoint::tx_search::Request::new( + query, prove, page, per_page, order, + )) + .await + } + + /// `/abci_query`: query the ABCI application + async fn abci_query( + &self, + path: Option, + data: V, + height: Option, + prove: bool, + ) -> Result + where + V: Into> + Send, + { + Ok(self + .perform(tendermint_rpc::endpoint::abci_query::Request::new( + path, data, height, prove, + )) + .await? + .response) + } + + /// `/block_results`: get ABCI results for the latest block. + async fn latest_block_results( + &self, + ) -> Result { + self.perform(block_results::Request::default()).await + } + + /// `/blockchain`: get block headers for `min` <= `height` <= `max`. + /// + /// Block headers are returned in descending order (highest first). + /// + /// Returns at most 20 items. + async fn blockchain( + &self, + min: H, + max: H, + ) -> Result + where + H: Into + Send, + { + // TODO(tarcieri): return errors for invalid params before making + // request? + self.perform(blockchain::Request::new(min.into(), max.into())) + .await + } + + /// `/commit`: get block commit at a given height. + async fn commit(&self, height: H) -> Result + where + H: Into + Send, + { + self.perform(commit::Request::new(height.into())).await + } + + /// `/consensus_params`: get current consensus parameters at the specified + /// height. + async fn consensus_params( + &self, + height: H, + ) -> Result + where + H: Into + Send, + { + self.perform(consensus_params::Request::new(Some(height.into()))) + .await + } + + /// `/consensus_state`: get current consensus state + async fn consensus_state( + &self, + ) -> Result { + self.perform(consensus_state::Request::new()).await + } + + /// `/consensus_params`: get the latest consensus parameters. + async fn latest_consensus_params( + &self, + ) -> Result { + self.perform(consensus_params::Request::new(None)).await + } + + /// `/commit`: get the latest block commit + async fn latest_commit(&self) -> Result { + self.perform(commit::Request::default()).await + } + + /// `/health`: get node health. + /// + /// Returns empty result (200 OK) on success, no response in case of an + /// error. + async fn health(&self) -> Result<(), Error> { + self.perform(health::Request).await?; + Ok(()) + } + + /// `/net_info`: obtain information about P2P and other network connections. + async fn net_info(&self) -> Result { + self.perform(net_info::Request).await + } + + /// `/status`: get Tendermint status including node info, pubkey, latest + /// block hash, app hash, block height and time. + async fn status(&self) -> Result { + self.perform(status::Request).await + } + + /// Perform a request against the RPC endpoint + async fn perform(&self, request: R) -> Result + where + R: tendermint_rpc::SimpleRequest; +} + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum Error { + #[error("{0}")] + Tendermint(#[from] tendermint_rpc::Error), + #[error("Decoding error: {0}")] + Decoding(#[from] std::io::Error), + #[error("Info log: {0}, error code: {1}")] + Query(String, u32), + #[error("Invalid block height: {0} (overflown i64)")] + InvalidHeight(BlockHeight), +} + +#[async_trait::async_trait(?Send)] +impl Client for C { + type Error = Error; + + async fn request( + &self, + path: String, + data: Option>, + height: Option, + prove: bool, + ) -> Result { + let data = data.unwrap_or_default(); + let height = height + .map(|height| { + tendermint::block::Height::try_from(height.0) + .map_err(|_err| Error::InvalidHeight(height)) + }) + .transpose()?; + let response = self + .abci_query( + // TODO open the private Path constructor in tendermint-rpc + Some(std::str::FromStr::from_str(&path).unwrap()), + data, + height, + prove, + ) + .await?; + use tendermint::abci::Code; + match response.code { + Code::Ok => Ok(EncodedResponseQuery { + data: response.value, + info: response.info, + proof: response.proof, + }), + Code::Err(code) => Err(Error::Query(response.info, code)), + } + } + + async fn perform(&self, request: R) -> Result + where + R: tendermint_rpc::SimpleRequest, + { + tendermint_rpc::Client::perform(self, request).await + } } /// Temporary domain-type for `tendermint_proto::abci::RequestQuery`, copied diff --git a/shared/src/ledger/rpc.rs b/shared/src/ledger/rpc.rs new file mode 100644 index 0000000000..a1360f3f24 --- /dev/null +++ b/shared/src/ledger/rpc.rs @@ -0,0 +1,852 @@ +//! SDK RPC queries +use std::collections::{HashMap, HashSet}; + +use borsh::BorshDeserialize; +use itertools::Itertools; +use masp_primitives::asset_type::AssetType; +use masp_primitives::merkle_tree::MerklePath; +use masp_primitives::sapling::Node; +use namada_core::types::address::Address; +use serde::Serialize; +use tokio::time::Duration; + +use crate::ledger::events::Event; +use crate::ledger::governance::parameters::GovParams; +use crate::ledger::governance::storage as gov_storage; +use crate::ledger::native_vp::governance::utils::Votes; +use crate::ledger::pos::types::decimal_mult_u64; +use crate::ledger::pos::{self, BondId, Bonds, Slash}; +use crate::ledger::queries::RPC; +use crate::proto::Tx; +use crate::tendermint::merkle::proof::Proof; +use crate::tendermint_rpc::error::Error as TError; +use crate::tendermint_rpc::query::Query; +use crate::tendermint_rpc::Order; +use crate::types::governance::{ + ProposalResult, ProposalVote, TallyResult, VotePower, +}; +use crate::types::hash::Hash; +use crate::types::key::*; +use crate::types::storage::{BlockHeight, BlockResults, Epoch, PrefixValue}; +use crate::types::token::balance_key; +use crate::types::{storage, token}; + +/// Query the status of a given transaction. +/// +/// If a response is not delivered until `deadline`, we exit the cli with an +/// error. +pub async fn query_tx_status( + client: &C, + status: TxEventQuery<'_>, + deadline: Duration, +) -> Event { + const ONE_SECOND: Duration = Duration::from_secs(1); + // sleep for the duration of `backoff`, + // and update the underlying value + async fn sleep_update(query: TxEventQuery<'_>, backoff: &mut Duration) { + tracing::debug!( + ?query, + duration = ?backoff, + "Retrying tx status query after timeout", + ); + // simple linear backoff - if an event is not available, + // increase the backoff duration by one second + async_std::task::sleep(*backoff).await; + *backoff += ONE_SECOND; + } + + let mut backoff = ONE_SECOND; + loop { + tracing::debug!(query = ?status, "Querying tx status"); + let maybe_event = match query_tx_events(client, status).await { + Ok(response) => response, + Err(_err) => { + // tracing::debug!(%err, "ABCI query failed"); + sleep_update(status, &mut backoff).await; + continue; + } + }; + if let Some(e) = maybe_event { + break e; + } else if deadline < backoff { + panic!( + "Transaction status query deadline of {deadline:?} exceeded" + ); + } else { + sleep_update(status, &mut backoff).await; + } + } +} + +/// Query the epoch of the last committed block +pub async fn query_epoch( + client: &C, +) -> Epoch { + let epoch = unwrap_client_response::(RPC.shell().epoch(client).await); + println!("Last committed epoch: {}", epoch); + epoch +} + +/// Query the last committed block +pub async fn query_block( + client: &C, +) -> crate::tendermint_rpc::endpoint::block::Response { + let response = client.latest_block().await.unwrap(); + println!( + "Last committed block ID: {}, height: {}, time: {}", + response.block_id, + response.block.header.height, + response.block.header.time + ); + response +} + +/// A helper to unwrap client's response. Will shut down process on error. +fn unwrap_client_response( + response: Result, +) -> T { + response.unwrap_or_else(|_err| { + panic!("Error in the query"); + }) +} + +/// Query the results of the last committed block +pub async fn query_results( + client: &C, +) -> Vec { + unwrap_client_response::(RPC.shell().read_results(client).await) +} + +/// Query token amount of owner. +pub async fn get_token_balance( + client: &C, + token: &Address, + owner: &Address, +) -> Option { + let balance_key = balance_key(token, owner); + query_storage_value(client, &balance_key).await +} + +/// Get account's public key stored in its storage sub-space +pub async fn get_public_key( + client: &C, + address: &Address, +) -> Option { + let key = pk_key(address); + query_storage_value(client, &key).await +} + +/// Check if the given address is a known validator. +pub async fn is_validator( + client: &C, + address: &Address, +) -> bool { + unwrap_client_response::( + RPC.vp().pos().is_validator(client, address).await, + ) +} + +/// Check if a given address is a known delegator +pub async fn is_delegator( + client: &C, + address: &Address, +) -> bool { + let bonds_prefix = pos::bonds_for_source_prefix(address); + let bonds = + query_storage_prefix::(client, &bonds_prefix).await; + bonds.is_some() && bonds.unwrap().count() > 0 +} + +/// Check if a given address is a known delegator at the given epoch +pub async fn is_delegator_at( + client: &C, + address: &Address, + epoch: Epoch, +) -> bool { + let key = pos::bonds_for_source_prefix(address); + let bonds_iter = query_storage_prefix::(client, &key).await; + if let Some(mut bonds) = bonds_iter { + bonds.any(|(_, bond)| bond.get(epoch).is_some()) + } else { + false + } +} + +/// Check if the address exists on chain. Established address exists if it has a +/// stored validity predicate. Implicit and internal addresses always return +/// true. +pub async fn known_address( + client: &C, + address: &Address, +) -> bool { + match address { + Address::Established(_) => { + // Established account exists if it has a VP + let key = storage::Key::validity_predicate(address); + query_has_storage_key(client, &key).await + } + Address::Implicit(_) | Address::Internal(_) => true, + } +} + +/// Query a conversion. +pub async fn query_conversion( + client: &C, + asset_type: AssetType, +) -> Option<( + Address, + Epoch, + masp_primitives::transaction::components::Amount, + MerklePath, +)> { + Some(unwrap_client_response::( + RPC.shell().read_conversion(client, &asset_type).await, + )) +} + +/// Query a storage value and decode it with [`BorshDeserialize`]. +pub async fn query_storage_value( + client: &C, + key: &storage::Key, +) -> Option +where + T: BorshDeserialize, + C: crate::ledger::queries::Client + Sync, +{ + // In case `T` is a unit (only thing that encodes to 0 bytes), we have to + // use `storage_has_key` instead of `storage_value`, because `storage_value` + // returns 0 bytes when the key is not found. + let maybe_unit = T::try_from_slice(&[]); + if let Ok(unit) = maybe_unit { + return if unwrap_client_response::( + RPC.shell().storage_has_key(client, key).await, + ) { + Some(unit) + } else { + None + }; + } + + let response = unwrap_client_response::( + RPC.shell() + .storage_value(client, None, None, false, key) + .await, + ); + if response.data.is_empty() { + return None; + } + T::try_from_slice(&response.data[..]) + .map(Some) + .unwrap_or_else(|err| { + panic!("Error decoding the value: {}", err); + }) +} + +/// Query a storage value and the proof without decoding. +pub async fn query_storage_value_bytes< + C: crate::ledger::queries::Client + Sync, +>( + client: &C, + key: &storage::Key, + height: Option, + prove: bool, +) -> (Option>, Option) { + let data = None; + let response = unwrap_client_response::( + RPC.shell() + .storage_value(client, data, height, prove, key) + .await, + ); + if response.data.is_empty() { + (None, response.proof) + } else { + (Some(response.data), response.proof) + } +} + +/// Query a range of storage values with a matching prefix and decode them with +/// [`BorshDeserialize`]. Returns an iterator of the storage keys paired with +/// their associated values. +pub async fn query_storage_prefix( + client: &C, + key: &storage::Key, +) -> Option> +where + T: BorshDeserialize, +{ + let values = unwrap_client_response::( + RPC.shell() + .storage_prefix(client, None, None, false, key) + .await, + ); + let decode = + |PrefixValue { key, value }: PrefixValue| match T::try_from_slice( + &value[..], + ) { + Err(err) => { + eprintln!( + "Skipping a value for key {}. Error in decoding: {}", + key, err + ); + None + } + Ok(value) => Some((key, value)), + }; + if values.data.is_empty() { + None + } else { + Some(values.data.into_iter().filter_map(decode)) + } +} + +/// Query to check if the given storage key exists. +pub async fn query_has_storage_key( + client: &C, + key: &storage::Key, +) -> bool { + unwrap_client_response::( + RPC.shell().storage_has_key(client, key).await, + ) +} + +/// Represents a query for an event pertaining to the specified transaction +#[derive(Debug, Copy, Clone)] +pub enum TxEventQuery<'a> { + /// Queries whether transaction with given hash was accepted + Accepted(&'a str), + /// Queries whether transaction with given hash was applied + Applied(&'a str), +} + +impl<'a> TxEventQuery<'a> { + /// The event type to which this event query pertains + pub fn event_type(self) -> &'static str { + match self { + TxEventQuery::Accepted(_) => "accepted", + TxEventQuery::Applied(_) => "applied", + } + } + + /// The transaction to which this event query pertains + pub fn tx_hash(self) -> &'a str { + match self { + TxEventQuery::Accepted(tx_hash) => tx_hash, + TxEventQuery::Applied(tx_hash) => tx_hash, + } + } +} + +/// Transaction event queries are semantically a subset of general queries +impl<'a> From> for Query { + fn from(tx_query: TxEventQuery<'a>) -> Self { + match tx_query { + TxEventQuery::Accepted(tx_hash) => { + Query::default().and_eq("accepted.hash", tx_hash) + } + TxEventQuery::Applied(tx_hash) => { + Query::default().and_eq("applied.hash", tx_hash) + } + } + } +} + +/// Call the corresponding `tx_event_query` RPC method, to fetch +/// the current status of a transation. +pub async fn query_tx_events( + client: &C, + tx_event_query: TxEventQuery<'_>, +) -> std::result::Result< + Option, + ::Error, +> { + let tx_hash: Hash = tx_event_query.tx_hash().try_into().unwrap(); + match tx_event_query { + TxEventQuery::Accepted(_) => { + RPC.shell().accepted(client, &tx_hash).await + } + /*.wrap_err_with(|| { + eyre!("Failed querying whether a transaction was accepted") + })*/, + TxEventQuery::Applied(_) => RPC.shell().applied(client, &tx_hash).await, /*.wrap_err_with(|| { + eyre!("Error querying whether a transaction was applied") + })*/ + } +} + +/// Dry run a transaction +pub async fn dry_run_tx( + client: &C, + tx_bytes: Vec, +) -> namada_core::types::transaction::TxResult { + let (data, height, prove) = (Some(tx_bytes), None, false); + unwrap_client_response::( + RPC.shell().dry_run_tx(client, data, height, prove).await, + ) + .data +} + +/// Data needed for broadcasting a tx and +/// monitoring its progress on chain +/// +/// Txs may be either a dry run or else +/// they should be encrypted and included +/// in a wrapper. +#[derive(Debug, Clone)] +pub enum TxBroadcastData { + /// Dry run broadcast data + DryRun(Tx), + /// Wrapper broadcast data + Wrapper { + /// Transaction to broadcast + tx: Tx, + /// Hash of the wrapper transaction + wrapper_hash: String, + /// Hash of decrypted transaction + decrypted_hash: String, + }, +} + +/// A parsed event from tendermint relating to a transaction +#[derive(Debug, Serialize)] +pub struct TxResponse { + /// Response information + pub info: String, + /// Response log + pub log: String, + /// Block height + pub height: String, + /// Transaction height + pub hash: String, + /// Response code + pub code: String, + /// Gas used + pub gas_used: String, + /// Initialized accounts + pub initialized_accounts: Vec
, +} + +impl TryFrom for TxResponse { + type Error = String; + + fn try_from(event: Event) -> Result { + fn missing_field_err(field: &str) -> String { + format!("Field \"{field}\" not present in event") + } + + let hash = event + .get("hash") + .ok_or_else(|| missing_field_err("hash"))? + .clone(); + let info = event + .get("info") + .ok_or_else(|| missing_field_err("info"))? + .clone(); + let log = event + .get("log") + .ok_or_else(|| missing_field_err("log"))? + .clone(); + let height = event + .get("height") + .ok_or_else(|| missing_field_err("height"))? + .clone(); + let code = event + .get("code") + .ok_or_else(|| missing_field_err("code"))? + .clone(); + let gas_used = event + .get("gas_used") + .ok_or_else(|| missing_field_err("gas_used"))? + .clone(); + let initialized_accounts = event + .get("initialized_accounts") + .map(String::as_str) + // TODO: fix finalize block, to return initialized accounts, + // even when we reject a tx? + .map_or(Ok(vec![]), |initialized_accounts| { + serde_json::from_str(initialized_accounts) + .map_err(|err| format!("JSON decode error: {err}")) + })?; + + Ok(TxResponse { + hash, + info, + log, + height, + code, + gas_used, + initialized_accounts, + }) + } +} + +impl TxResponse { + /// Convert an [`Event`] to a [`TxResponse`], or error out. + pub fn from_event(event: Event) -> Self { + event.try_into().unwrap_or_else(|err| { + panic!("Error fetching TxResponse: {err}"); + }) + } +} + +/// Lookup the full response accompanying the specified transaction event +// TODO: maybe remove this in favor of `query_tx_status` +pub async fn query_tx_response( + client: &C, + tx_query: TxEventQuery<'_>, +) -> Result { + // Find all blocks that apply a transaction with the specified hash + let blocks = &client + .block_search(tx_query.into(), 1, 255, Order::Ascending) + .await + .expect("Unable to query for transaction with given hash") + .blocks; + // Get the block results corresponding to a block to which + // the specified transaction belongs + let block = &blocks + .get(0) + .ok_or_else(|| { + TError::server( + "Unable to find a block applying the given transaction" + .to_string(), + ) + })? + .block; + let response_block_results = client + .block_results(block.header.height) + .await + .expect("Unable to retrieve block containing transaction"); + // Search for the event where the specified transaction is + // applied to the blockchain + let query_event_opt = + response_block_results.end_block_events.and_then(|events| { + events + .iter() + .find(|event| { + event.type_str == tx_query.event_type() + && event.attributes.iter().any(|tag| { + tag.key.as_ref() == "hash" + && tag.value.as_ref() == tx_query.tx_hash() + }) + }) + .cloned() + }); + let query_event = query_event_opt.ok_or_else(|| { + TError::server( + "Unable to find the event corresponding to the specified \ + transaction" + .to_string(), + ) + })?; + // Reformat the event attributes so as to ease value extraction + let event_map: std::collections::HashMap<&str, &str> = query_event + .attributes + .iter() + .map(|tag| (tag.key.as_ref(), tag.value.as_ref())) + .collect(); + // Summarize the transaction results that we were searching for + let result = TxResponse { + info: event_map["info"].to_string(), + log: event_map["log"].to_string(), + height: event_map["height"].to_string(), + hash: event_map["hash"].to_string(), + code: event_map["code"].to_string(), + gas_used: event_map["gas_used"].to_string(), + initialized_accounts: serde_json::from_str( + event_map["initialized_accounts"], + ) + .unwrap_or_default(), + }; + Ok(result) +} + +/// Get the votes for a given proposal id +pub async fn get_proposal_votes( + client: &C, + epoch: Epoch, + proposal_id: u64, +) -> Votes { + let validators = get_all_validators(client, epoch).await; + + let vote_prefix_key = + gov_storage::get_proposal_vote_prefix_key(proposal_id); + let vote_iter = + query_storage_prefix::(client, &vote_prefix_key).await; + + let mut yay_validators: HashMap = HashMap::new(); + let mut yay_delegators: HashMap> = + HashMap::new(); + let mut nay_delegators: HashMap> = + HashMap::new(); + + if let Some(vote_iter) = vote_iter { + for (key, vote) in vote_iter { + let voter_address = gov_storage::get_voter_address(&key) + .expect("Vote key should contain the voting address.") + .clone(); + if vote.is_yay() && validators.contains(&voter_address) { + let amount: VotePower = + get_validator_stake(client, epoch, &voter_address) + .await + .into(); + yay_validators.insert(voter_address, amount); + } else if !validators.contains(&voter_address) { + let validator_address = + gov_storage::get_vote_delegation_address(&key) + .expect( + "Vote key should contain the delegation address.", + ) + .clone(); + let delegator_token_amount = get_bond_amount_at( + client, + &voter_address, + &validator_address, + epoch, + ) + .await; + if let Some(amount) = delegator_token_amount { + if vote.is_yay() { + let entry = + yay_delegators.entry(voter_address).or_default(); + entry + .insert(validator_address, VotePower::from(amount)); + } else { + let entry = + nay_delegators.entry(voter_address).or_default(); + entry + .insert(validator_address, VotePower::from(amount)); + } + } + } + } + } + + Votes { + yay_validators, + yay_delegators, + nay_delegators, + } +} + +/// Get all validators in the given epoch +pub async fn get_all_validators( + client: &C, + epoch: Epoch, +) -> HashSet
{ + unwrap_client_response::( + RPC.vp() + .pos() + .validator_addresses(client, &Some(epoch)) + .await, + ) +} + +/// Get the total staked tokens in the given epoch +pub async fn get_total_staked_tokens< + C: crate::ledger::queries::Client + Sync, +>( + client: &C, + epoch: Epoch, +) -> token::Amount { + unwrap_client_response::( + RPC.vp().pos().total_stake(client, &Some(epoch)).await, + ) +} + +/// Get the given validator's stake at the given epoch +pub async fn get_validator_stake( + client: &C, + epoch: Epoch, + validator: &Address, +) -> token::Amount { + unwrap_client_response::( + RPC.vp() + .pos() + .validator_stake(client, validator, &Some(epoch)) + .await, + ) +} + +/// Get the delegator's delegation +pub async fn get_delegators_delegation< + C: crate::ledger::queries::Client + Sync, +>( + client: &C, + address: &Address, +) -> HashSet
{ + unwrap_client_response::( + RPC.vp().pos().delegations(client, address).await, + ) +} + +/// Get the givernance parameters +pub async fn get_governance_parameters< + C: crate::ledger::queries::Client + Sync, +>( + client: &C, +) -> GovParams { + use crate::types::token::Amount; + let key = gov_storage::get_max_proposal_code_size_key(); + let max_proposal_code_size = query_storage_value::(client, &key) + .await + .expect("Parameter should be definied."); + + let key = gov_storage::get_max_proposal_content_key(); + let max_proposal_content_size = query_storage_value::(client, &key) + .await + .expect("Parameter should be definied."); + + let key = gov_storage::get_min_proposal_fund_key(); + let min_proposal_fund = query_storage_value::(client, &key) + .await + .expect("Parameter should be definied."); + + let key = gov_storage::get_min_proposal_grace_epoch_key(); + let min_proposal_grace_epochs = query_storage_value::(client, &key) + .await + .expect("Parameter should be definied."); + + let key = gov_storage::get_min_proposal_period_key(); + let min_proposal_period = query_storage_value::(client, &key) + .await + .expect("Parameter should be definied."); + + let key = gov_storage::get_max_proposal_period_key(); + let max_proposal_period = query_storage_value::(client, &key) + .await + .expect("Parameter should be definied."); + + GovParams { + min_proposal_fund: u64::from(min_proposal_fund), + max_proposal_code_size, + min_proposal_period, + max_proposal_period, + max_proposal_content_size, + min_proposal_grace_epochs, + } +} + +/// Compute the result of a proposal +pub async fn compute_tally( + client: &C, + epoch: Epoch, + votes: Votes, +) -> ProposalResult { + let total_staked_tokens: VotePower = + get_total_staked_tokens(client, epoch).await.into(); + + let Votes { + yay_validators, + yay_delegators, + nay_delegators, + } = votes; + + let mut total_yay_staked_tokens = VotePower::from(0_u64); + for (_, amount) in yay_validators.clone().into_iter() { + total_yay_staked_tokens += amount; + } + + // YAY: Add delegator amount whose validator didn't vote / voted nay + for (_, vote_map) in yay_delegators.iter() { + for (validator_address, vote_power) in vote_map.iter() { + if !yay_validators.contains_key(validator_address) { + total_yay_staked_tokens += vote_power; + } + } + } + + // NAY: Remove delegator amount whose validator validator vote yay + for (_, vote_map) in nay_delegators.iter() { + for (validator_address, vote_power) in vote_map.iter() { + if yay_validators.contains_key(validator_address) { + total_yay_staked_tokens -= vote_power; + } + } + } + + if total_yay_staked_tokens >= (total_staked_tokens / 3) * 2 { + ProposalResult { + result: TallyResult::Passed, + total_voting_power: total_staked_tokens, + total_yay_power: total_yay_staked_tokens, + total_nay_power: 0, + } + } else { + ProposalResult { + result: TallyResult::Rejected, + total_voting_power: total_staked_tokens, + total_yay_power: total_yay_staked_tokens, + total_nay_power: 0, + } + } +} + +/// Get the bond amount at the given epoch +pub async fn get_bond_amount_at( + client: &C, + delegator: &Address, + validator: &Address, + epoch: Epoch, +) -> Option { + let slashes_key = pos::validator_slashes_key(validator); + let slashes = query_storage_value::(client, &slashes_key) + .await + .unwrap_or_default(); + let bond_key = pos::bond_key(&BondId { + source: delegator.clone(), + validator: validator.clone(), + }); + let epoched_bonds = + query_storage_value::(client, &bond_key).await; + match epoched_bonds { + Some(epoched_bonds) => { + let mut delegated_amount: token::Amount = 0.into(); + for bond in epoched_bonds.iter() { + let mut to_deduct = bond.neg_deltas; + for (epoch_start, &(mut delta)) in + bond.pos_deltas.iter().sorted() + { + // deduct bond's neg_deltas + if to_deduct > delta { + to_deduct -= delta; + // If the whole bond was deducted, continue to + // the next one + continue; + } else { + delta -= to_deduct; + to_deduct = token::Amount::default(); + } + + delta = apply_slashes(&slashes, delta, *epoch_start, None); + if epoch >= *epoch_start { + delegated_amount += delta; + } + } + } + Some(delegated_amount) + } + None => None, + } +} + +/// Accumulate slashes starting from `epoch_start` until (optionally) +/// `withdraw_epoch` and apply them to the token amount `delta`. +fn apply_slashes( + slashes: &[Slash], + mut delta: token::Amount, + epoch_start: Epoch, + withdraw_epoch: Option, +) -> token::Amount { + let mut slashed = token::Amount::default(); + for slash in slashes { + if slash.epoch >= epoch_start + && slash.epoch < withdraw_epoch.unwrap_or_else(|| u64::MAX.into()) + { + let raw_delta: u64 = delta.into(); + let current_slashed = + token::Amount::from(decimal_mult_u64(slash.rate, raw_delta)); + slashed += current_slashed; + delta -= current_slashed; + } + } + delta +} diff --git a/shared/src/ledger/signing.rs b/shared/src/ledger/signing.rs new file mode 100644 index 0000000000..940cc56598 --- /dev/null +++ b/shared/src/ledger/signing.rs @@ -0,0 +1,206 @@ +//! Functions to sign transactions +use borsh::BorshSerialize; +use namada_core::types::address::{Address, ImplicitAddress}; + +use crate::ledger::rpc::TxBroadcastData; +use crate::ledger::tx::Error; +use crate::ledger::wallet::{Wallet, WalletUtils}; +use crate::ledger::{args, rpc}; +use crate::proto::Tx; +use crate::types::key::*; +use crate::types::storage::Epoch; +use crate::types::transaction::{hash_tx, Fee, WrapperTx}; + +/// Find the public key for the given address and try to load the keypair +/// for it from the wallet. If the keypair is encrypted but a password is not +/// supplied, then it is interactively prompted. Errors if the key cannot be +/// found or loaded. +pub async fn find_keypair< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + addr: &Address, + password: Option, +) -> Result { + match addr { + Address::Established(_) => { + println!( + "Looking-up public key of {} from the ledger...", + addr.encode() + ); + let public_key = rpc::get_public_key(client, addr).await.ok_or( + Error::Other(format!( + "No public key found for the address {}", + addr.encode() + )), + )?; + wallet.find_key_by_pk(&public_key, password).map_err(|err| { + Error::Other(format!( + "Unable to load the keypair from the wallet for public \ + key {}. Failed with: {}", + public_key, err + )) + }) + } + Address::Implicit(ImplicitAddress(pkh)) => { + wallet.find_key_by_pkh(pkh, password).map_err(|err| { + Error::Other(format!( + "Unable to load the keypair from the wallet for the \ + implicit address {}. Failed with: {}", + addr.encode(), + err + )) + }) + } + Address::Internal(_) => other_err(format!( + "Internal address {} doesn't have any signing keys.", + addr + )), + } +} + +/// Carries types that can be directly/indirectly used to sign a transaction. +#[allow(clippy::large_enum_variant)] +#[derive(Clone)] +pub enum TxSigningKey { + /// Do not sign any transaction + None, + /// Obtain the actual keypair from wallet and use that to sign + WalletKeypair(common::SecretKey), + /// Obtain the keypair corresponding to given address from wallet and sign + WalletAddress(Address), + /// Directly use the given secret key to sign transactions + SecretKey(common::SecretKey), +} + +/// Given CLI arguments and some defaults, determine the rightful transaction +/// signer. Return the given signing key or public key of the given signer if +/// possible. If no explicit signer given, use the `default`. If no `default` +/// is given, an `Error` is returned. +pub async fn tx_signer< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + args: &args::Tx, + default: TxSigningKey, +) -> Result { + // Override the default signing key source if possible + let default = if let Some(signing_key) = &args.signing_key { + TxSigningKey::WalletKeypair(signing_key.clone()) + } else if let Some(signer) = &args.signer { + TxSigningKey::WalletAddress(signer.clone()) + } else { + default + }; + // Now actually fetch the signing key and apply it + match default { + TxSigningKey::WalletKeypair(signing_key) => Ok(signing_key), + TxSigningKey::WalletAddress(signer) => { + let signer = signer; + let signing_key = find_keypair::( + client, + wallet, + &signer, + args.password.clone(), + ) + .await?; + // Check if the signer is implicit account that needs to reveal its + // PK first + if matches!(signer, Address::Implicit(_)) { + let pk: common::PublicKey = signing_key.ref_to(); + super::tx::reveal_pk_if_needed::( + client, wallet, &pk, args, + ) + .await?; + } + Ok(signing_key) + } + TxSigningKey::SecretKey(signing_key) => { + // Check if the signing key needs to reveal its PK first + let pk: common::PublicKey = signing_key.ref_to(); + super::tx::reveal_pk_if_needed::(client, wallet, &pk, args) + .await?; + Ok(signing_key) + } + TxSigningKey::None => other_err( + "All transactions must be signed; please either specify the key \ + or the address from which to look up the signing key." + .to_string(), + ), + } +} + +/// Sign a transaction with a given signing key or public key of a given signer. +/// If no explicit signer given, use the `default`. If no `default` is given, +/// Error. +/// +/// If this is not a dry run, the tx is put in a wrapper and returned along with +/// hashes needed for monitoring the tx on chain. +/// +/// If it is a dry run, it is not put in a wrapper, but returned as is. +pub async fn sign_tx< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + tx: Tx, + args: &args::Tx, + default: TxSigningKey, +) -> Result { + let keypair = tx_signer::(client, wallet, args, default).await?; + let tx = tx.sign(&keypair); + + let epoch = rpc::query_epoch(client).await; + Ok(if args.dry_run { + TxBroadcastData::DryRun(tx) + } else { + sign_wrapper(args, epoch, tx, &keypair).await + }) +} + +/// Create a wrapper tx from a normal tx. Get the hash of the +/// wrapper and its payload which is needed for monitoring its +/// progress on chain. +pub async fn sign_wrapper( + args: &args::Tx, + epoch: Epoch, + tx: Tx, + keypair: &common::SecretKey, +) -> TxBroadcastData { + let tx = { + WrapperTx::new( + Fee { + amount: args.fee_amount, + token: args.fee_token.clone(), + }, + keypair, + epoch, + args.gas_limit.clone(), + tx, + // TODO: Actually use the fetched encryption key + Default::default(), + ) + }; + + // We use this to determine when the wrapper tx makes it on-chain + let wrapper_hash = hash_tx(&tx.try_to_vec().unwrap()).to_string(); + // We use this to determine when the decrypted inner tx makes it + // on-chain + let decrypted_hash = tx.tx_hash.to_string(); + TxBroadcastData::Wrapper { + tx: tx + .sign(keypair) + .expect("Wrapper tx signing keypair should be correct"), + wrapper_hash, + decrypted_hash, + } +} + +fn other_err(string: String) -> Result { + Err(Error::Other(string)) +} diff --git a/shared/src/ledger/tx.rs b/shared/src/ledger/tx.rs new file mode 100644 index 0000000000..bd1206fa1a --- /dev/null +++ b/shared/src/ledger/tx.rs @@ -0,0 +1,1381 @@ +//! SDK functions to construct different types of transactions +use std::borrow::Cow; + +use borsh::BorshSerialize; +use itertools::Either::*; +use masp_primitives::transaction::builder; +use namada_core::types::address::{masp, masp_tx_key, Address}; +use prost::EncodeError; +use rust_decimal::Decimal; +use thiserror::Error; +use tokio::time::Duration; + +use crate::ibc::applications::ics20_fungible_token_transfer::msgs::transfer::MsgTransfer; +use crate::ibc::signer::Signer; +use crate::ibc::timestamp::Timestamp as IbcTimestamp; +use crate::ibc::tx_msg::Msg; +use crate::ibc::Height as IbcHeight; +use crate::ibc_proto::cosmos::base::v1beta1::Coin; +use crate::ledger::args; +use crate::ledger::governance::storage as gov_storage; +use crate::ledger::masp::{ShieldedContext, ShieldedUtils}; +use crate::ledger::pos::{BondId, Bonds, CommissionRates, Unbonds}; +use crate::ledger::rpc::{self, TxBroadcastData, TxResponse}; +use crate::ledger::signing::{find_keypair, sign_tx, tx_signer, TxSigningKey}; +use crate::ledger::wallet::{Wallet, WalletUtils}; +use crate::proto::Tx; +use crate::tendermint_rpc::endpoint::broadcast::tx_sync::Response; +use crate::tendermint_rpc::error::Error as RpcError; +use crate::types::key::*; +use crate::types::masp::TransferTarget; +use crate::types::storage::{Epoch, RESERVED_ADDRESS_PREFIX}; +use crate::types::time::DateTimeUtc; +use crate::types::transaction::{pos, InitAccount, UpdateVp}; +use crate::types::{storage, token}; +use crate::vm::WasmValidationError; +use crate::{ledger, vm}; + +/// Default timeout in seconds for requests to the `/accepted` +/// and `/applied` ABCI query endpoints. +const DEFAULT_NAMADA_EVENTS_MAX_WAIT_TIME_SECONDS: u64 = 60; + +/// Errors to do with transaction events. +#[derive(Error, Debug)] +pub enum Error { + /// Expect a dry running transaction + #[error( + "Expected a dry-run transaction, received a wrapper transaction \ + instead: {0:?}" + )] + ExpectDryRun(Tx), + /// Expect a wrapped encrypted running transaction + #[error("Cannot broadcast a dry-run transaction")] + ExpectWrappedRun(Tx), + /// Error during broadcasting a transaction + #[error("Encountered error while broadcasting transaction: {0}")] + TxBroadcast(RpcError), + /// Invalid comission rate set + #[error("Invalid new commission rate, received {0}")] + InvalidCommisionRate(Decimal), + /// Invalid validator address + #[error("The address {0} doesn't belong to any known validator account.")] + InvalidValidatorAddress(Address), + /// Rate of epoch change too large for current epoch + #[error( + "New rate, {0}, is too large of a change with respect to the \ + predecessor epoch in which the rate will take effect." + )] + TooLargeOfChange(Decimal), + /// Error retrieving from storage + #[error("Error retrieving from storage")] + Retrival, + /// No unbonded bonds ready to withdraw in the current epoch + #[error( + "There are no unbonded bonds ready to withdraw in the current epoch \ + {0}." + )] + NoUnbondReady(Epoch), + /// No unbonded bonds found + #[error("No unbonded bonds found")] + NoUnbondFound, + /// No bonds found + #[error("No bonds found")] + NoBondFound, + /// Lower bond amount than the unbond + #[error( + "The total bonds of the source {0} is lower than the amount to be \ + unbonded. Amount to unbond is {1} and the total bonds is {2}." + )] + LowerBondThanUnbond(Address, token::Amount, token::Amount), + /// Balance is too low + #[error( + "The balance of the source {0} of token {1} is lower than the amount \ + to be transferred. Amount to transfer is {2} and the balance is {3}." + )] + BalanceTooLow(Address, Address, token::Amount, token::Amount), + /// Token Address does not exist on chain + #[error("The token address {0} doesn't exist on chain.")] + TokenDoesNotExist(Address), + /// Source address does not exist on chain + #[error("The address {0} doesn't exist on chain.")] + LocationDoesNotExist(Address), + /// Target Address does not exist on chain + #[error("The source address {0} doesn't exist on chain.")] + SourceDoesNotExist(Address), + /// Source Address does not exist on chain + #[error("The target address {0} doesn't exist on chain.")] + TargetLocationDoesNotExist(Address), + /// No Balance found for token + #[error("No balance found for the source {0} of token {1}")] + NoBalanceForToken(Address, Address), + /// Negative balance after transfer + #[error( + "The balance of the source {0} is lower than the amount to be \ + transferred and fees. Amount to transfer is {1} {2} and fees are {3} \ + {4}." + )] + NegativeBalanceAfterTransfer( + Address, + token::Amount, + Address, + token::Amount, + Address, + ), + /// No Balance found for token + #[error("{0}")] + MaspError(builder::Error), + /// Wasm validation failed + #[error("Validity predicate code validation failed with {0}")] + WasmValidationFailure(WasmValidationError), + /// Encoding transaction failure + #[error("Encoding tx data, {0}, shouldn't fail")] + EncodeTxFailure(std::io::Error), + /// Like EncodeTxFailure but for the encode error type + #[error("Encoding tx data, {0}, shouldn't fail")] + EncodeFailure(EncodeError), + /// Encoding public key failure + #[error("Encoding a public key, {0}, shouldn't fail")] + EncodeKeyFailure(std::io::Error), + /// Updating an VP of an implicit account + #[error( + "A validity predicate of an implicit address cannot be directly \ + updated. You can use an established address for this purpose." + )] + ImplicitUpdate, + // This should be removed? or rather refactored as it communicates + // the same information as the ImplicitUpdate + /// Updating a VP of an internal implicit address + #[error( + "A validity predicate of an internal address cannot be directly \ + updated." + )] + ImplicitInternalError, + /// Epoch not in storage + #[error("Proposal end epoch is not in the storage.")] + EpochNotInStorage, + /// Other Errors that may show up when using the interface + #[error("{0}")] + Other(String), +} + +/// Submit transaction and wait for result. Returns a list of addresses +/// initialized in the transaction if any. In dry run, this is always empty. +pub async fn process_tx< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + args: &args::Tx, + tx: Tx, + default_signer: TxSigningKey, +) -> Result, Error> { + let to_broadcast = + sign_tx::(client, wallet, tx, args, default_signer).await?; + // NOTE: use this to print the request JSON body: + + // let request = + // tendermint_rpc::endpoint::broadcast::tx_commit::Request::new( + // tx_bytes.clone().into(), + // ); + // use tendermint_rpc::Request; + // let request_body = request.into_json(); + // println!("HTTP request body: {}", request_body); + + if args.dry_run { + expect_dry_broadcast(to_broadcast, client, vec![]).await + } else { + // Either broadcast or submit transaction and collect result into + // sum type + let result = if args.broadcast_only { + Left(broadcast_tx(client, &to_broadcast).await) + } else { + Right(submit_tx(client, to_broadcast).await) + }; + // Return result based on executed operation, otherwise deal with + // the encountered errors uniformly + match result { + Right(Ok(result)) => Ok(result.initialized_accounts), + Left(Ok(_)) => Ok(Vec::default()), + Right(Err(err)) => Err(err), + Left(Err(err)) => Err(err), + } + } +} + +/// Submit transaction to reveal public key +pub async fn submit_reveal_pk< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + args: args::RevealPk, +) -> Result<(), Error> { + let args::RevealPk { + tx: args, + public_key, + } = args; + let public_key = public_key; + if !reveal_pk_if_needed::(client, wallet, &public_key, &args).await? { + let addr: Address = (&public_key).into(); + println!("PK for {addr} is already revealed, nothing to do."); + Ok(()) + } else { + Ok(()) + } +} + +/// Submit transaction to rveeal public key if needed +pub async fn reveal_pk_if_needed< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + public_key: &common::PublicKey, + args: &args::Tx, +) -> Result { + let addr: Address = public_key.into(); + // Check if PK revealed + if args.force || !has_revealed_pk(client, &addr).await { + // If not, submit it + submit_reveal_pk_aux::(client, wallet, public_key, args).await?; + Ok(true) + } else { + Ok(false) + } +} + +/// Check if the public key for the given address has been revealed +pub async fn has_revealed_pk( + client: &C, + addr: &Address, +) -> bool { + rpc::get_public_key(client, addr).await.is_some() +} + +/// Submit transaction to reveal the given public key +pub async fn submit_reveal_pk_aux< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + public_key: &common::PublicKey, + args: &args::Tx, +) -> Result<(), Error> { + let addr: Address = public_key.into(); + println!("Submitting a tx to reveal the public key for address {addr}..."); + let tx_data = public_key.try_to_vec().map_err(Error::EncodeKeyFailure)?; + let tx_code = args.tx_code_path.clone(); + let tx = Tx::new(tx_code, Some(tx_data)); + + // submit_tx without signing the inner tx + let keypair = if let Some(signing_key) = &args.signing_key { + Ok(signing_key.clone()) + } else if let Some(signer) = args.signer.as_ref() { + let signer = signer; + find_keypair::(client, wallet, signer, args.password.clone()) + .await + } else { + find_keypair::(client, wallet, &addr, args.password.clone()).await + }?; + let epoch = rpc::query_epoch(client).await; + let to_broadcast = if args.dry_run { + TxBroadcastData::DryRun(tx) + } else { + super::signing::sign_wrapper(args, epoch, tx, &keypair).await + }; + + // Logic is the same as process_tx + if args.dry_run { + expect_dry_broadcast(to_broadcast, client, ()).await + } else { + // Either broadcast or submit transaction and collect result into + // sum type + let result = if args.broadcast_only { + Left(broadcast_tx(client, &to_broadcast).await) + } else { + Right(submit_tx(client, to_broadcast).await) + }; + // Return result based on executed operation, otherwise deal with + // the encountered errors uniformly + match result { + Right(Err(err)) => Err(err), + Left(Err(err)) => Err(err), + _ => Ok(()), + } + } +} + +/// Broadcast a transaction to be included in the blockchain and checks that +/// the tx has been successfully included into the mempool of a validator +/// +/// In the case of errors in any of those stages, an error message is returned +pub async fn broadcast_tx( + rpc_cli: &C, + to_broadcast: &TxBroadcastData, +) -> Result { + let (tx, wrapper_tx_hash, decrypted_tx_hash) = match to_broadcast { + TxBroadcastData::Wrapper { + tx, + wrapper_hash, + decrypted_hash, + } => Ok((tx, wrapper_hash, decrypted_hash)), + TxBroadcastData::DryRun(tx) => Err(Error::ExpectWrappedRun(tx.clone())), + }?; + + tracing::debug!( + transaction = ?to_broadcast, + "Broadcasting transaction", + ); + + // TODO: configure an explicit timeout value? we need to hack away at + // `tendermint-rs` for this, which is currently using a hard-coded 30s + // timeout. + let response = + lift_rpc_error(rpc_cli.broadcast_tx_sync(tx.to_bytes().into()).await)?; + + if response.code == 0.into() { + println!("Transaction added to mempool: {:?}", response); + // Print the transaction identifiers to enable the extraction of + // acceptance/application results later + { + println!("Wrapper transaction hash: {:?}", wrapper_tx_hash); + println!("Inner transaction hash: {:?}", decrypted_tx_hash); + } + Ok(response) + } else { + Err(Error::TxBroadcast(RpcError::server( + serde_json::to_string(&response).unwrap(), + ))) + } +} + +/// Broadcast a transaction to be included in the blockchain. +/// +/// Checks that +/// 1. The tx has been successfully included into the mempool of a validator +/// 2. The tx with encrypted payload has been included on the blockchain +/// 3. The decrypted payload of the tx has been included on the blockchain. +/// +/// In the case of errors in any of those stages, an error message is returned +pub async fn submit_tx( + client: &C, + to_broadcast: TxBroadcastData, +) -> Result { + let (_, wrapper_hash, decrypted_hash) = match &to_broadcast { + TxBroadcastData::Wrapper { + tx, + wrapper_hash, + decrypted_hash, + } => Ok((tx, wrapper_hash, decrypted_hash)), + TxBroadcastData::DryRun(tx) => Err(Error::ExpectWrappedRun(tx.clone())), + }?; + + // Broadcast the supplied transaction + broadcast_tx(client, &to_broadcast).await?; + + let deadline = + Duration::from_secs(DEFAULT_NAMADA_EVENTS_MAX_WAIT_TIME_SECONDS); + + tracing::debug!( + transaction = ?to_broadcast, + ?deadline, + "Awaiting transaction approval", + ); + + let parsed = { + let wrapper_query = + crate::ledger::rpc::TxEventQuery::Accepted(wrapper_hash.as_str()); + let event = rpc::query_tx_status(client, wrapper_query, deadline).await; + let parsed = TxResponse::from_event(event); + + println!( + "Transaction accepted with result: {}", + serde_json::to_string_pretty(&parsed).unwrap() + ); + // The transaction is now on chain. We wait for it to be decrypted + // and applied + if parsed.code == 0.to_string() { + // We also listen to the event emitted when the encrypted + // payload makes its way onto the blockchain + let decrypted_query = + rpc::TxEventQuery::Applied(decrypted_hash.as_str()); + let event = + rpc::query_tx_status(client, decrypted_query, deadline).await; + let parsed = TxResponse::from_event(event); + println!( + "Transaction applied with result: {}", + serde_json::to_string_pretty(&parsed).unwrap() + ); + Ok(parsed) + } else { + Ok(parsed) + } + }; + + tracing::debug!( + transaction = ?to_broadcast, + "Transaction approved", + ); + + parsed +} + +/// Save accounts initialized from a tx into the wallet, if any. +pub async fn save_initialized_accounts( + wallet: &mut Wallet, + args: &args::Tx, + initialized_accounts: Vec
, +) { + let len = initialized_accounts.len(); + if len != 0 { + // Store newly initialized account addresses in the wallet + println!( + "The transaction initialized {} new account{}", + len, + if len == 1 { "" } else { "s" } + ); + // Store newly initialized account addresses in the wallet + for (ix, address) in initialized_accounts.iter().enumerate() { + let encoded = address.encode(); + let alias: Cow = match &args.initialized_account_alias { + Some(initialized_account_alias) => { + if len == 1 { + // If there's only one account, use the + // alias as is + initialized_account_alias.into() + } else { + // If there're multiple accounts, use + // the alias as prefix, followed by + // index number + format!("{}{}", initialized_account_alias, ix).into() + } + } + None => U::read_alias(&encoded).into(), + }; + let alias = alias.into_owned(); + let added = wallet.add_address(alias.clone(), address.clone()); + match added { + Some(new_alias) if new_alias != encoded => { + println!( + "Added alias {} for address {}.", + new_alias, encoded + ); + } + _ => println!("No alias added for address {}.", encoded), + }; + } + } +} + +/// Submit validator comission rate change +pub async fn submit_validator_commission_change< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + args: args::TxCommissionRateChange, +) -> Result<(), Error> { + let epoch = rpc::query_epoch(client).await; + + let tx_code = args.tx_code_path; + + let validator = args.validator.clone(); + if rpc::is_validator(client, &validator).await { + if args.rate < Decimal::ZERO || args.rate > Decimal::ONE { + if args.tx.force { + eprintln!( + "Invalid new commission rate, received {}", + args.rate + ); + Ok(()) + } else { + Err(Error::InvalidCommisionRate(args.rate)) + } + } else { + Ok(()) + }?; + + let commission_rate_key = + ledger::pos::validator_commission_rate_key(&validator); + let max_commission_rate_change_key = + ledger::pos::validator_max_commission_rate_change_key(&validator); + let commission_rates = rpc::query_storage_value::( + client, + &commission_rate_key, + ) + .await; + let max_change = rpc::query_storage_value::( + client, + &max_commission_rate_change_key, + ) + .await; + + match (commission_rates, max_change) { + (Some(rates), Some(max_change)) => { + // Assuming that pipeline length = 2 + let rate_next_epoch = rates.get(epoch.next()).unwrap(); + let epoch_change = (args.rate - rate_next_epoch).abs(); + if epoch_change > max_change { + if args.tx.force { + eprintln!( + "New rate, {epoch_change}, is too large of a \ + change with respect to the predecessor epoch in \ + which the rate will take effect." + ); + Ok(()) + } else { + Err(Error::TooLargeOfChange(epoch_change)) + } + } else { + Ok(()) + } + } + _ => { + if args.tx.force { + eprintln!("Error retrieving from storage"); + Ok(()) + } else { + Err(Error::Retrival) + } + } + }?; + Ok(()) + } else if args.tx.force { + eprintln!("The given address {validator} is not a validator."); + Ok(()) + } else { + Err(Error::InvalidValidatorAddress(validator)) + }?; + + let data = pos::CommissionChange { + validator: args.validator.clone(), + new_rate: args.rate, + }; + let data = data.try_to_vec().map_err(Error::EncodeTxFailure)?; + + let tx = Tx::new(tx_code, Some(data)); + let default_signer = args.validator.clone(); + process_tx::( + client, + wallet, + &args.tx, + tx, + TxSigningKey::WalletAddress(default_signer), + ) + .await?; + Ok(()) +} + +/// Submit transaction to withdraw an unbond +pub async fn submit_withdraw< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + args: args::Withdraw, +) -> Result<(), Error> { + let epoch = rpc::query_epoch(client).await; + + let validator = + known_validator_or_err(args.validator.clone(), args.tx.force, client) + .await?; + + let source = args.source.clone(); + let tx_code = args.tx_code_path; + + // Check the source's current unbond amount + let bond_source = source.clone().unwrap_or_else(|| validator.clone()); + let bond_id = BondId { + source: bond_source.clone(), + validator: validator.clone(), + }; + let bond_key = ledger::pos::unbond_key(&bond_id); + let unbonds = + rpc::query_storage_value::(client, &bond_key).await; + match unbonds { + Some(unbonds) => { + let mut unbonded_amount: token::Amount = 0.into(); + if let Some(unbond) = unbonds.get(epoch) { + for delta in unbond.deltas.values() { + unbonded_amount += *delta; + } + } + if unbonded_amount == 0.into() { + if args.tx.force { + eprintln!( + "There are no unbonded bonds ready to withdraw in the \ + current epoch {}.", + epoch + ); + Ok(()) + } else { + Err(Error::NoUnbondReady(epoch)) + } + } else { + Ok(()) + } + } + None => { + if args.tx.force { + eprintln!("No unbonded bonds found"); + Ok(()) + } else { + Err(Error::NoUnbondFound) + } + } + }?; + + let data = pos::Withdraw { validator, source }; + let data = data.try_to_vec().map_err(Error::EncodeTxFailure)?; + + let tx = Tx::new(tx_code, Some(data)); + let default_signer = args.source.unwrap_or(args.validator); + process_tx::( + client, + wallet, + &args.tx, + tx, + TxSigningKey::WalletAddress(default_signer), + ) + .await?; + Ok(()) +} + +/// Submit a transaction to unbond +pub async fn submit_unbond< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + args: args::Unbond, +) -> Result<(), Error> { + let validator = + known_validator_or_err(args.validator.clone(), args.tx.force, client) + .await?; + let source = args.source.clone(); + let tx_code = args.tx_code_path; + + // Check the source's current bond amount + let bond_source = source.clone().unwrap_or_else(|| validator.clone()); + let bond_id = BondId { + source: bond_source.clone(), + validator: validator.clone(), + }; + let bond_key = ledger::pos::bond_key(&bond_id); + let bonds = rpc::query_storage_value::(client, &bond_key).await; + match bonds { + Some(bonds) => { + let mut bond_amount: token::Amount = 0.into(); + for bond in bonds.iter() { + for delta in bond.pos_deltas.values() { + bond_amount += *delta; + } + } + if args.amount > bond_amount { + if args.tx.force { + eprintln!( + "The total bonds of the source {} is lower than the \ + amount to be unbonded. Amount to unbond is {} and \ + the total bonds is {}.", + bond_source, args.amount, bond_amount + ); + Ok(()) + } else { + Err(Error::LowerBondThanUnbond( + bond_source, + args.amount, + bond_amount, + )) + } + } else { + Ok(()) + } + } + None => { + if args.tx.force { + eprintln!("No bonds found"); + Ok(()) + } else { + Err(Error::NoBondFound) + } + } + }?; + + let data = pos::Unbond { + validator, + amount: args.amount, + source, + }; + let data = data.try_to_vec().map_err(Error::EncodeTxFailure)?; + + let tx = Tx::new(tx_code, Some(data)); + let default_signer = args.source.unwrap_or(args.validator); + process_tx::( + client, + wallet, + &args.tx, + tx, + TxSigningKey::WalletAddress(default_signer), + ) + .await?; + Ok(()) +} + +/// Submit a transaction to bond +pub async fn submit_bond< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + args: args::Bond, +) -> Result<(), Error> { + let validator = + known_validator_or_err(args.validator.clone(), args.tx.force, client) + .await?; + + // Check that the source address exists on chain + let source = args.source.clone(); + let source = match args.source.clone() { + Some(source) => source_exists_or_err(source, args.tx.force, client) + .await + .map(Some), + None => Ok(source), + }?; + // 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(&args.native_token, bond_source); + + // TODO Should we state the same error message for the native token? + check_balance_too_low_err( + &args.native_token, + bond_source, + args.amount, + balance_key, + args.tx.force, + client, + ) + .await?; + + let tx_code = args.tx_code_path; + let bond = pos::Bond { + validator, + amount: args.amount, + source, + }; + let data = bond.try_to_vec().map_err(Error::EncodeTxFailure)?; + + let tx = Tx::new(tx_code, Some(data)); + let default_signer = args.source.unwrap_or(args.validator); + process_tx::( + client, + wallet, + &args.tx, + tx, + TxSigningKey::WalletAddress(default_signer), + ) + .await?; + Ok(()) +} + +/// Check if current epoch is in the last third of the voting period of the +/// proposal. This ensures that it is safe to optimize the vote writing to +/// storage. +pub async fn is_safe_voting_window( + client: &C, + proposal_id: u64, + proposal_start_epoch: Epoch, +) -> Result { + let current_epoch = rpc::query_epoch(client).await; + + let proposal_end_epoch_key = + gov_storage::get_voting_end_epoch_key(proposal_id); + let proposal_end_epoch = + rpc::query_storage_value::(client, &proposal_end_epoch_key) + .await; + + match proposal_end_epoch { + Some(proposal_end_epoch) => { + Ok(!crate::ledger::native_vp::governance::utils::is_valid_validator_voting_period( + current_epoch, + proposal_start_epoch, + proposal_end_epoch, + )) + } + None => { + Err(Error::EpochNotInStorage) + } + } +} + +/// Submit an IBC transfer +pub async fn submit_ibc_transfer< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + args: args::TxIbcTransfer, +) -> Result<(), Error> { + // Check that the source address exists on chain + let source = + source_exists_or_err(args.source.clone(), args.tx.force, client) + .await?; + // We cannot check the receiver + + let token = token_exists_or_err(args.token, args.tx.force, client).await?; + + // Check source balance + let (sub_prefix, balance_key) = match args.sub_prefix { + Some(sub_prefix) => { + let sub_prefix = storage::Key::parse(sub_prefix).unwrap(); + let prefix = token::multitoken_balance_prefix(&token, &sub_prefix); + ( + Some(sub_prefix), + token::multitoken_balance_key(&prefix, &source), + ) + } + None => (None, token::balance_key(&token, &source)), + }; + + check_balance_too_low_err( + &token, + &source, + args.amount, + balance_key, + args.tx.force, + client, + ) + .await?; + + let tx_code = args.tx_code_path; + + let denom = match sub_prefix { + // To parse IbcToken address, remove the address prefix + Some(sp) => sp.to_string().replace(RESERVED_ADDRESS_PREFIX, ""), + None => token.to_string(), + }; + let token = Some(Coin { + denom, + amount: args.amount.to_string(), + }); + + // this height should be that of the destination chain, not this chain + let timeout_height = match args.timeout_height { + Some(h) => IbcHeight::new(0, h), + None => IbcHeight::zero(), + }; + + let now: crate::tendermint::Time = DateTimeUtc::now().try_into().unwrap(); + let now: IbcTimestamp = now.into(); + let timeout_timestamp = if let Some(offset) = args.timeout_sec_offset { + (now + Duration::new(offset, 0)).unwrap() + } else if timeout_height.is_zero() { + // we cannot set 0 to both the height and the timestamp + (now + Duration::new(3600, 0)).unwrap() + } else { + IbcTimestamp::none() + }; + + let msg = MsgTransfer { + source_port: args.port_id, + source_channel: args.channel_id, + token, + sender: Signer::new(source.to_string()), + receiver: Signer::new(args.receiver), + timeout_height, + timeout_timestamp, + }; + tracing::debug!("IBC transfer message {:?}", msg); + let any_msg = msg.to_any(); + let mut data = vec![]; + prost::Message::encode(&any_msg, &mut data) + .map_err(Error::EncodeFailure)?; + + let tx = Tx::new(tx_code, Some(data)); + process_tx::( + client, + wallet, + &args.tx, + tx, + TxSigningKey::WalletAddress(args.source), + ) + .await?; + Ok(()) +} + +/// Submit an ordinary transfer +pub async fn submit_transfer< + C: crate::ledger::queries::Client + Sync, + V: WalletUtils, + U: ShieldedUtils, +>( + client: &C, + wallet: &mut Wallet, + shielded: &mut ShieldedContext, + args: args::TxTransfer, +) -> Result<(), Error> { + // Check that the source address exists on chain + let force = args.tx.force; + let transfer_source = args.source.clone(); + let source = source_exists_or_err( + transfer_source.effective_address(), + force, + client, + ) + .await?; + // Check that the target address exists on chain + let transfer_target = args.target.clone(); + let target = target_exists_or_err( + transfer_target.effective_address(), + force, + client, + ) + .await?; + + // Check that the token address exists on chain + let token = + &(token_exists_or_err(args.token.clone(), force, client).await?); + + // Check source balance + let (sub_prefix, balance_key) = match &args.sub_prefix { + Some(sub_prefix) => { + let sub_prefix = storage::Key::parse(sub_prefix).unwrap(); + let prefix = token::multitoken_balance_prefix(token, &sub_prefix); + ( + Some(sub_prefix), + token::multitoken_balance_key(&prefix, &source), + ) + } + None => (None, token::balance_key(token, &source)), + }; + + check_balance_too_low_err( + token, + &source, + args.amount, + balance_key, + args.tx.force, + client, + ) + .await?; + + let tx_code = args.tx_code_path.clone(); + let masp_addr = masp(); + // For MASP sources, use a special sentinel key recognized by VPs as default + // signer. Also, if the transaction is shielded, redact the amount and token + // types by setting the transparent value to 0 and token type to a constant. + // This has no side-effect because transaction is to self. + let (default_signer, amount, token) = + if source == masp_addr && target == masp_addr { + // TODO Refactor me, we shouldn't rely on any specific token here. + ( + TxSigningKey::SecretKey(masp_tx_key()), + 0.into(), + args.native_token.clone(), + ) + } else if source == masp_addr { + ( + TxSigningKey::SecretKey(masp_tx_key()), + args.amount, + token.clone(), + ) + } else { + ( + TxSigningKey::WalletAddress(source.clone()), + args.amount, + token.clone(), + ) + }; + // 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::(client, wallet, &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 &args.target { + TransferTarget::PaymentAddress(pa) if pa.is_pinned() => Some(pa.hash()), + _ => None, + }; + + let stx_result = shielded + .gen_shielded_transfer(client, args.clone(), shielded_gas) + .await; + let shielded = match stx_result { + Ok(stx) => Ok(stx.map(|x| x.0)), + Err(builder::Error::ChangeIsNegative(_)) => { + Err(Error::NegativeBalanceAfterTransfer( + source.clone(), + args.amount, + token.clone(), + args.tx.fee_amount, + args.tx.fee_token.clone(), + )) + } + Err(err) => Err(Error::MaspError(err)), + }?; + + let transfer = token::Transfer { + source: source.clone(), + target, + token, + sub_prefix, + amount, + key, + shielded, + }; + tracing::debug!("Transfer data {:?}", transfer); + let data = transfer.try_to_vec().map_err(Error::EncodeTxFailure)?; + + let tx = Tx::new(tx_code, Some(data)); + let signing_address = TxSigningKey::WalletAddress(source); + process_tx::(client, wallet, &args.tx, tx, signing_address).await?; + Ok(()) +} + +/// Submit a transaction to initialize an account +pub async fn submit_init_account< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + args: args::TxInitAccount, +) -> Result<(), Error> { + let public_key = args.public_key; + let vp_code = args.vp_code_path; + // Validate the VP code + validate_untrusted_code_err(&vp_code, args.tx.force)?; + + let tx_code = args.tx_code_path; + let data = InitAccount { + public_key, + vp_code, + }; + let data = data.try_to_vec().map_err(Error::EncodeTxFailure)?; + let tx = Tx::new(tx_code, Some(data)); + // TODO Move unwrap to an either + let initialized_accounts = process_tx::( + client, + wallet, + &args.tx, + tx, + TxSigningKey::WalletAddress(args.source), + ) + .await + .unwrap(); + save_initialized_accounts::(wallet, &args.tx, initialized_accounts) + .await; + Ok(()) +} + +/// Submit a transaction to update a VP +pub async fn submit_update_vp< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + args: args::TxUpdateVp, +) -> Result<(), Error> { + let addr = args.addr.clone(); + + // Check that the address is established and exists on chain + match &addr { + Address::Established(_) => { + let exists = rpc::known_address::(client, &addr).await; + if !exists { + if args.tx.force { + eprintln!("The address {} doesn't exist on chain.", addr); + Ok(()) + } else { + Err(Error::LocationDoesNotExist(addr.clone())) + } + } else { + Ok(()) + } + } + Address::Implicit(_) => { + if args.tx.force { + eprintln!( + "A validity predicate of an implicit address cannot be \ + directly updated. You can use an established address for \ + this purpose." + ); + Ok(()) + } else { + Err(Error::ImplicitUpdate) + } + } + Address::Internal(_) => { + if args.tx.force { + eprintln!( + "A validity predicate of an internal address cannot be \ + directly updated." + ); + Ok(()) + } else { + Err(Error::ImplicitInternalError) + } + } + }?; + + let vp_code = args.vp_code_path; + // Validate the VP code + if let Err(err) = vm::validate_untrusted_wasm(&vp_code) { + if args.tx.force { + eprintln!("Validity predicate code validation failed with {}", err); + Ok(()) + } else { + Err(Error::WasmValidationFailure(err)) + } + } else { + Ok(()) + }?; + + let tx_code = args.tx_code_path; + + let data = UpdateVp { addr, vp_code }; + let data = data.try_to_vec().map_err(Error::EncodeTxFailure)?; + + let tx = Tx::new(tx_code, Some(data)); + process_tx::( + client, + wallet, + &args.tx, + tx, + TxSigningKey::WalletAddress(args.addr), + ) + .await?; + Ok(()) +} + +/// Submit a custom transaction +pub async fn submit_custom< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + args: args::TxCustom, +) -> Result<(), Error> { + let tx_code = args.code_path; + let data = args.data_path; + let tx = Tx::new(tx_code, data); + let initialized_accounts = + process_tx::(client, wallet, &args.tx, tx, TxSigningKey::None) + .await?; + save_initialized_accounts::(wallet, &args.tx, initialized_accounts) + .await; + Ok(()) +} + +async fn expect_dry_broadcast( + to_broadcast: TxBroadcastData, + client: &C, + ret: T, +) -> Result { + match to_broadcast { + TxBroadcastData::DryRun(tx) => { + rpc::dry_run_tx(client, tx.to_bytes()).await; + Ok(ret) + } + TxBroadcastData::Wrapper { + tx, + wrapper_hash: _, + decrypted_hash: _, + } => Err(Error::ExpectDryRun(tx)), + } +} + +fn lift_rpc_error(res: Result) -> Result { + res.map_err(Error::TxBroadcast) +} + +/// Returns the given validator if the given address is a validator, +/// otherwise returns an error, force forces the address through even +/// if it isn't a validator +async fn known_validator_or_err( + validator: Address, + force: bool, + client: &C, +) -> Result { + // Check that the validator address exists on chain + let is_validator = rpc::is_validator(client, &validator).await; + if !is_validator { + if force { + eprintln!( + "The address {} doesn't belong to any known validator account.", + validator + ); + Ok(validator) + } else { + Err(Error::InvalidValidatorAddress(validator)) + } + } else { + Ok(validator) + } +} + +/// general pattern for checking if an address exists on the chain, or +/// throwing an error if it's not forced. Takes a generic error +/// message and the error type. +async fn address_exists_or_err( + addr: Address, + force: bool, + client: &C, + message: String, + err: F, +) -> Result +where + C: crate::ledger::queries::Client + Sync, + F: FnOnce(Address) -> Error, +{ + let addr_exists = rpc::known_address::(client, &addr).await; + if !addr_exists { + if force { + eprintln!("{}", message); + Ok(addr) + } else { + Err(err(addr)) + } + } else { + Ok(addr) + } +} + +/// Returns the given token if the given address exists on chain +/// otherwise returns an error, force forces the address through even +/// if it isn't on chain +async fn token_exists_or_err( + token: Address, + force: bool, + client: &C, +) -> Result { + let message = + format!("The token address {} doesn't exist on chain.", token); + address_exists_or_err( + token, + force, + client, + message, + Error::TokenDoesNotExist, + ) + .await +} + +/// Returns the given source address if the given address exists on chain +/// otherwise returns an error, force forces the address through even +/// if it isn't on chain +async fn source_exists_or_err( + token: Address, + force: bool, + client: &C, +) -> Result { + let message = + format!("The source address {} doesn't exist on chain.", token); + address_exists_or_err( + token, + force, + client, + message, + Error::SourceDoesNotExist, + ) + .await +} + +/// Returns the given target address if the given address exists on chain +/// otherwise returns an error, force forces the address through even +/// if it isn't on chain +async fn target_exists_or_err( + token: Address, + force: bool, + client: &C, +) -> Result { + let message = + format!("The target address {} doesn't exist on chain.", token); + address_exists_or_err( + token, + force, + client, + message, + Error::TargetLocationDoesNotExist, + ) + .await +} + +/// checks the balance at the given address is enough to transfer the +/// given amount, along with the balance even existing. force +/// overrides this +async fn check_balance_too_low_err( + token: &Address, + source: &Address, + amount: token::Amount, + balance_key: storage::Key, + force: bool, + client: &C, +) -> Result<(), Error> { + match rpc::query_storage_value::(client, &balance_key) + .await + { + Some(balance) => { + if balance < amount { + if force { + eprintln!( + "The balance of the source {} of token {} is lower \ + than the amount to be transferred. Amount to \ + transfer is {} and the balance is {}.", + source, token, amount, balance + ); + Ok(()) + } else { + Err(Error::BalanceTooLow( + source.clone(), + token.clone(), + amount, + balance, + )) + } + } else { + Ok(()) + } + } + None => { + if force { + eprintln!( + "No balance found for the source {} of token {}", + source, token + ); + Ok(()) + } else { + Err(Error::NoBalanceForToken(source.clone(), token.clone())) + } + } + } +} + +fn validate_untrusted_code_err( + vp_code: &Vec, + force: bool, +) -> Result<(), Error> { + if let Err(err) = vm::validate_untrusted_wasm(vp_code) { + if force { + eprintln!("Validity predicate code validation failed with {}", err); + Ok(()) + } else { + Err(Error::WasmValidationFailure(err)) + } + } else { + Ok(()) + } +} diff --git a/shared/src/ledger/wallet/alias.rs b/shared/src/ledger/wallet/alias.rs new file mode 100644 index 0000000000..13d977b852 --- /dev/null +++ b/shared/src/ledger/wallet/alias.rs @@ -0,0 +1,103 @@ +//! Wallet address and key aliases. + +use std::convert::Infallible; +use std::fmt::Display; +use std::hash::Hash; +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; + +/// Aliases created from raw strings are kept in-memory as given, but their +/// `Serialize` and `Display` instance converts them to lowercase. Their +/// `PartialEq` instance is case-insensitive. +#[derive(Clone, Debug, Default, Deserialize, PartialOrd, Ord, Eq)] +#[serde(transparent)] +pub struct Alias(String); + +impl Alias { + /// Normalize an alias to lower-case + pub fn normalize(&self) -> String { + self.0.to_lowercase() + } + + /// Returns the length of the underlying `String`. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Is the underlying `String` empty? + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl Serialize for Alias { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.normalize().serialize(serializer) + } +} + +impl PartialEq for Alias { + fn eq(&self, other: &Self) -> bool { + self.normalize() == other.normalize() + } +} + +impl Hash for Alias { + fn hash(&self, state: &mut H) { + self.normalize().hash(state); + } +} + +impl From for Alias +where + T: AsRef, +{ + fn from(raw: T) -> Self { + Self(raw.as_ref().to_owned()) + } +} + +impl From for String { + fn from(alias: Alias) -> Self { + alias.normalize() + } +} + +impl<'a> From<&'a Alias> for String { + fn from(alias: &'a Alias) -> Self { + alias.normalize() + } +} + +impl Display for Alias { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.normalize().fmt(f) + } +} + +impl FromStr for Alias { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(Self(s.into())) + } +} + +/// Default alias of a validator's account key +pub fn validator_key(validator_alias: &Alias) -> Alias { + format!("{validator_alias}-validator-key").into() +} + +/// Default alias of a validator's consensus key +pub fn validator_consensus_key(validator_alias: &Alias) -> Alias { + format!("{validator_alias}-consensus-key").into() +} + +/// Default alias of a validator's Tendermint node key +pub fn validator_tendermint_node_key(validator_alias: &Alias) -> Alias { + format!("{validator_alias}-tendermint-node-key").into() +} diff --git a/shared/src/ledger/wallet/keys.rs b/shared/src/ledger/wallet/keys.rs new file mode 100644 index 0000000000..ce8c1769dc --- /dev/null +++ b/shared/src/ledger/wallet/keys.rs @@ -0,0 +1,244 @@ +//! Cryptographic keys for digital signatures support for the wallet. + +use std::fmt::Display; +use std::marker::PhantomData; +use std::str::FromStr; + +use borsh::{BorshDeserialize, BorshSerialize}; +use data_encoding::HEXLOWER; +use orion::{aead, kdf}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::ledger::wallet::WalletUtils; + +const ENCRYPTED_KEY_PREFIX: &str = "encrypted:"; +const UNENCRYPTED_KEY_PREFIX: &str = "unencrypted:"; + +/// A keypair stored in a wallet +#[derive(Debug)] +pub enum StoredKeypair +where + ::Err: Display, +{ + /// An encrypted keypair + Encrypted(EncryptedKeypair), + /// An raw (unencrypted) keypair + Raw(T), +} + +impl Serialize + for StoredKeypair +where + ::Err: Display, +{ + fn serialize( + &self, + serializer: S, + ) -> std::result::Result + where + S: serde::Serializer, + { + // String encoded, because toml doesn't support enums + match self { + StoredKeypair::Encrypted(encrypted) => { + let keypair_string = + format!("{}{}", ENCRYPTED_KEY_PREFIX, encrypted); + serde::Serialize::serialize(&keypair_string, serializer) + } + StoredKeypair::Raw(raw) => { + let keypair_string = + format!("{}{}", UNENCRYPTED_KEY_PREFIX, raw); + serde::Serialize::serialize(&keypair_string, serializer) + } + } + } +} + +impl<'de, T: BorshSerialize + BorshDeserialize + Display + FromStr> + Deserialize<'de> for StoredKeypair +where + ::Err: Display, +{ + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + + let keypair_string: String = + serde::Deserialize::deserialize(deserializer) + .map_err(|err| { + DeserializeStoredKeypairError::InvalidStoredKeypairString( + err.to_string(), + ) + }) + .map_err(D::Error::custom)?; + if let Some(raw) = keypair_string.strip_prefix(UNENCRYPTED_KEY_PREFIX) { + FromStr::from_str(raw) + .map(|keypair| Self::Raw(keypair)) + .map_err(|err| { + DeserializeStoredKeypairError::InvalidStoredKeypairString( + err.to_string(), + ) + }) + .map_err(D::Error::custom) + } else if let Some(encrypted) = + keypair_string.strip_prefix(ENCRYPTED_KEY_PREFIX) + { + FromStr::from_str(encrypted) + .map(Self::Encrypted) + .map_err(|err| { + DeserializeStoredKeypairError::InvalidStoredKeypairString( + err.to_string(), + ) + }) + .map_err(D::Error::custom) + } else { + Err(DeserializeStoredKeypairError::MissingPrefix) + .map_err(D::Error::custom) + } + } +} + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum DeserializeStoredKeypairError { + #[error("The stored keypair is not valid: {0}")] + InvalidStoredKeypairString(String), + #[error("The stored keypair is missing a prefix")] + MissingPrefix, +} + +/// An encrypted keypair stored in a wallet +#[derive(Debug)] +pub struct EncryptedKeypair( + Vec, + PhantomData, +); + +impl Display for EncryptedKeypair { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", HEXLOWER.encode(self.0.as_ref())) + } +} + +impl FromStr for EncryptedKeypair { + type Err = data_encoding::DecodeError; + + fn from_str(s: &str) -> Result { + HEXLOWER.decode(s.as_ref()).map(|x| Self(x, PhantomData)) + } +} + +#[allow(missing_docs)] +#[derive(Debug, Error)] +pub enum DecryptionError { + #[error("Unexpected encryption salt")] + BadSalt, + #[error("Unable to decrypt the keypair. Is the password correct?")] + DecryptionError, + #[error("Unable to deserialize the keypair")] + DeserializingError, + #[error("Asked not to decrypt")] + NotDecrypting, +} + +impl + StoredKeypair +where + ::Err: Display, +{ + /// Construct a keypair for storage. If no password is provided, the keypair + /// will be stored raw without encryption. Returns the key for storing and a + /// reference-counting point to the raw key. + pub fn new(keypair: T, password: Option) -> (Self, T) { + match password { + Some(password) => ( + Self::Encrypted(EncryptedKeypair::new(&keypair, password)), + keypair, + ), + None => (Self::Raw(keypair.clone()), keypair), + } + } + + /// Get a raw keypair from a stored keypair. If the keypair is encrypted and + /// no password is provided in the argument, a password will be prompted + /// from stdin. + pub fn get( + &self, + decrypt: bool, + password: Option, + ) -> Result { + match self { + StoredKeypair::Encrypted(encrypted_keypair) => { + if decrypt { + let password = password.unwrap_or_else(|| { + U::read_password("Enter decryption password: ") + }); + let key = encrypted_keypair.decrypt(password)?; + Ok(key) + } else { + Err(DecryptionError::NotDecrypting) + } + } + StoredKeypair::Raw(keypair) => Ok(keypair.clone()), + } + } + + /// Indicates whether this key has been encrypted or not + pub fn is_encrypted(&self) -> bool { + match self { + StoredKeypair::Encrypted(_) => true, + StoredKeypair::Raw(_) => false, + } + } +} + +impl EncryptedKeypair { + /// Encrypt a keypair and store it with its salt. + pub fn new(keypair: &T, password: String) -> Self { + let salt = encryption_salt(); + let encryption_key = encryption_key(&salt, password); + + let data = keypair + .try_to_vec() + .expect("Serializing keypair shouldn't fail"); + + let encrypted_keypair = aead::seal(&encryption_key, &data) + .expect("Encryption of data shouldn't fail"); + + let encrypted_data = [salt.as_ref(), &encrypted_keypair].concat(); + + Self(encrypted_data, PhantomData) + } + + /// Decrypt an encrypted keypair + pub fn decrypt(&self, password: String) -> Result { + let salt_len = encryption_salt().len(); + let (raw_salt, cipher) = self.0.split_at(salt_len); + + let salt = kdf::Salt::from_slice(raw_salt) + .map_err(|_| DecryptionError::BadSalt)?; + + let encryption_key = encryption_key(&salt, password); + + let decrypted_data = aead::open(&encryption_key, cipher) + .map_err(|_| DecryptionError::DecryptionError)?; + + T::try_from_slice(&decrypted_data) + .map_err(|_| DecryptionError::DeserializingError) + } +} + +/// Keypair encryption salt +fn encryption_salt() -> kdf::Salt { + kdf::Salt::default() +} + +/// Make encryption secret key from a password. +fn encryption_key(salt: &kdf::Salt, password: String) -> kdf::SecretKey { + kdf::Password::from_slice(password.as_bytes()) + .and_then(|password| kdf::derive_key(&password, salt, 3, 1 << 17, 32)) + .expect("Generation of encryption secret key shouldn't fail") +} diff --git a/shared/src/ledger/wallet/mod.rs b/shared/src/ledger/wallet/mod.rs new file mode 100644 index 0000000000..7515c1109a --- /dev/null +++ b/shared/src/ledger/wallet/mod.rs @@ -0,0 +1,493 @@ +//! Provides functionality for managing keys and addresses for a user +mod alias; +mod keys; +pub mod pre_genesis; +mod store; + +use std::collections::HashMap; +use std::fmt::Display; +use std::marker::PhantomData; +use std::str::FromStr; + +pub use alias::Alias; +use borsh::{BorshDeserialize, BorshSerialize}; +use masp_primitives::zip32::ExtendedFullViewingKey; +pub use pre_genesis::gen_key_to_store; +pub use store::{gen_sk, Store}; +use thiserror::Error; + +pub use self::keys::{DecryptionError, StoredKeypair}; +pub use self::store::{ConfirmationResponse, ValidatorData, ValidatorKeys}; +use crate::types::address::Address; +use crate::types::key::*; +use crate::types::masp::{ + ExtendedSpendingKey, ExtendedViewingKey, PaymentAddress, +}; + +/// Captures the interactive parts of the wallet's functioning +pub trait WalletUtils { + /// The location where the wallet is stored + type Storage; + /// Read the password for encryption/decryption from the file/env/stdin. + /// Panics if all options are empty/invalid. + fn read_password(prompt_msg: &str) -> String; + + /// Read an alias from the file/env/stdin. + fn read_alias(prompt_msg: &str) -> String; + + /// The given alias has been selected but conflicts with another alias in + /// the store. Offer the user to either replace existing mapping, alter the + /// chosen alias to a name of their chosing, or cancel the aliasing. + fn show_overwrite_confirmation( + alias: &Alias, + alias_for: &str, + ) -> store::ConfirmationResponse; +} + +/// A degenerate implementation of wallet interactivity +pub struct SdkWalletUtils(PhantomData); + +impl WalletUtils for SdkWalletUtils { + type Storage = U; + + fn read_password(_prompt_msg: &str) -> String { + panic!("attempted to prompt for password in non-interactive mode"); + } + + fn read_alias(_prompt_msg: &str) -> String { + panic!("attempted to prompt for alias in non-interactive mode"); + } + + fn show_overwrite_confirmation( + _alias: &Alias, + _alias_for: &str, + ) -> store::ConfirmationResponse { + // Automatically replace aliases in non-interactive mode + store::ConfirmationResponse::Replace + } +} + +/// The error that is produced when a given key cannot be obtained +#[derive(Error, Debug)] +pub enum FindKeyError { + /// Could not find a given key in the wallet + #[error("No matching key found")] + KeyNotFound, + /// Could not decrypt a given key in the wallet + #[error("{0}")] + KeyDecryptionError(keys::DecryptionError), +} + +/// Represents a collection of keys and addresses while caching key decryptions +#[derive(Debug)] +pub struct Wallet { + store_dir: U::Storage, + store: Store, + decrypted_key_cache: HashMap, + decrypted_spendkey_cache: HashMap, +} + +impl Wallet { + /// Create a new wallet from the given backing store and storage location + pub fn new(store_dir: U::Storage, store: Store) -> Self { + Self { + store_dir, + store, + decrypted_key_cache: HashMap::default(), + decrypted_spendkey_cache: HashMap::default(), + } + } + + /// Generate a new keypair and derive an implicit address from its public + /// and insert them into the store with the provided alias, converted to + /// lower case. If none provided, the alias will be the public key hash (in + /// lowercase too). If the key is to be encrypted, will prompt for + /// password from stdin. Stores the key in decrypted key cache and + /// returns the alias of the key and a reference-counting pointer to the + /// key. + pub fn gen_key( + &mut self, + scheme: SchemeType, + alias: Option, + password: Option, + ) -> (String, common::SecretKey) { + let (alias, key) = self.store.gen_key::(scheme, alias, password); + // Cache the newly added key + self.decrypted_key_cache.insert(alias.clone(), key.clone()); + (alias.into(), key) + } + + /// Generate a spending key and store it under the given alias in the wallet + pub fn gen_spending_key( + &mut self, + alias: String, + password: Option, + ) -> (String, ExtendedSpendingKey) { + let (alias, key) = self.store.gen_spending_key::(alias, password); + // Cache the newly added key + self.decrypted_spendkey_cache.insert(alias.clone(), key); + (alias.into(), key) + } + + /// Add validator data to the store + pub fn add_validator_data( + &mut self, + address: Address, + keys: ValidatorKeys, + ) { + self.store.add_validator_data(address, keys); + } + + /// Returns the validator data, if it exists. + pub fn get_validator_data(&self) -> Option<&ValidatorData> { + self.store.get_validator_data() + } + + /// Returns the validator data, if it exists. the save function + /// cannot be called after using this method as it involves a + /// partial move + pub fn take_validator_data(&mut self) -> Option<&mut ValidatorData> { + self.store.validator_data() + } + + /// Find the stored key by an alias, a public key hash or a public key. + /// If the key is encrypted and password not supplied, then password will be + /// interactively prompted. Any keys that are decrypted are stored in and + /// read from a cache to avoid prompting for password multiple times. + pub fn find_key( + &mut self, + alias_pkh_or_pk: impl AsRef, + password: Option, + ) -> Result { + // Try cache first + if let Some(cached_key) = self + .decrypted_key_cache + .get(&alias_pkh_or_pk.as_ref().into()) + { + return Ok(cached_key.clone()); + } + // If not cached, look-up in store + let stored_key = self + .store + .find_key(alias_pkh_or_pk.as_ref()) + .ok_or(FindKeyError::KeyNotFound)?; + Self::decrypt_stored_key::<_>( + &mut self.decrypted_key_cache, + stored_key, + alias_pkh_or_pk.into(), + password, + ) + } + + /// Find the spending key with the given alias in the wallet and return it. + /// If the spending key is encrypted but a password is not supplied, then it + /// will be interactively prompted. + pub fn find_spending_key( + &mut self, + alias: impl AsRef, + password: Option, + ) -> Result { + // Try cache first + if let Some(cached_key) = + self.decrypted_spendkey_cache.get(&alias.as_ref().into()) + { + return Ok(*cached_key); + } + // If not cached, look-up in store + let stored_spendkey = self + .store + .find_spending_key(alias.as_ref()) + .ok_or(FindKeyError::KeyNotFound)?; + Self::decrypt_stored_key::<_>( + &mut self.decrypted_spendkey_cache, + stored_spendkey, + alias.into(), + password, + ) + } + + /// Find the viewing key with the given alias in the wallet and return it + pub fn find_viewing_key( + &mut self, + alias: impl AsRef, + ) -> Result<&ExtendedViewingKey, FindKeyError> { + self.store + .find_viewing_key(alias.as_ref()) + .ok_or(FindKeyError::KeyNotFound) + } + + /// Find the payment address with the given alias in the wallet and return + /// it + pub fn find_payment_addr( + &self, + alias: impl AsRef, + ) -> Option<&PaymentAddress> { + self.store.find_payment_addr(alias.as_ref()) + } + + /// Find the stored key by a public key. + /// If the key is encrypted and password not supplied, then password will be + /// interactively prompted for. Any keys that are decrypted are stored in + /// and read from a cache to avoid prompting for password multiple times. + pub fn find_key_by_pk( + &mut self, + pk: &common::PublicKey, + password: Option, + ) -> Result { + // Try to look-up alias for the given pk. Otherwise, use the PKH string. + let pkh: PublicKeyHash = pk.into(); + let alias = self + .store + .find_alias_by_pkh(&pkh) + .unwrap_or_else(|| pkh.to_string().into()); + // Try read cache + if let Some(cached_key) = self.decrypted_key_cache.get(&alias) { + return Ok(cached_key.clone()); + } + // Look-up from store + let stored_key = self + .store + .find_key_by_pk(pk) + .ok_or(FindKeyError::KeyNotFound)?; + Self::decrypt_stored_key::<_>( + &mut self.decrypted_key_cache, + stored_key, + alias, + password, + ) + } + + /// Find the stored key by a public key hash. + /// If the key is encrypted and password is not supplied, then password will + /// be interactively prompted for. Any keys that are decrypted are stored in + /// and read from a cache to avoid prompting for password multiple times. + pub fn find_key_by_pkh( + &mut self, + pkh: &PublicKeyHash, + password: Option, + ) -> Result { + // Try to look-up alias for the given pk. Otherwise, use the PKH string. + let alias = self + .store + .find_alias_by_pkh(pkh) + .unwrap_or_else(|| pkh.to_string().into()); + // Try read cache + if let Some(cached_key) = self.decrypted_key_cache.get(&alias) { + return Ok(cached_key.clone()); + } + // Look-up from store + let stored_key = self + .store + .find_key_by_pkh(pkh) + .ok_or(FindKeyError::KeyNotFound)?; + Self::decrypt_stored_key::<_>( + &mut self.decrypted_key_cache, + stored_key, + alias, + password, + ) + } + + /// Decrypt stored key, if it's not stored un-encrypted. + /// If a given storage key needs to be decrypted and password is not + /// supplied, then interactively prompt for password and if successfully + /// decrypted, store it in a cache. + fn decrypt_stored_key< + T: FromStr + Display + BorshSerialize + BorshDeserialize + Clone, + >( + decrypted_key_cache: &mut HashMap, + stored_key: &StoredKeypair, + alias: Alias, + password: Option, + ) -> Result + where + ::Err: Display, + { + match stored_key { + StoredKeypair::Encrypted(encrypted) => { + let password = password.unwrap_or_else(|| { + U::read_password("Enter decryption password: ") + }); + let key = encrypted + .decrypt(password) + .map_err(FindKeyError::KeyDecryptionError)?; + decrypted_key_cache.insert(alias.clone(), key); + decrypted_key_cache + .get(&alias) + .cloned() + .ok_or(FindKeyError::KeyNotFound) + } + StoredKeypair::Raw(raw) => Ok(raw.clone()), + } + } + + /// Get all known keys by their alias, paired with PKH, if known. + pub fn get_keys( + &self, + ) -> HashMap< + String, + (&StoredKeypair, Option<&PublicKeyHash>), + > { + self.store + .get_keys() + .into_iter() + .map(|(alias, value)| (alias.into(), value)) + .collect() + } + + /// Find the stored address by an alias. + pub fn find_address(&self, alias: impl AsRef) -> Option<&Address> { + self.store.find_address(alias) + } + + /// Find an alias by the address if it's in the wallet. + pub fn find_alias(&self, address: &Address) -> Option<&Alias> { + self.store.find_alias(address) + } + + /// Get all known addresses by their alias, paired with PKH, if known. + pub fn get_addresses(&self) -> HashMap { + self.store + .get_addresses() + .iter() + .map(|(alias, value)| (alias.into(), value.clone())) + .collect() + } + + /// Get all known payment addresses by their alias + pub fn get_payment_addrs(&self) -> HashMap { + self.store + .get_payment_addrs() + .iter() + .map(|(alias, value)| (alias.into(), *value)) + .collect() + } + + /// Get all known viewing keys by their alias + pub fn get_viewing_keys(&self) -> HashMap { + self.store + .get_viewing_keys() + .iter() + .map(|(alias, value)| (alias.into(), *value)) + .collect() + } + + /// Get all known viewing keys by their alias + pub fn get_spending_keys( + &self, + ) -> HashMap> { + self.store + .get_spending_keys() + .iter() + .map(|(alias, value)| (alias.into(), value)) + .collect() + } + + /// Add a new address with the given alias. If the alias is already used, + /// will ask whether the existing alias should be replaced, a different + /// alias is desired, or the alias creation should be cancelled. Return + /// the chosen alias if the address has been added, otherwise return + /// nothing. + pub fn add_address( + &mut self, + alias: impl AsRef, + address: Address, + ) -> Option { + self.store + .insert_address::(alias.into(), address) + .map(Into::into) + } + + /// Insert a new key with the given alias. If the alias is already used, + /// will prompt for overwrite confirmation. + pub fn insert_keypair( + &mut self, + alias: String, + keypair: StoredKeypair, + pkh: PublicKeyHash, + ) -> Option { + self.store + .insert_keypair::(alias.into(), keypair, pkh) + .map(Into::into) + } + + /// Insert a viewing key into the wallet under the given alias + pub fn insert_viewing_key( + &mut self, + alias: String, + view_key: ExtendedViewingKey, + ) -> Option { + self.store + .insert_viewing_key::(alias.into(), view_key) + .map(Into::into) + } + + /// Insert a spending key into the wallet under the given alias + pub fn insert_spending_key( + &mut self, + alias: String, + spend_key: StoredKeypair, + viewkey: ExtendedViewingKey, + ) -> Option { + self.store + .insert_spending_key::(alias.into(), spend_key, viewkey) + .map(Into::into) + } + + /// Encrypt the given spending key and insert it into the wallet under the + /// given alias + pub fn encrypt_insert_spending_key( + &mut self, + alias: String, + spend_key: ExtendedSpendingKey, + password: Option, + ) -> Option { + self.store + .insert_spending_key::( + alias.into(), + StoredKeypair::new(spend_key, password).0, + ExtendedFullViewingKey::from(&spend_key.into()).into(), + ) + .map(Into::into) + } + + /// Insert a payment address into the wallet under the given alias + pub fn insert_payment_addr( + &mut self, + alias: String, + payment_addr: PaymentAddress, + ) -> Option { + self.store + .insert_payment_addr::(alias.into(), payment_addr) + .map(Into::into) + } + + /// Extend this wallet from pre-genesis validator wallet. + pub fn extend_from_pre_genesis_validator( + &mut self, + validator_address: Address, + validator_alias: Alias, + other: pre_genesis::ValidatorWallet, + ) { + self.store.extend_from_pre_genesis_validator( + validator_address, + validator_alias, + other, + ) + } + + /// Provide immutable access to the backing store + pub fn store(&self) -> &Store { + &self.store + } + + /// Provide mutable access to the backing store + pub fn store_mut(&mut self) -> &mut Store { + &mut self.store + } + + /// Access storage location data + pub fn store_dir(&self) -> &U::Storage { + &self.store_dir + } +} diff --git a/shared/src/ledger/wallet/pre_genesis.rs b/shared/src/ledger/wallet/pre_genesis.rs new file mode 100644 index 0000000000..333a9f21e7 --- /dev/null +++ b/shared/src/ledger/wallet/pre_genesis.rs @@ -0,0 +1,80 @@ +//! Provides functionality for managing validator keys +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::ledger::wallet; +use crate::ledger::wallet::{store, StoredKeypair}; +use crate::types::key::{common, SchemeType}; + +/// Ways in which wallet store operations can fail +#[derive(Error, Debug)] +pub enum ReadError { + /// Failed decoding the wallet store + #[error("Failed decoding the wallet store: {0}")] + Decode(toml::de::Error), + /// Failed to read the wallet store + #[error("Failed to read the wallet store from {0}: {1}")] + ReadWallet(String, String), + /// Failed to write the wallet store + #[error("Failed to write the wallet store: {0}")] + StoreNewWallet(String), + /// Failed to decode a key + #[error("Failed to decode a key: {0}")] + Decryption(wallet::keys::DecryptionError), +} + +/// Validator pre-genesis wallet includes all the required keys for genesis +/// setup and a cache of decrypted keys. +pub struct ValidatorWallet { + /// The wallet store that can be written/read to/from TOML + pub store: ValidatorStore, + /// Cryptographic keypair for validator account key + pub account_key: common::SecretKey, + /// Cryptographic keypair for consensus key + pub consensus_key: common::SecretKey, + /// Cryptographic keypair for Tendermint node key + pub tendermint_node_key: common::SecretKey, +} + +/// Validator pre-genesis wallet store includes all the required keys for +/// genesis setup. +#[derive(Serialize, Deserialize, Debug)] +pub struct ValidatorStore { + /// Cryptographic keypair for validator account key + pub account_key: wallet::StoredKeypair, + /// Cryptographic keypair for consensus key + pub consensus_key: wallet::StoredKeypair, + /// Cryptographic keypair for Tendermint node key + pub tendermint_node_key: wallet::StoredKeypair, + /// Special validator keys + pub validator_keys: wallet::ValidatorKeys, +} + +impl ValidatorStore { + /// Decode from TOML string bytes + pub fn decode(data: Vec) -> Result { + toml::from_slice(&data) + } + + /// Encode in TOML string bytes + pub fn encode(&self) -> Vec { + toml::to_vec(self).expect( + "Serializing of validator pre-genesis wallet shouldn't fail", + ) + } +} + +/// Generate a key and then encrypt it +pub fn gen_key_to_store( + scheme: SchemeType, + password: &Option, +) -> (StoredKeypair, common::SecretKey) { + let sk = store::gen_sk(scheme); + StoredKeypair::new(sk, password.clone()) +} + +impl From for ReadError { + fn from(err: wallet::keys::DecryptionError) -> Self { + ReadError::Decryption(err) + } +} diff --git a/shared/src/ledger/wallet/store.rs b/shared/src/ledger/wallet/store.rs new file mode 100644 index 0000000000..84870427de --- /dev/null +++ b/shared/src/ledger/wallet/store.rs @@ -0,0 +1,568 @@ +use std::collections::HashMap; +use std::str::FromStr; + +use bimap::BiHashMap; +use masp_primitives::zip32::ExtendedFullViewingKey; +#[cfg(feature = "masp-tx-gen")] +use rand_core::RngCore; +use serde::{Deserialize, Serialize}; + +use super::alias::{self, Alias}; +use super::pre_genesis; +use crate::ledger::wallet::{StoredKeypair, WalletUtils}; +use crate::types::address::{Address, ImplicitAddress}; +use crate::types::key::dkg_session_keys::DkgKeypair; +use crate::types::key::*; +use crate::types::masp::{ + ExtendedSpendingKey, ExtendedViewingKey, PaymentAddress, +}; + +/// Actions that can be taken when there is an alias conflict +pub enum ConfirmationResponse { + /// Replace the existing alias + Replace, + /// Reselect the alias that is ascribed to a given entity + Reselect(Alias), + /// Skip assigning the given entity an alias + Skip, +} + +/// Special keys for a validator +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ValidatorKeys { + /// Special keypair for signing protocol txs + pub protocol_keypair: common::SecretKey, + /// Special session keypair needed by validators for participating + /// in the DKG protocol + pub dkg_keypair: Option, +} + +impl ValidatorKeys { + /// Get the protocol keypair + pub fn get_protocol_keypair(&self) -> &common::SecretKey { + &self.protocol_keypair + } +} + +/// Special data associated with a validator +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ValidatorData { + /// The address associated to a validator + pub address: Address, + /// special keys for a validator + pub keys: ValidatorKeys, +} + +/// A Storage area for keys and addresses +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct Store { + /// Known viewing keys + view_keys: HashMap, + /// Known spending keys + spend_keys: HashMap>, + /// Known payment addresses + payment_addrs: HashMap, + /// Cryptographic keypairs + keys: HashMap>, + /// Namada address book + addresses: BiHashMap, + /// Known mappings of public key hashes to their aliases in the `keys` + /// field. Used for look-up by a public key. + pkhs: HashMap, + /// Special keys if the wallet belongs to a validator + pub(crate) validator_data: Option, +} + +impl Store { + /// Find the stored key by an alias, a public key hash or a public key. + pub fn find_key( + &self, + alias_pkh_or_pk: impl AsRef, + ) -> Option<&StoredKeypair> { + let alias_pkh_or_pk = alias_pkh_or_pk.as_ref(); + // Try to find by alias + self.keys + .get(&alias_pkh_or_pk.into()) + // Try to find by PKH + .or_else(|| { + let pkh = PublicKeyHash::from_str(alias_pkh_or_pk).ok()?; + self.find_key_by_pkh(&pkh) + }) + // Try to find by PK + .or_else(|| { + let pk = common::PublicKey::from_str(alias_pkh_or_pk).ok()?; + self.find_key_by_pk(&pk) + }) + } + + /// Find the spending key with the given alias and return it + pub fn find_spending_key( + &self, + alias: impl AsRef, + ) -> Option<&StoredKeypair> { + self.spend_keys.get(&alias.into()) + } + + /// Find the viewing key with the given alias and return it + pub fn find_viewing_key( + &self, + alias: impl AsRef, + ) -> Option<&ExtendedViewingKey> { + self.view_keys.get(&alias.into()) + } + + /// Find the payment address with the given alias and return it + pub fn find_payment_addr( + &self, + alias: impl AsRef, + ) -> Option<&PaymentAddress> { + self.payment_addrs.get(&alias.into()) + } + + /// Find the stored key by a public key. + pub fn find_key_by_pk( + &self, + pk: &common::PublicKey, + ) -> Option<&StoredKeypair> { + let pkh = PublicKeyHash::from(pk); + self.find_key_by_pkh(&pkh) + } + + /// Find the stored key by a public key hash. + pub fn find_key_by_pkh( + &self, + pkh: &PublicKeyHash, + ) -> Option<&StoredKeypair> { + let alias = self.pkhs.get(pkh)?; + self.keys.get(alias) + } + + /// Find the stored alias for a public key hash. + pub fn find_alias_by_pkh(&self, pkh: &PublicKeyHash) -> Option { + self.pkhs.get(pkh).cloned() + } + + /// Find the stored address by an alias. + pub fn find_address(&self, alias: impl AsRef) -> Option<&Address> { + self.addresses.get_by_left(&alias.into()) + } + + /// Find an alias by the address if it's in the wallet. + pub fn find_alias(&self, address: &Address) -> Option<&Alias> { + self.addresses.get_by_right(address) + } + + /// Get all known keys by their alias, paired with PKH, if known. + pub fn get_keys( + &self, + ) -> HashMap< + Alias, + (&StoredKeypair, Option<&PublicKeyHash>), + > { + let mut keys: HashMap< + Alias, + (&StoredKeypair, Option<&PublicKeyHash>), + > = self + .pkhs + .iter() + .filter_map(|(pkh, alias)| { + let key = &self.keys.get(alias)?; + Some((alias.clone(), (*key, Some(pkh)))) + }) + .collect(); + self.keys.iter().for_each(|(alias, key)| { + if !keys.contains_key(alias) { + keys.insert(alias.clone(), (key, None)); + } + }); + keys + } + + /// Get all known addresses by their alias, paired with PKH, if known. + pub fn get_addresses(&self) -> &BiHashMap { + &self.addresses + } + + /// Get all known payment addresses by their alias. + pub fn get_payment_addrs(&self) -> &HashMap { + &self.payment_addrs + } + + /// Get all known viewing keys by their alias. + pub fn get_viewing_keys(&self) -> &HashMap { + &self.view_keys + } + + /// Get all known spending keys by their alias. + pub fn get_spending_keys( + &self, + ) -> &HashMap> { + &self.spend_keys + } + + #[cfg(feature = "masp-tx-gen")] + fn generate_spending_key() -> ExtendedSpendingKey { + use rand::rngs::OsRng; + let mut spend_key = [0; 32]; + OsRng.fill_bytes(&mut spend_key); + masp_primitives::zip32::ExtendedSpendingKey::master(spend_key.as_ref()) + .into() + } + + /// Generate a new keypair and insert it into the store with the provided + /// alias. If none provided, the alias will be the public key hash. + /// If no password is provided, the keypair will be stored raw without + /// encryption. Returns the alias of the key and a reference-counting + /// pointer to the key. + pub fn gen_key( + &mut self, + scheme: SchemeType, + alias: Option, + password: Option, + ) -> (Alias, common::SecretKey) { + let sk = gen_sk(scheme); + let pkh: PublicKeyHash = PublicKeyHash::from(&sk.ref_to()); + let (keypair_to_store, raw_keypair) = StoredKeypair::new(sk, password); + let address = Address::Implicit(ImplicitAddress(pkh.clone())); + let alias: Alias = alias.unwrap_or_else(|| pkh.clone().into()).into(); + if self + .insert_keypair::(alias.clone(), keypair_to_store, pkh) + .is_none() + { + panic!("Action cancelled, no changes persisted."); + } + if self.insert_address::(alias.clone(), address).is_none() { + panic!("Action cancelled, no changes persisted."); + } + (alias, raw_keypair) + } + + /// Generate a spending key similarly to how it's done for keypairs + pub fn gen_spending_key( + &mut self, + alias: String, + password: Option, + ) -> (Alias, ExtendedSpendingKey) { + let spendkey = Self::generate_spending_key(); + let viewkey = ExtendedFullViewingKey::from(&spendkey.into()).into(); + let (spendkey_to_store, _raw_spendkey) = + StoredKeypair::new(spendkey, password); + let alias = Alias::from(alias); + if self + .insert_spending_key::(alias.clone(), spendkey_to_store, viewkey) + .is_none() + { + panic!("Action cancelled, no changes persisted."); + } + (alias, spendkey) + } + + /// Add validator data to the store + pub fn add_validator_data( + &mut self, + address: Address, + keys: ValidatorKeys, + ) { + self.validator_data = Some(ValidatorData { address, keys }); + } + + /// Returns the validator data, if it exists + pub fn get_validator_data(&self) -> Option<&ValidatorData> { + self.validator_data.as_ref() + } + + /// Returns the validator data, if it exists + pub fn validator_data(&mut self) -> Option<&mut ValidatorData> { + self.validator_data.as_mut() + } + + /// Insert a new key with the given alias. If the alias is already used, + /// will prompt for overwrite/reselection confirmation. If declined, then + /// keypair is not inserted and nothing is returned, otherwise selected + /// alias is returned. + pub fn insert_keypair( + &mut self, + alias: Alias, + keypair: StoredKeypair, + pkh: PublicKeyHash, + ) -> Option { + if alias.is_empty() { + println!( + "Empty alias given, defaulting to {}.", + Into::::into(pkh.to_string()) + ); + } + // Addresses and keypairs can share aliases, so first remove any + // addresses sharing the same namesake before checking if alias has been + // used. + let counterpart_address = self.addresses.remove_by_left(&alias); + if self.contains_alias(&alias) { + match U::show_overwrite_confirmation(&alias, "a key") { + ConfirmationResponse::Replace => {} + ConfirmationResponse::Reselect(new_alias) => { + // Restore the removed address in case the recursive prompt + // terminates with a cancellation + counterpart_address + .map(|x| self.addresses.insert(alias.clone(), x.1)); + return self.insert_keypair::(new_alias, keypair, pkh); + } + ConfirmationResponse::Skip => { + // Restore the removed address since this insertion action + // has now been cancelled + counterpart_address + .map(|x| self.addresses.insert(alias.clone(), x.1)); + return None; + } + } + } + self.remove_alias(&alias); + self.keys.insert(alias.clone(), keypair); + self.pkhs.insert(pkh, alias.clone()); + // Since it is intended for the inserted keypair to share its namesake + // with the pre-existing address + counterpart_address.map(|x| self.addresses.insert(alias.clone(), x.1)); + Some(alias) + } + + /// Insert spending keys similarly to how it's done for keypairs + pub fn insert_spending_key( + &mut self, + alias: Alias, + spendkey: StoredKeypair, + viewkey: ExtendedViewingKey, + ) -> Option { + if alias.is_empty() { + eprintln!("Empty alias given."); + return None; + } + if self.contains_alias(&alias) { + match U::show_overwrite_confirmation(&alias, "a spending key") { + ConfirmationResponse::Replace => {} + ConfirmationResponse::Reselect(new_alias) => { + return self.insert_spending_key::( + new_alias, spendkey, viewkey, + ); + } + ConfirmationResponse::Skip => return None, + } + } + self.remove_alias(&alias); + self.spend_keys.insert(alias.clone(), spendkey); + // Simultaneously add the derived viewing key to ease balance viewing + self.view_keys.insert(alias.clone(), viewkey); + Some(alias) + } + + /// Insert viewing keys similarly to how it's done for keypairs + pub fn insert_viewing_key( + &mut self, + alias: Alias, + viewkey: ExtendedViewingKey, + ) -> Option { + if alias.is_empty() { + eprintln!("Empty alias given."); + return None; + } + if self.contains_alias(&alias) { + match U::show_overwrite_confirmation(&alias, "a viewing key") { + ConfirmationResponse::Replace => {} + ConfirmationResponse::Reselect(new_alias) => { + return self.insert_viewing_key::(new_alias, viewkey); + } + ConfirmationResponse::Skip => return None, + } + } + self.remove_alias(&alias); + self.view_keys.insert(alias.clone(), viewkey); + Some(alias) + } + + /// Check if any map of the wallet contains the given alias + fn contains_alias(&self, alias: &Alias) -> bool { + self.payment_addrs.contains_key(alias) + || self.view_keys.contains_key(alias) + || self.spend_keys.contains_key(alias) + || self.keys.contains_key(alias) + || self.addresses.contains_left(alias) + } + + /// Completely remove the given alias from all maps in the wallet + fn remove_alias(&mut self, alias: &Alias) { + self.payment_addrs.remove(alias); + self.view_keys.remove(alias); + self.spend_keys.remove(alias); + self.keys.remove(alias); + self.addresses.remove_by_left(alias); + self.pkhs.retain(|_key, val| val != alias); + } + + /// Insert payment addresses similarly to how it's done for keypairs + pub fn insert_payment_addr( + &mut self, + alias: Alias, + payment_addr: PaymentAddress, + ) -> Option { + if alias.is_empty() { + eprintln!("Empty alias given."); + return None; + } + if self.contains_alias(&alias) { + match U::show_overwrite_confirmation(&alias, "a payment address") { + ConfirmationResponse::Replace => {} + ConfirmationResponse::Reselect(new_alias) => { + return self + .insert_payment_addr::(new_alias, payment_addr); + } + ConfirmationResponse::Skip => return None, + } + } + self.remove_alias(&alias); + self.payment_addrs.insert(alias.clone(), payment_addr); + Some(alias) + } + + /// Helper function to restore keypair given alias-keypair mapping and the + /// pkhs-alias mapping. + fn restore_keypair( + &mut self, + alias: Alias, + key: Option>, + pkh: Option, + ) { + key.map(|x| self.keys.insert(alias.clone(), x)); + pkh.map(|x| self.pkhs.insert(x, alias.clone())); + } + + /// Insert a new address with the given alias. If the alias is already used, + /// will prompt for overwrite/reselection confirmation, which when declined, + /// the address won't be added. Return the selected alias if the address has + /// been added. + pub fn insert_address( + &mut self, + alias: Alias, + address: Address, + ) -> Option { + if alias.is_empty() { + println!("Empty alias given, defaulting to {}.", address.encode()); + } + // Addresses and keypairs can share aliases, so first remove any keys + // sharing the same namesake before checking if alias has been used. + let counterpart_key = self.keys.remove(&alias); + let mut counterpart_pkh = None; + self.pkhs.retain(|k, v| { + if v == &alias { + counterpart_pkh = Some(k.clone()); + false + } else { + true + } + }); + if self.addresses.contains_left(&alias) { + match U::show_overwrite_confirmation(&alias, "an address") { + ConfirmationResponse::Replace => {} + ConfirmationResponse::Reselect(new_alias) => { + // Restore the removed keypair in case the recursive prompt + // terminates with a cancellation + self.restore_keypair( + alias, + counterpart_key, + counterpart_pkh, + ); + return self.insert_address::(new_alias, address); + } + ConfirmationResponse::Skip => { + // Restore the removed keypair since this insertion action + // has now been cancelled + self.restore_keypair( + alias, + counterpart_key, + counterpart_pkh, + ); + return None; + } + } + } + self.remove_alias(&alias); + self.addresses.insert(alias.clone(), address); + // Since it is intended for the inserted address to share its namesake + // with the pre-existing keypair + self.restore_keypair(alias.clone(), counterpart_key, counterpart_pkh); + Some(alias) + } + + /// Extend this store from pre-genesis validator wallet. + pub fn extend_from_pre_genesis_validator( + &mut self, + validator_address: Address, + validator_alias: Alias, + other: pre_genesis::ValidatorWallet, + ) { + let account_key_alias = alias::validator_key(&validator_alias); + let consensus_key_alias = + alias::validator_consensus_key(&validator_alias); + let tendermint_node_key_alias = + alias::validator_tendermint_node_key(&validator_alias); + + let keys = [ + (account_key_alias.clone(), other.store.account_key), + (consensus_key_alias.clone(), other.store.consensus_key), + ( + tendermint_node_key_alias.clone(), + other.store.tendermint_node_key, + ), + ]; + self.keys.extend(keys.into_iter()); + + let account_pk = other.account_key.ref_to(); + let consensus_pk = other.consensus_key.ref_to(); + let tendermint_node_pk = other.tendermint_node_key.ref_to(); + let addresses = [ + (account_key_alias.clone(), (&account_pk).into()), + (consensus_key_alias.clone(), (&consensus_pk).into()), + ( + tendermint_node_key_alias.clone(), + (&tendermint_node_pk).into(), + ), + ]; + self.addresses.extend(addresses.into_iter()); + + let pkhs = [ + ((&account_pk).into(), account_key_alias), + ((&consensus_pk).into(), consensus_key_alias), + ((&tendermint_node_pk).into(), tendermint_node_key_alias), + ]; + self.pkhs.extend(pkhs.into_iter()); + + self.validator_data = Some(ValidatorData { + address: validator_address, + keys: other.store.validator_keys, + }); + } + + /// Decode a Store from the given bytes + pub fn decode(data: Vec) -> Result { + toml::from_slice(&data) + } + + /// Encode a store into a string of bytes + pub fn encode(&self) -> Vec { + toml::to_vec(self).expect("Serializing of store shouldn't fail") + } +} + +/// Generate a new secret key. +pub fn gen_sk(scheme: SchemeType) -> common::SecretKey { + use rand::rngs::OsRng; + let mut csprng = OsRng {}; + match scheme { + SchemeType::Ed25519 => ed25519::SigScheme::generate(&mut csprng) + .try_to_sk() + .unwrap(), + SchemeType::Secp256k1 => secp256k1::SigScheme::generate(&mut csprng) + .try_to_sk() + .unwrap(), + SchemeType::Common => common::SigScheme::generate(&mut csprng) + .try_to_sk() + .unwrap(), + } +} diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 4403453dc9..8e498713c6 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -18,7 +18,7 @@ abciplus = [ wasm-runtime = ["namada/wasm-runtime"] [dependencies] -namada = {path = "../shared", default-features = false, features = ["testing"]} +namada = {path = "../shared", default-features = false, features = ["testing", "namada-sdk"]} namada_vp_prelude = {path = "../vp_prelude", default-features = false} namada_tx_prelude = {path = "../tx_prelude", default-features = false} chrono = {version = "0.4.22", default-features = false, features = ["clock", "std"]} diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index dc129818c3..9e35bc3760 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -21,7 +21,7 @@ use color_eyre::eyre::Result; use data_encoding::HEXLOWER; use namada::types::address::{btc, eth, masp_rewards, Address}; use namada::types::token; -use namada_apps::client::tx::ShieldedContext; +use namada_apps::client::tx::CLIShieldedUtils; use namada_apps::config::genesis::genesis_config::{ GenesisConfig, ParametersConfig, PosParamsConfig, }; @@ -477,7 +477,7 @@ fn ledger_txs_and_queries() -> Result<()> { #[test] fn masp_txs_and_queries() -> Result<()> { // Download the shielded pool parameters before starting node - let _ = ShieldedContext::new(PathBuf::new()); + let _ = CLIShieldedUtils::new(PathBuf::new()); // Lengthen epoch to ensure that a transaction can be constructed and // submitted within the same block. Necessary to ensure that conversion is // not invalidated. @@ -746,7 +746,7 @@ fn masp_txs_and_queries() -> Result<()> { #[test] fn masp_pinned_txs() -> Result<()> { // Download the shielded pool parameters before starting node - let _ = ShieldedContext::new(PathBuf::new()); + let _ = CLIShieldedUtils::new(PathBuf::new()); // Lengthen epoch to ensure that a transaction can be constructed and // submitted within the same block. Necessary to ensure that conversion is // not invalidated. @@ -906,7 +906,7 @@ fn masp_pinned_txs() -> Result<()> { #[test] fn masp_incentives() -> Result<()> { // Download the shielded pool parameters before starting node - let _ = ShieldedContext::new(PathBuf::new()); + let _ = CLIShieldedUtils::new(PathBuf::new()); // Lengthen epoch to ensure that a transaction can be constructed and // submitted within the same block. Necessary to ensure that conversion is // not invalidated. diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index bd82ea3be8..19a370f6d4 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -98,6 +98,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ark-ed-on-bls12-381" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b7ada17db3854f5994e74e60b18e10e818594935ee7e1d329800c117b32970" +dependencies = [ + "ark-bls12-381", + "ark-ec", + "ark-ff", + "ark-std", +] + [[package]] name = "ark-ff" version = "0.3.0" @@ -138,6 +150,19 @@ dependencies = [ "syn", ] +[[package]] +name = "ark-poly" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b0f78f47537c2f15706db7e98fe64cc1711dbf9def81218194e17239e53e5aa" +dependencies = [ + "ark-ff", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.11.2", +] + [[package]] name = "ark-serialize" version = "0.3.0" @@ -188,6 +213,102 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +[[package]] +name = "async-channel" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794" +dependencies = [ + "async-lock", + "autocfg", + "concurrent-queue", + "futures-lite", + "libc", + "log", + "parking", + "polling", + "slab", + "socket2", + "waker-fn", + "windows-sys 0.42.0", +] + +[[package]] +name = "async-lock" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" +dependencies = [ + "event-listener", + "futures-lite", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-channel", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils 0.8.12", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + [[package]] name = "async-stream" version = "0.3.3" @@ -209,6 +330,12 @@ dependencies = [ "syn", ] +[[package]] +name = "async-task" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" + [[package]] name = "async-trait" version = "0.1.58" @@ -236,6 +363,12 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "atomic-waker" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debc29dde2e69f9e47506b525f639ed42300fc014a3e007832592448fa8e4599" + [[package]] name = "autocfg" version = "1.1.0" @@ -312,6 +445,24 @@ dependencies = [ "crunchy 0.1.6", ] +[[package]] +name = "bimap" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0455254eb5c6964c4545d8bac815e1a1be4f3afe0ae695ea539c12d728d44b" +dependencies = [ + "serde", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bip0039" version = "0.9.0" @@ -349,7 +500,7 @@ checksum = "42b2a9a8e3c7544f5ce2b475f2f56580a3102b37e0ee001558ad4faedcf56cf4" dependencies = [ "bech32", "bitcoin_hashes", - "secp256k1", + "secp256k1 0.22.1", "serde", ] @@ -380,6 +531,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.5", +] + [[package]] name = "blake2b_simd" version = "0.5.11" @@ -473,6 +633,20 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" +[[package]] +name = "blocking" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c67b173a56acffd6d2326fb7ab938ba0b00a71480e14902b2591c87bc5741e8" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "atomic-waker", + "fastrand", + "futures-lite", +] + [[package]] name = "bls12_381" version = "0.6.1" @@ -701,6 +875,15 @@ dependencies = [ "syn", ] +[[package]] +name = "concurrent-queue" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e" +dependencies = [ + "crossbeam-utils 0.8.12", +] + [[package]] name = "const-oid" version = "0.7.1" @@ -966,6 +1149,12 @@ dependencies = [ "crypto_api", ] +[[package]] +name = "ct-codecs" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b7eb4404b8195a9abb6356f4ac07d8ba267045c8d6d220ac4dc992e6cc75df" + [[package]] name = "ct-logs" version = "0.8.0" @@ -975,6 +1164,16 @@ dependencies = [ "sct", ] +[[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "curve25519-dalek" version = "3.2.0" @@ -1221,6 +1420,7 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" dependencies = [ + "serde", "signature", ] @@ -1247,6 +1447,10 @@ checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ "curve25519-dalek", "ed25519", + "merlin", + "rand 0.7.3", + "serde", + "serde_bytes", "sha2 0.9.9", "zeroize", ] @@ -1334,6 +1538,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "eyre" version = "0.6.8" @@ -1359,6 +1569,43 @@ dependencies = [ "instant", ] +[[package]] +name = "ferveo" +version = "0.1.1" +source = "git+https://github.com/anoma/ferveo#1022ab2c7ccc689abcc05e5a08df6fb0c2a3fc65" +dependencies = [ + "anyhow", + "ark-bls12-381", + "ark-ec", + "ark-ed-on-bls12-381", + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "bincode", + "blake2", + "blake2b_simd 1.0.0", + "borsh", + "digest 0.10.5", + "ed25519-dalek", + "either", + "ferveo-common", + "group-threshold-cryptography", + "hex", + "itertools", + "measure_time", + "miracl_core", + "num", + "rand 0.7.3", + "rand 0.8.5", + "serde", + "serde_bytes", + "serde_json", + "subproductdomain", + "subtle", + "zeroize", +] + [[package]] name = "ferveo-common" version = "0.1.0" @@ -1482,6 +1729,21 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + [[package]] name = "futures-macro" version = "0.3.25" @@ -1580,6 +1842,18 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "group" version = "0.11.0" @@ -1592,6 +1866,30 @@ dependencies = [ "subtle", ] +[[package]] +name = "group-threshold-cryptography" +version = "0.1.0" +source = "git+https://github.com/anoma/ferveo#1022ab2c7ccc689abcc05e5a08df6fb0c2a3fc65" +dependencies = [ + "anyhow", + "ark-bls12-381", + "ark-ec", + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "blake2b_simd 1.0.0", + "chacha20", + "hex", + "itertools", + "miracl_core", + "rand 0.8.5", + "rand_core 0.6.4", + "rayon", + "subproductdomain", + "thiserror", +] + [[package]] name = "gumdrop" version = "0.8.1" @@ -2083,6 +2381,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] @@ -2142,6 +2443,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9b7d56ba4a8344d6be9729995e6b06f928af29998cdf79fe390cbf6b1fee838" +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -2246,6 +2556,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if 1.0.0", + "value-bag", ] [[package]] @@ -2302,6 +2613,8 @@ dependencies = [ "lazy_static", "rand 0.8.5", "rand_core 0.6.4", + "ripemd160", + "secp256k1 0.20.3", "serde", "sha2 0.9.9", "subtle", @@ -2345,6 +2658,16 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" +[[package]] +name = "measure_time" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56220900f1a0923789ecd6bf25fbae8af3b2f1ff3e9e297fc9b6b8674dd4d852" +dependencies = [ + "instant", + "log", +] + [[package]] name = "memchr" version = "2.5.0" @@ -2393,6 +2716,18 @@ dependencies = [ "nonempty", ] +[[package]] +name = "merlin" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e261cf0f8b3c42ded9f7d2bb59dea03aa52bc8a1cbc7482f9fc3fd1229d3b42" +dependencies = [ + "byteorder", + "keccak", + "rand_core 0.5.1", + "zeroize", +] + [[package]] name = "mime" version = "0.3.16" @@ -2420,6 +2755,12 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "miracl_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94c7128ba23c81f6471141b90f17654f89ef44a56e14b8a4dd0fddfccd655277" + [[package]] name = "moka" version = "0.8.6" @@ -2458,8 +2799,10 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" name = "namada" version = "0.12.0" dependencies = [ + "async-std", "async-trait", "bellman", + "bimap", "bls12_381", "borsh", "circular-queue", @@ -2474,19 +2817,26 @@ dependencies = [ "masp_proofs", "namada_core", "namada_proof_of_stake", + "orion", "parity-wasm", "paste", "proptest", "prost", "pwasm-utils", + "rand 0.8.5", + "rand_core 0.6.4", "rayon", "rust_decimal", + "serde", "serde_json", "sha2 0.9.9", "tempfile", "tendermint", "tendermint-proto", + "tendermint-rpc", "thiserror", + "tokio", + "toml", "tracing", "wasmer", "wasmer-cache", @@ -2503,6 +2853,7 @@ name = "namada_core" version = "0.12.0" dependencies = [ "ark-bls12-381", + "ark-ec", "ark-serialize", "bech32", "bellman", @@ -2511,7 +2862,9 @@ dependencies = [ "data-encoding", "derivative", "ed25519-consensus", + "ferveo", "ferveo-common", + "group-threshold-cryptography", "ibc", "ibc-proto", "ics23", @@ -2664,6 +3017,20 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" +[[package]] +name = "num" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.3" @@ -2676,6 +3043,15 @@ dependencies = [ "serde", ] +[[package]] +name = "num-complex" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19" +dependencies = [ + "num-traits", +] + [[package]] name = "num-derive" version = "0.3.3" @@ -2697,6 +3073,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.1" @@ -2795,6 +3182,18 @@ dependencies = [ "zcash_note_encryption 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "orion" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6624905ddd92e460ff0685567539ed1ac985b2dee4c92c7edcd64fce905b00c" +dependencies = [ + "ct-codecs", + "getrandom 0.2.8", + "subtle", + "zeroize", +] + [[package]] name = "pairing" version = "0.21.0" @@ -2810,6 +3209,12 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1ad0aff30c1da14b1254fcb2af73e1fa9a28670e584a626f53a369d0e157304" +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + [[package]] name = "parking_lot" version = "0.12.1" @@ -2919,11 +3324,10 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pest" -version = "2.4.1" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a528564cc62c19a7acac4d81e01f39e53e25e17b934878f4c6d25cc2836e62f8" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" dependencies = [ - "thiserror", "ucd-trie", ] @@ -2980,6 +3384,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "polling" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22122d5ec4f9fe1b3916419b76be1e80bcb93f618d071d2edf841b137b2a2bd6" +dependencies = [ + "autocfg", + "cfg-if 1.0.0", + "libc", + "log", + "wepoll-ffi", + "windows-sys 0.42.0", +] + [[package]] name = "poly1305" version = "0.7.2" @@ -3689,16 +4107,34 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secp256k1" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d03ceae636d0fed5bae6a7f4f664354c5f4fcedf6eef053fef17e49f837d0a" +dependencies = [ + "secp256k1-sys 0.4.2", +] + [[package]] name = "secp256k1" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26947345339603ae8395f68e2f3d85a6b0a8ddfe6315818e80b8504415099db0" dependencies = [ - "secp256k1-sys", + "secp256k1-sys 0.5.2", "serde", ] +[[package]] +name = "secp256k1-sys" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957da2573cde917463ece3570eab4a0b3f19de6f1646cde62e6fd3868f566036" +dependencies = [ + "cc", +] + [[package]] name = "secp256k1-sys" version = "0.5.2" @@ -3998,6 +4434,19 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "subproductdomain" +version = "0.1.0" +source = "git+https://github.com/anoma/ferveo#1022ab2c7ccc689abcc05e5a08df6fb0c2a3fc65" +dependencies = [ + "anyhow", + "ark-ec", + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", +] + [[package]] name = "subtle" version = "2.4.1" @@ -4077,7 +4526,7 @@ dependencies = [ [[package]] name = "tendermint" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=679227ab920dbb45cdd5acb97f49c4fe2ebb5a44#679227ab920dbb45cdd5acb97f49c4fe2ebb5a44" dependencies = [ "async-trait", "bytes", @@ -4107,7 +4556,7 @@ dependencies = [ [[package]] name = "tendermint-config" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=679227ab920dbb45cdd5acb97f49c4fe2ebb5a44#679227ab920dbb45cdd5acb97f49c4fe2ebb5a44" dependencies = [ "flex-error", "serde", @@ -4120,7 +4569,7 @@ dependencies = [ [[package]] name = "tendermint-light-client" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=679227ab920dbb45cdd5acb97f49c4fe2ebb5a44#679227ab920dbb45cdd5acb97f49c4fe2ebb5a44" dependencies = [ "contracts", "crossbeam-channel 0.4.4", @@ -4141,7 +4590,7 @@ dependencies = [ [[package]] name = "tendermint-light-client-verifier" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=679227ab920dbb45cdd5acb97f49c4fe2ebb5a44#679227ab920dbb45cdd5acb97f49c4fe2ebb5a44" dependencies = [ "derive_more", "flex-error", @@ -4153,7 +4602,7 @@ dependencies = [ [[package]] name = "tendermint-proto" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=679227ab920dbb45cdd5acb97f49c4fe2ebb5a44#679227ab920dbb45cdd5acb97f49c4fe2ebb5a44" dependencies = [ "bytes", "flex-error", @@ -4170,7 +4619,7 @@ dependencies = [ [[package]] name = "tendermint-rpc" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=679227ab920dbb45cdd5acb97f49c4fe2ebb5a44#679227ab920dbb45cdd5acb97f49c4fe2ebb5a44" dependencies = [ "async-trait", "async-tungstenite", @@ -4203,7 +4652,7 @@ dependencies = [ [[package]] name = "tendermint-testgen" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=679227ab920dbb45cdd5acb97f49c4fe2ebb5a44#679227ab920dbb45cdd5acb97f49c4fe2ebb5a44" dependencies = [ "ed25519-dalek", "gumdrop", @@ -4237,18 +4686,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.37" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.37" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", @@ -4730,6 +5179,16 @@ dependencies = [ "getrandom 0.2.8", ] +[[package]] +name = "value-bag" +version = "1.0.0-alpha.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55" +dependencies = [ + "ctor", + "version_check", +] + [[package]] name = "version_check" version = "0.9.4" @@ -4756,6 +5215,12 @@ dependencies = [ "libc", ] +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + [[package]] name = "walkdir" version = "2.3.2" @@ -4820,6 +5285,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.83" @@ -5156,6 +5633,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + [[package]] name = "which" version = "4.3.0" diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index 062d9fb3dc..6ab20d261a 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -14,13 +14,13 @@ borsh-derive = {git = "https://github.com/heliaxdev/borsh-rs.git", rev = "cd5223 borsh-derive-internal = {git = "https://github.com/heliaxdev/borsh-rs.git", rev = "cd5223e5103c4f139e0c54cf8259b7ec5ec4073a"} borsh-schema-derive-internal = {git = "https://github.com/heliaxdev/borsh-rs.git", rev = "cd5223e5103c4f139e0c54cf8259b7ec5ec4073a"} # patched to a commit on the `eth-bridge-integration+consensus-timeout` branch of our fork -tendermint = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} -tendermint-config = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} -tendermint-proto = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} -tendermint-rpc = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} -tendermint-testgen = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} -tendermint-light-client = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} -tendermint-light-client-verifier = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} +tendermint = {git="https://github.com/heliaxdev/tendermint-rs.git", rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} +tendermint-config = {git="https://github.com/heliaxdev/tendermint-rs.git", rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} +tendermint-proto = {git="https://github.com/heliaxdev/tendermint-rs.git", rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} +tendermint-rpc = {git="https://github.com/heliaxdev/tendermint-rs.git", rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} +tendermint-testgen = {git="https://github.com/heliaxdev/tendermint-rs.git", rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} +tendermint-light-client = {git="https://github.com/heliaxdev/tendermint-rs.git", rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} +tendermint-light-client-verifier = {git="https://github.com/heliaxdev/tendermint-rs.git", rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} # patched to a commit on the `eth-bridge-integration` branch of our fork ibc = {git = "https://github.com/heliaxdev/ibc-rs.git", rev = "f4703dfe2c1f25cc431279ab74f10f3e0f6827e2"} diff --git a/wasm/checksums.json b/wasm/checksums.json index b13508300c..a99df0c8bc 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,20 +1,20 @@ { - "tx_bond.wasm": "tx_bond.be9c75f96b3b4880b7934d42ee218582b6304f6326a4588d1e6ac1ea4cc61c49.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.cd861e0e82f4934be6d8382d6fff98286b4fadbc20ab826b9e817f6666021273.wasm", - "tx_ibc.wasm": "tx_ibc.13daeb0c88abba264d3052129eda0713bcf1a71f6f69bf37ec2494d0d9119f1f.wasm", - "tx_init_account.wasm": "tx_init_account.e21cfd7e96802f8e841613fb89f1571451401d002a159c5e9586855ac1374df5.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.b9a77bc9e416f33f1e715f25696ae41582e1b379422f7a643549884e0c73e9de.wasm", - "tx_init_validator.wasm": "tx_init_validator.1e9732873861c625f239e74245f8c504a57359c06614ba40387a71811ca4a097.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.47bc922a8be5571620a647ae442a1af7d03d05d29bef95f0b32cdfe00b11fee9.wasm", - "tx_transfer.wasm": "tx_transfer.bbd1ef5d9461c78f0288986de046baad77e10671addc5edaf3c68ea1ae4ecc99.wasm", - "tx_unbond.wasm": "tx_unbond.c0a690d0ad43a94294a6405bae3327f638a657446c74dc61dbb3a4d2ce488b5e.wasm", - "tx_update_vp.wasm": "tx_update_vp.ee2e9b882c4accadf4626e87d801c9ac8ea8c61ccea677e0532fc6c1ee7db6a2.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.263fd9f4cb40f283756f394d86bdea3417e9ecd0568d6582c07a5b6bd14287d6.wasm", - "tx_withdraw.wasm": "tx_withdraw.6ce8faf6a32340178ddeaeb91a9b40e7f0433334e5c1f357964bf8e11d0077f1.wasm", - "vp_implicit.wasm": "vp_implicit.17f5c2af947ccfadce22d0fffecde1a1b4bc4ca3acd5dd8b459c3dce4afcb4e8.wasm", - "vp_masp.wasm": "vp_masp.5620cb6e555161641337d308851c760fbab4f9d3693cfd378703aa55e285249d.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.362584b063cc4aaf8b72af0ed8af8d05a179ebefec596b6ab65e0ca255ec3c80.wasm", - "vp_token.wasm": "vp_token.a289723dd182fe0206e6c4cf1f426a6100787b20e2653d2fad6031e8106157f3.wasm", - "vp_user.wasm": "vp_user.b83b2d0616bb2244c8a92021665a0be749282a53fe1c493e98c330a6ed983833.wasm", - "vp_validator.wasm": "vp_validator.59e3e7729e14eeacc17d76b736d1760d59a1a6e9d6acbc9a870e1835438f524a.wasm" + "tx_bond.wasm": "tx_bond.fd0584036bed91278878e939ca75d1d1eea1356b08652d9802a33ae7d40e63a1.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.7d8ebecc8001aa06b19bfb58852044ce5c928ba63ef0681532d329092b2c07d2.wasm", + "tx_ibc.wasm": "tx_ibc.621225d32e24f0a6a62d496d360398e0211b45d0b044e62ad776b667dae2e5b0.wasm", + "tx_init_account.wasm": "tx_init_account.05a2fd217ca9a8fca2a3b145b78fa4677cd6f236c7ef4090fe96803555923c68.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.5f8d4e0956dd908eb9a5a93fa8705ce57a3f0375acb57993e1ed88efdd3be75a.wasm", + "tx_init_validator.wasm": "tx_init_validator.79e1d78674a1ec7fdaab5adee0965222401625892eb0e75b1ce4b1d83f521d8b.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.0db1e35f022e3efbcc974409fcbe0ded8062a38fe015002b87c6da396688c28e.wasm", + "tx_transfer.wasm": "tx_transfer.047e3ab42ddeb3e045db4ca0e3c3896edaf392cf98563ac17e40ecd3d155c612.wasm", + "tx_unbond.wasm": "tx_unbond.e01b0032a40beaf212611a03240be463c855841a5d446425090b2438c71b2dc9.wasm", + "tx_update_vp.wasm": "tx_update_vp.e19d594ea1eced596b96b21ce4a8513be4f8859d807a80b3fbe57d4f67fe93fc.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.fcd7372363098e5905f133e48b713b186234640f575cf25100154c9ce1218507.wasm", + "tx_withdraw.wasm": "tx_withdraw.0fbc63d71a67e5c6d6c3bc1e25e99f1ebcdf4b9335fadf73c555984e05190e39.wasm", + "vp_implicit.wasm": "vp_implicit.a5798a2500270bfd286552f43e52abc3ba3a2844966595b2aa46d20c6b4a32db.wasm", + "vp_masp.wasm": "vp_masp.a055ca419be1fb826c912b1f37dc01a70e75d6b7672924bcfc8fe4eb6730e7db.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.0bbb333a4f6947d52c2c73b6d99abb4d482961e487a0dca72c6278bc1e883485.wasm", + "vp_token.wasm": "vp_token.9cd955a27b2bcc98849185faa35cdd3debe1573cdae08a841a04fddd86ef65c9.wasm", + "vp_user.wasm": "vp_user.27c17d97530284f1690273ef8affe1925153dedbf05d8556bfe9178be18ad866.wasm", + "vp_validator.wasm": "vp_validator.42db54ef509b4310cb984e04fa76ff1861775dfe54e6cb0187a072322d819df7.wasm" } \ No newline at end of file diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index 1abb89a73c..70f2be93dd 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -98,6 +98,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ark-ed-on-bls12-381" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b7ada17db3854f5994e74e60b18e10e818594935ee7e1d329800c117b32970" +dependencies = [ + "ark-bls12-381", + "ark-ec", + "ark-ff", + "ark-std", +] + [[package]] name = "ark-ff" version = "0.3.0" @@ -138,6 +150,19 @@ dependencies = [ "syn", ] +[[package]] +name = "ark-poly" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b0f78f47537c2f15706db7e98fe64cc1711dbf9def81218194e17239e53e5aa" +dependencies = [ + "ark-ff", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.11.2", +] + [[package]] name = "ark-serialize" version = "0.3.0" @@ -188,6 +213,102 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +[[package]] +name = "async-channel" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794" +dependencies = [ + "async-lock", + "autocfg", + "concurrent-queue", + "futures-lite", + "libc", + "log", + "parking", + "polling", + "slab", + "socket2", + "waker-fn", + "windows-sys 0.42.0", +] + +[[package]] +name = "async-lock" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" +dependencies = [ + "event-listener", + "futures-lite", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-channel", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils 0.8.12", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + [[package]] name = "async-stream" version = "0.3.3" @@ -209,6 +330,12 @@ dependencies = [ "syn", ] +[[package]] +name = "async-task" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" + [[package]] name = "async-trait" version = "0.1.58" @@ -236,6 +363,12 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "atomic-waker" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debc29dde2e69f9e47506b525f639ed42300fc014a3e007832592448fa8e4599" + [[package]] name = "autocfg" version = "1.1.0" @@ -312,6 +445,24 @@ dependencies = [ "crunchy 0.1.6", ] +[[package]] +name = "bimap" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0455254eb5c6964c4545d8bac815e1a1be4f3afe0ae695ea539c12d728d44b" +dependencies = [ + "serde", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bip0039" version = "0.9.0" @@ -349,7 +500,7 @@ checksum = "42b2a9a8e3c7544f5ce2b475f2f56580a3102b37e0ee001558ad4faedcf56cf4" dependencies = [ "bech32", "bitcoin_hashes", - "secp256k1", + "secp256k1 0.22.1", "serde", ] @@ -380,6 +531,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.5", +] + [[package]] name = "blake2b_simd" version = "0.5.11" @@ -473,6 +633,20 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" +[[package]] +name = "blocking" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c67b173a56acffd6d2326fb7ab938ba0b00a71480e14902b2591c87bc5741e8" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "atomic-waker", + "fastrand", + "futures-lite", +] + [[package]] name = "bls12_381" version = "0.6.1" @@ -701,6 +875,15 @@ dependencies = [ "syn", ] +[[package]] +name = "concurrent-queue" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e" +dependencies = [ + "crossbeam-utils 0.8.12", +] + [[package]] name = "const-oid" version = "0.7.1" @@ -966,6 +1149,12 @@ dependencies = [ "crypto_api", ] +[[package]] +name = "ct-codecs" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b7eb4404b8195a9abb6356f4ac07d8ba267045c8d6d220ac4dc992e6cc75df" + [[package]] name = "ct-logs" version = "0.8.0" @@ -975,6 +1164,16 @@ dependencies = [ "sct", ] +[[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "curve25519-dalek" version = "3.2.0" @@ -1221,6 +1420,7 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" dependencies = [ + "serde", "signature", ] @@ -1247,6 +1447,10 @@ checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ "curve25519-dalek", "ed25519", + "merlin", + "rand 0.7.3", + "serde", + "serde_bytes", "sha2 0.9.9", "zeroize", ] @@ -1334,6 +1538,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "eyre" version = "0.6.8" @@ -1359,6 +1569,43 @@ dependencies = [ "instant", ] +[[package]] +name = "ferveo" +version = "0.1.1" +source = "git+https://github.com/anoma/ferveo#1022ab2c7ccc689abcc05e5a08df6fb0c2a3fc65" +dependencies = [ + "anyhow", + "ark-bls12-381", + "ark-ec", + "ark-ed-on-bls12-381", + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "bincode", + "blake2", + "blake2b_simd 1.0.0", + "borsh", + "digest 0.10.5", + "ed25519-dalek", + "either", + "ferveo-common", + "group-threshold-cryptography", + "hex", + "itertools", + "measure_time", + "miracl_core", + "num", + "rand 0.7.3", + "rand 0.8.5", + "serde", + "serde_bytes", + "serde_json", + "subproductdomain", + "subtle", + "zeroize", +] + [[package]] name = "ferveo-common" version = "0.1.0" @@ -1482,6 +1729,21 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + [[package]] name = "futures-macro" version = "0.3.25" @@ -1580,6 +1842,18 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "group" version = "0.11.0" @@ -1592,6 +1866,30 @@ dependencies = [ "subtle", ] +[[package]] +name = "group-threshold-cryptography" +version = "0.1.0" +source = "git+https://github.com/anoma/ferveo#1022ab2c7ccc689abcc05e5a08df6fb0c2a3fc65" +dependencies = [ + "anyhow", + "ark-bls12-381", + "ark-ec", + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "blake2b_simd 1.0.0", + "chacha20", + "hex", + "itertools", + "miracl_core", + "rand 0.8.5", + "rand_core 0.6.4", + "rayon", + "subproductdomain", + "thiserror", +] + [[package]] name = "gumdrop" version = "0.8.1" @@ -2083,6 +2381,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] @@ -2142,6 +2443,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9b7d56ba4a8344d6be9729995e6b06f928af29998cdf79fe390cbf6b1fee838" +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -2246,6 +2556,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if 1.0.0", + "value-bag", ] [[package]] @@ -2302,6 +2613,8 @@ dependencies = [ "lazy_static", "rand 0.8.5", "rand_core 0.6.4", + "ripemd160", + "secp256k1 0.20.3", "serde", "sha2 0.9.9", "subtle", @@ -2345,6 +2658,16 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" +[[package]] +name = "measure_time" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56220900f1a0923789ecd6bf25fbae8af3b2f1ff3e9e297fc9b6b8674dd4d852" +dependencies = [ + "instant", + "log", +] + [[package]] name = "memchr" version = "2.5.0" @@ -2393,6 +2716,18 @@ dependencies = [ "nonempty", ] +[[package]] +name = "merlin" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e261cf0f8b3c42ded9f7d2bb59dea03aa52bc8a1cbc7482f9fc3fd1229d3b42" +dependencies = [ + "byteorder", + "keccak", + "rand_core 0.5.1", + "zeroize", +] + [[package]] name = "mime" version = "0.3.16" @@ -2420,6 +2755,12 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "miracl_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94c7128ba23c81f6471141b90f17654f89ef44a56e14b8a4dd0fddfccd655277" + [[package]] name = "moka" version = "0.8.6" @@ -2458,8 +2799,10 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" name = "namada" version = "0.12.0" dependencies = [ + "async-std", "async-trait", "bellman", + "bimap", "bls12_381", "borsh", "circular-queue", @@ -2474,19 +2817,26 @@ dependencies = [ "masp_proofs", "namada_core", "namada_proof_of_stake", + "orion", "parity-wasm", "paste", "proptest", "prost", "pwasm-utils", + "rand 0.8.5", + "rand_core 0.6.4", "rayon", "rust_decimal", + "serde", "serde_json", "sha2 0.9.9", "tempfile", "tendermint", "tendermint-proto", + "tendermint-rpc", "thiserror", + "tokio", + "toml", "tracing", "wasmer", "wasmer-cache", @@ -2503,6 +2853,7 @@ name = "namada_core" version = "0.12.0" dependencies = [ "ark-bls12-381", + "ark-ec", "ark-serialize", "bech32", "bellman", @@ -2511,7 +2862,9 @@ dependencies = [ "data-encoding", "derivative", "ed25519-consensus", + "ferveo", "ferveo-common", + "group-threshold-cryptography", "ibc", "ibc-proto", "ics23", @@ -2656,6 +3009,20 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" +[[package]] +name = "num" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.3" @@ -2668,6 +3035,15 @@ dependencies = [ "serde", ] +[[package]] +name = "num-complex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d" +dependencies = [ + "num-traits", +] + [[package]] name = "num-derive" version = "0.3.3" @@ -2689,6 +3065,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.1" @@ -2787,6 +3174,18 @@ dependencies = [ "zcash_note_encryption 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "orion" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6624905ddd92e460ff0685567539ed1ac985b2dee4c92c7edcd64fce905b00c" +dependencies = [ + "ct-codecs", + "getrandom 0.2.8", + "subtle", + "zeroize", +] + [[package]] name = "pairing" version = "0.21.0" @@ -2802,6 +3201,12 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1ad0aff30c1da14b1254fcb2af73e1fa9a28670e584a626f53a369d0e157304" +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + [[package]] name = "parking_lot" version = "0.12.1" @@ -2911,11 +3316,10 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pest" -version = "2.4.1" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a528564cc62c19a7acac4d81e01f39e53e25e17b934878f4c6d25cc2836e62f8" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" dependencies = [ - "thiserror", "ucd-trie", ] @@ -2972,6 +3376,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "polling" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22122d5ec4f9fe1b3916419b76be1e80bcb93f618d071d2edf841b137b2a2bd6" +dependencies = [ + "autocfg", + "cfg-if 1.0.0", + "libc", + "log", + "wepoll-ffi", + "windows-sys 0.42.0", +] + [[package]] name = "poly1305" version = "0.7.2" @@ -3681,16 +4099,34 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secp256k1" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d03ceae636d0fed5bae6a7f4f664354c5f4fcedf6eef053fef17e49f837d0a" +dependencies = [ + "secp256k1-sys 0.4.2", +] + [[package]] name = "secp256k1" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26947345339603ae8395f68e2f3d85a6b0a8ddfe6315818e80b8504415099db0" dependencies = [ - "secp256k1-sys", + "secp256k1-sys 0.5.2", "serde", ] +[[package]] +name = "secp256k1-sys" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957da2573cde917463ece3570eab4a0b3f19de6f1646cde62e6fd3868f566036" +dependencies = [ + "cc", +] + [[package]] name = "secp256k1-sys" version = "0.5.2" @@ -3990,6 +4426,19 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "subproductdomain" +version = "0.1.0" +source = "git+https://github.com/anoma/ferveo#1022ab2c7ccc689abcc05e5a08df6fb0c2a3fc65" +dependencies = [ + "anyhow", + "ark-ec", + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", +] + [[package]] name = "subtle" version = "2.4.1" @@ -4069,7 +4518,7 @@ dependencies = [ [[package]] name = "tendermint" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=679227ab920dbb45cdd5acb97f49c4fe2ebb5a44#679227ab920dbb45cdd5acb97f49c4fe2ebb5a44" dependencies = [ "async-trait", "bytes", @@ -4099,7 +4548,7 @@ dependencies = [ [[package]] name = "tendermint-config" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=679227ab920dbb45cdd5acb97f49c4fe2ebb5a44#679227ab920dbb45cdd5acb97f49c4fe2ebb5a44" dependencies = [ "flex-error", "serde", @@ -4112,7 +4561,7 @@ dependencies = [ [[package]] name = "tendermint-light-client" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=679227ab920dbb45cdd5acb97f49c4fe2ebb5a44#679227ab920dbb45cdd5acb97f49c4fe2ebb5a44" dependencies = [ "contracts", "crossbeam-channel 0.4.4", @@ -4133,7 +4582,7 @@ dependencies = [ [[package]] name = "tendermint-light-client-verifier" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=679227ab920dbb45cdd5acb97f49c4fe2ebb5a44#679227ab920dbb45cdd5acb97f49c4fe2ebb5a44" dependencies = [ "derive_more", "flex-error", @@ -4145,7 +4594,7 @@ dependencies = [ [[package]] name = "tendermint-proto" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=679227ab920dbb45cdd5acb97f49c4fe2ebb5a44#679227ab920dbb45cdd5acb97f49c4fe2ebb5a44" dependencies = [ "bytes", "flex-error", @@ -4162,7 +4611,7 @@ dependencies = [ [[package]] name = "tendermint-rpc" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=679227ab920dbb45cdd5acb97f49c4fe2ebb5a44#679227ab920dbb45cdd5acb97f49c4fe2ebb5a44" dependencies = [ "async-trait", "async-tungstenite", @@ -4195,7 +4644,7 @@ dependencies = [ [[package]] name = "tendermint-testgen" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=e6c684731f21bffd89886d3e91074b96aee074ba#e6c684731f21bffd89886d3e91074b96aee074ba" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=679227ab920dbb45cdd5acb97f49c4fe2ebb5a44#679227ab920dbb45cdd5acb97f49c4fe2ebb5a44" dependencies = [ "ed25519-dalek", "gumdrop", @@ -4229,18 +4678,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.37" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.37" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", @@ -4711,6 +5160,16 @@ dependencies = [ "getrandom 0.2.8", ] +[[package]] +name = "value-bag" +version = "1.0.0-alpha.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55" +dependencies = [ + "ctor", + "version_check", +] + [[package]] name = "version_check" version = "0.9.4" @@ -4726,6 +5185,12 @@ dependencies = [ "libc", ] +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + [[package]] name = "walkdir" version = "2.3.2" @@ -4790,6 +5255,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.83" @@ -5126,6 +5603,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + [[package]] name = "which" version = "4.3.0" diff --git a/wasm_for_tests/wasm_source/Cargo.toml b/wasm_for_tests/wasm_source/Cargo.toml index dd17f4c0dc..7343556c44 100644 --- a/wasm_for_tests/wasm_source/Cargo.toml +++ b/wasm_for_tests/wasm_source/Cargo.toml @@ -38,13 +38,13 @@ borsh-derive = {git = "https://github.com/heliaxdev/borsh-rs.git", rev = "cd5223 borsh-derive-internal = {git = "https://github.com/heliaxdev/borsh-rs.git", rev = "cd5223e5103c4f139e0c54cf8259b7ec5ec4073a"} borsh-schema-derive-internal = {git = "https://github.com/heliaxdev/borsh-rs.git", rev = "cd5223e5103c4f139e0c54cf8259b7ec5ec4073a"} # patched to a commit on the `eth-bridge-integration+consensus-timeout` branch of our fork -tendermint = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} -tendermint-config = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} -tendermint-proto = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} -tendermint-rpc = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} -tendermint-testgen = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} -tendermint-light-client = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} -tendermint-light-client-verifier = {git = "https://github.com/heliaxdev/tendermint-rs.git", rev = "e6c684731f21bffd89886d3e91074b96aee074ba"} +tendermint = {git="https://github.com/heliaxdev/tendermint-rs.git", rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} +tendermint-config = {git="https://github.com/heliaxdev/tendermint-rs.git", rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} +tendermint-proto = {git="https://github.com/heliaxdev/tendermint-rs.git", rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} +tendermint-rpc = {git="https://github.com/heliaxdev/tendermint-rs.git", rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} +tendermint-testgen = {git="https://github.com/heliaxdev/tendermint-rs.git", rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} +tendermint-light-client = {git="https://github.com/heliaxdev/tendermint-rs.git", rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} +tendermint-light-client-verifier = {git="https://github.com/heliaxdev/tendermint-rs.git", rev="679227ab920dbb45cdd5acb97f49c4fe2ebb5a44"} # patched to a commit on the `eth-bridge-integration` branch of our fork ibc = {git = "https://github.com/heliaxdev/ibc-rs.git", rev = "f4703dfe2c1f25cc431279ab74f10f3e0f6827e2"}