From 6922d7b793d94ca3c019a71cc5f81a972f3410f6 Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Fri, 6 Oct 2023 00:25:06 +0200 Subject: [PATCH 1/6] Add wallet address generator binary - used for generating new seed phrase and addresses for mainnet --- Cargo.lock | 30 ++++ Cargo.toml | 101 +++++------ test/Cargo.toml | 1 + test/functional/test_runner.py | 1 + test/functional/wallet_generate_addresses.py | 165 ++++++++++++++++++ test/src/bin/test_wallet_address_generator.rs | 31 ++++ wallet/src/lib.rs | 2 +- .../wallet-address-generator-lib/Cargo.toml | 20 +++ .../wallet-address-generator-lib/src/lib.rs | 160 +++++++++++++++++ wallet/wallet-address-generator/Cargo.toml | 21 +++ wallet/wallet-address-generator/src/main.rs | 31 ++++ wallet/wallet-cli/Cargo.toml | 2 +- 12 files changed, 514 insertions(+), 51 deletions(-) create mode 100644 test/functional/wallet_generate_addresses.py create mode 100644 test/src/bin/test_wallet_address_generator.rs create mode 100644 wallet/wallet-address-generator-lib/Cargo.toml create mode 100644 wallet/wallet-address-generator-lib/src/lib.rs create mode 100644 wallet/wallet-address-generator/Cargo.toml create mode 100644 wallet/wallet-address-generator/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 0ee31830fb..a39c3ef6ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3665,6 +3665,7 @@ dependencies = [ "thiserror", "tokio", "utils", + "wallet-address-generator-lib", "wallet-cli-lib", ] @@ -7164,6 +7165,35 @@ dependencies = [ "zeroize", ] +[[package]] +name = "wallet-address-generator" +version = "0.1.2" +dependencies = [ + "clap", + "common", + "crypto", + "thiserror", + "utils", + "wallet", + "wallet-address-generator-lib", + "wallet-controller", + "wallet-types", +] + +[[package]] +name = "wallet-address-generator-lib" +version = "0.1.2" +dependencies = [ + "clap", + "common", + "crypto", + "thiserror", + "utils", + "wallet", + "wallet-controller", + "wallet-types", +] + [[package]] name = "wallet-cli" version = "0.1.2" diff --git a/Cargo.toml b/Cargo.toml index 241034899f..1e8ceea729 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,55 +11,57 @@ edition = "2021" [workspace] members = [ - "accounting", # Accounting and balances abstractions. - "api-server/api-server-common", # API server, for light-wallets and block explorers: common between web-server and scanner. - "api-server/storage-test-suite",# Test suite for the abstract storage layer of the API server to ensure consistent behavior. - "api-server/scanner-daemon", # API server, for light-wallets and block explorers: blockchain scanner daemon. - "api-server/scanner-lib", # API server, for light-wallets and block explorers: blockchain scanner library. - "api-server/web-server", # API server, for light-wallets and block explorers: web-server. - "blockprod", # Block production with whatever consensus algorithm. - "chainstate", # Code on chainstate of blocks and transactions. - "chainstate/test-suite", # Tests for the chainstate, separated to make use of the chainstate test framework. - "common", # Everything else, until it's moved to another crate. - "consensus", # Consensus related logic. - "crypto", # Cryptographic primitives and their interfaces. - "dns_server", # DNS-server. - "logging", # Logging engine and its interfaces. - "mempool", # Mempool interface and implementation. - "mempool/types", # Common mempool types. - "merkletree", # Merkle tree implementation with merkle proofs. - "mocks", # Mock implementations of our traits (used for testing) - "node-daemon", # Node terminal binary. - "node-gui", # Node GUI binary. - "node-lib", # Node lib; the common library between daemon, tui and gui node executables. - "p2p", # P2p communication interfaces and protocols. - "p2p/backend-test-suite", # P2p backend agnostic tests. - "p2p/types", # P2p support types with minimal dependencies. - "pos_accounting", # PoS accounting and balances abstractions. - "rpc", # Rpc abstraction and implementation. - "script", # Bitcoin script and its interfaces. - "serialization", # Full featured serialization interfaces and implementations. - "serialization/core", # Serialization core tools. - "serialization/tagged", # Serialization for direct/tagged encoding style. - "serialization/tagged/derive", # direct/tagged encoding style derive macros. - "storage", # storage abstraction layer and its implementation. - "storage/backend-test-suite", # Tests for validating storage backend implementations. - "storage/core", # Core backend-agnostic storage abstraction. - "storage/inmemory", # In-memory storage backend implementation. - "storage/lmdb", # LMDB-based persistent storage backend implementation. - "storage/sqlite", # SQLite-based persistent storage backend implementation. - "subsystem", # Utilities for working with concurrent subsystems. - "test", # Integration tests. - "test-rpc-functions", # RPC functions specifically for tests. - "test-utils", # Various utilities for tests. - "utils", # Various utilities. - "utxo", # Utxo and related utilities (cache, undo, etc.). - "wallet", # Wallet primitives. - "wallet/wallet-cli", # Wallet CLI/REPL binary. - "wallet/wallet-cli-lib", # Wallet CLI/REPL lib. - "wallet/wallet-controller", # Common code for wallet UI applications. - "wallet/wallet-node-client", # Wallet-to-node communication tools. - "wasm-crypto", # WASM bindings for the crypto crate. + "accounting", # Accounting and balances abstractions. + "api-server/api-server-common", # API server, for light-wallets and block explorers: common between web-server and scanner. + "api-server/storage-test-suite", # Test suite for the abstract storage layer of the API server to ensure consistent behavior. + "api-server/scanner-daemon", # API server, for light-wallets and block explorers: blockchain scanner daemon. + "api-server/scanner-lib", # API server, for light-wallets and block explorers: blockchain scanner library. + "api-server/web-server", # API server, for light-wallets and block explorers: web-server. + "blockprod", # Block production with whatever consensus algorithm. + "chainstate", # Code on chainstate of blocks and transactions. + "chainstate/test-suite", # Tests for the chainstate, separated to make use of the chainstate test framework. + "common", # Everything else, until it's moved to another crate. + "consensus", # Consensus related logic. + "crypto", # Cryptographic primitives and their interfaces. + "dns_server", # DNS-server. + "logging", # Logging engine and its interfaces. + "mempool", # Mempool interface and implementation. + "mempool/types", # Common mempool types. + "merkletree", # Merkle tree implementation with merkle proofs. + "mocks", # Mock implementations of our traits (used for testing) + "node-daemon", # Node terminal binary. + "node-gui", # Node GUI binary. + "node-lib", # Node lib; the common library between daemon, tui and gui node executables. + "p2p", # P2p communication interfaces and protocols. + "p2p/backend-test-suite", # P2p backend agnostic tests. + "p2p/types", # P2p support types with minimal dependencies. + "pos_accounting", # PoS accounting and balances abstractions. + "rpc", # Rpc abstraction and implementation. + "script", # Bitcoin script and its interfaces. + "serialization", # Full featured serialization interfaces and implementations. + "serialization/core", # Serialization core tools. + "serialization/tagged", # Serialization for direct/tagged encoding style. + "serialization/tagged/derive", # direct/tagged encoding style derive macros. + "storage", # storage abstraction layer and its implementation. + "storage/backend-test-suite", # Tests for validating storage backend implementations. + "storage/core", # Core backend-agnostic storage abstraction. + "storage/inmemory", # In-memory storage backend implementation. + "storage/lmdb", # LMDB-based persistent storage backend implementation. + "storage/sqlite", # SQLite-based persistent storage backend implementation. + "subsystem", # Utilities for working with concurrent subsystems. + "test", # Integration tests. + "test-rpc-functions", # RPC functions specifically for tests. + "test-utils", # Various utilities for tests. + "utils", # Various utilities. + "utxo", # Utxo and related utilities (cache, undo, etc.). + "wallet", # Wallet primitives. + "wallet/wallet-cli", # Wallet CLI/REPL binary. + "wallet/wallet-cli-lib", # Wallet CLI/REPL lib. + "wallet/wallet-controller", # Common code for wallet UI applications. + "wallet/wallet-node-client", # Wallet-to-node communication tools. + "wallet/wallet-address-generator", # Wallet address generator binary. + "wallet/wallet-address-generator-lib",# Wallet address generator lib. + "wasm-crypto", # WASM bindings for the crypto crate. ] default-members = [ @@ -83,6 +85,7 @@ default-members = [ "utxo", "wallet", "wallet/wallet-cli", + "wallet/wallet-address-generator", ] [dependencies] diff --git a/test/Cargo.toml b/test/Cargo.toml index 4f0ae9d9f9..1a0e63fc47 100644 --- a/test/Cargo.toml +++ b/test/Cargo.toml @@ -10,6 +10,7 @@ homepage = "https://github.com/mintlayer/mintlayer-core/issues" utils = { path = "../utils" } node-lib = { path = "../node-lib" } wallet-cli-lib = { path = "../wallet/wallet-cli-lib" } +wallet-address-generator-lib = { path = "../wallet/wallet-address-generator-lib" } clap = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ['full'] } diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 44e6478547..9545c4d9b8 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -134,6 +134,7 @@ class UnicodeOnWindowsError(ValueError): 'wallet_nfts.py', 'wallet_delegations.py', 'wallet_high_fee.py', + 'wallet_generate_addresses.py', 'mempool_basic_reorg.py', 'mempool_eviction.py', 'mempool_ibd.py', diff --git a/test/functional/wallet_generate_addresses.py b/test/functional/wallet_generate_addresses.py new file mode 100644 index 0000000000..eecf5fdbfd --- /dev/null +++ b/test/functional/wallet_generate_addresses.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023 RBB S.r.l +# Copyright (c) 2017-2021 The Bitcoin Core developers +# opensource@mintlayer.org +# SPDX-License-Identifier: MIT +# Licensed under the MIT License; +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Wallet address generator test + +Check that: +* We can create a new wallet, +* get an address +* send coins to the wallet's address +* sync the wallet with the node +* check balance +""" + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.mintlayer import (make_tx, reward_input, tx_input, ATOMS_PER_COIN) +from test_framework.util import assert_in, assert_equal +from test_framework.mintlayer import mintlayer_hash, block_input_data_obj +from test_framework.wallet_cli_controller import WalletCliController + +import asyncio +import sys +import subprocess +import os +import re + + +class WalletAddressGenerator(BitcoinTestFramework): + + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 1 + self.extra_args = [[ + "--blockprod-min-peers-to-produce-blocks=0", + ]] + + def setup_network(self): + self.setup_nodes() + self.sync_all(self.nodes[0:1]) + + def generate_block(self): + node = self.nodes[0] + + block_input_data = { "PoW": { "reward_destination": "AnyoneCanSpend" } } + block_input_data = block_input_data_obj.encode(block_input_data).to_hex()[2:] + + # create a new block, taking transactions from mempool + block = node.blockprod_generate_block(block_input_data, None) + node.chainstate_submit_block(block) + block_id = node.chainstate_best_block_id() + + # Wait for mempool to sync + self.wait_until(lambda: node.mempool_local_best_block_id() == block_id, timeout = 5) + + return block_id + + def run_generate_addresses(self, args = []): + addr_generator_cli = os.path.join(self.config["environment"]["BUILDDIR"], "test_wallet_address_generator"+self.config["environment"]["EXEEXT"] ) + args = ["--network", "regtest"] + args + self.log.info(f"sending args {args}") + + result = subprocess.run([addr_generator_cli, *args], stdout=subprocess.PIPE) + output = result.stdout.decode() + self.log.info(output) + + seed_phrase = re.search(r'"(.*?)"', output) + if not seed_phrase: + return None, None + seed_phrase = seed_phrase.group(1) + + addresses = output.splitlines()[2:] + + return seed_phrase, addresses + + + def run_test(self): + if 'win32' in sys.platform: + asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) + asyncio.run(self.async_test()) + + async def async_test(self): + node = self.nodes[0] + async with WalletCliController(node, self.config, self.log) as wallet: + # new wallet + await wallet.create_wallet() + + # check it is on genesis + best_block_height = await wallet.get_best_block_height() + self.log.info(f"best block height = {best_block_height}") + assert_equal(best_block_height, '0') + + # new address + pub_key_bytes = await wallet.new_public_key() + assert_equal(len(pub_key_bytes), 33) + + # Get chain tip + tip_id = node.chainstate_best_block_id() + self.log.debug(f'Tip: {tip_id}') + + # Submit a valid transaction + output = { + 'Transfer': [ { 'Coin': 100 * ATOMS_PER_COIN }, { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': pub_key_bytes}}} } ], + } + encoded_tx, tx_id = make_tx([reward_input(tip_id)], [output], 0) + + self.log.debug(f"Encoded transaction {tx_id}: {encoded_tx}") + + node.mempool_submit_transaction(encoded_tx) + assert node.mempool_contains_tx(tx_id) + + block_id = self.generate_block() # Block 1 + assert not node.mempool_contains_tx(tx_id) + + # sync the wallet + assert_in("Success", await wallet.sync()) + + # check wallet best block if it is synced + best_block_height = await wallet.get_best_block_height() + assert_equal(best_block_height, '1') + + best_block_id = await wallet.get_best_block() + assert_equal(best_block_id, block_id) + + assert_in("Coins amount: 100", await wallet.get_balance()) + + # use the new CLI tool to create a new seed_phrase and some addresses + seed_phrase, addresses = self.run_generate_addresses() + assert seed_phrase is not None + assert addresses is not None + + assert_equal(len(addresses), 20) + + # send some a coin to each one of the addresses to confirm all of them are valid + for addr in addresses: + assert_in("The transaction was submitted successfully", await wallet.send_to_address(addr, 1)) + self.generate_block() + + # close this wallet and create a new one with the new seed phrase + await wallet.close_wallet() + assert_in("New wallet created successfully", await wallet.recover_wallet(seed_phrase)) + assert_in("Success", await wallet.sync()) + assert_in(f"Coins amount: {len(addresses)}", await wallet.get_balance()) + + # check that if we specify the same seed phrase it will generate the same addresses + new_seed_phrase, new_addresses = self.run_generate_addresses(["--mnemonic", seed_phrase]) + assert_equal(seed_phrase, new_seed_phrase) + assert_equal(addresses, new_addresses) + + +if __name__ == '__main__': + WalletAddressGenerator().main() + + diff --git a/test/src/bin/test_wallet_address_generator.rs b/test/src/bin/test_wallet_address_generator.rs new file mode 100644 index 0000000000..782f2286e8 --- /dev/null +++ b/test/src/bin/test_wallet_address_generator.rs @@ -0,0 +1,31 @@ +// Copyright (c) 2023 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use clap::Parser; +use wallet_address_generator_lib::CliArgs; + +fn main() { + utils::rust_backtrace::enable(); + + if std::env::var("RUST_LOG").is_err() { + std::env::set_var("RUST_LOG", "info"); + } + + let args = CliArgs::parse(); + wallet_address_generator_lib::run(args).unwrap_or_else(|err| { + eprintln!("{}", err); + std::process::exit(1); + }) +} diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 334b5190a3..debbf3d44e 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -14,7 +14,7 @@ // limitations under the License. pub mod account; -mod key_chain; +pub mod key_chain; pub mod send_request; pub mod version; pub mod wallet; diff --git a/wallet/wallet-address-generator-lib/Cargo.toml b/wallet/wallet-address-generator-lib/Cargo.toml new file mode 100644 index 0000000000..6224cb60bb --- /dev/null +++ b/wallet/wallet-address-generator-lib/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "wallet-address-generator-lib" +license.workspace = true +edition.workspace = true +version.workspace = true +rust-version.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +common = { path = "../../common" } +utils = { path = "../../utils" } +wallet-controller = { path = "../wallet-controller" } +wallet-types = { path = "../types" } +wallet = { path = "../../wallet" } +crypto = { path = "../../crypto" } + +clap = { workspace = true, features = ["derive"] } + +thiserror.workspace = true diff --git a/wallet/wallet-address-generator-lib/src/lib.rs b/wallet/wallet-address-generator-lib/src/lib.rs new file mode 100644 index 0000000000..c0f12a0591 --- /dev/null +++ b/wallet/wallet-address-generator-lib/src/lib.rs @@ -0,0 +1,160 @@ +// Copyright (c) 2023 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use clap::{Parser, ValueEnum}; +use common::address::pubkeyhash::PublicKeyHash; +use common::address::Address; +use common::chain::config::{Builder, ChainType}; +use common::chain::{ChainConfig, Destination}; +use crypto::key::extended::{ExtendedPrivateKey, ExtendedPublicKey}; +use crypto::key::hdkd::u31::U31; +use crypto::key::hdkd::{child_number::ChildNumber, derivable::Derivable}; +use utils::ensure; +use wallet::WalletError; +use wallet::{ + key_chain::{make_account_path, KeyChainError, MasterKeyChain}, + WalletResult, +}; +use wallet_types::KeyPurpose; + +#[derive(Clone, Debug, ValueEnum)] +pub enum Network { + Mainnet, + Testnet, + Regtest, + Signet, +} + +impl From for ChainType { + fn from(value: Network) -> Self { + match value { + Network::Mainnet => ChainType::Mainnet, + Network::Testnet => ChainType::Testnet, + Network::Regtest => ChainType::Regtest, + Network::Signet => ChainType::Signet, + } + } +} + +#[derive(Parser, Debug)] +#[clap(version)] +pub struct CliArgs { + /// Which network to generate the addresses for + #[arg(long, value_enum, default_value_t = Network::Mainnet)] + pub network: Network, + + /// Number of addresses to generate and display + #[clap(long, default_value_t = 20)] + pub number_addresses: u8, + + /// Mnemonic phrase (12, 15, or 24 words as a single quoted argument). If not specified, a new mnemonic phrase is generated and printed. + #[clap(long)] + pub mnemonic: Option, +} + +#[derive(thiserror::Error, Debug)] +pub enum CliError { + #[error("Invalid input: {0}")] + InvalidInput(String), + #[error("Invalid mnemonic: {0}")] + InvalidMnemonic(wallet_controller::mnemonic::Error), + #[error("WalletError error: {0}")] + WalletError(#[from] WalletError), +} + +pub fn run(args: CliArgs) -> Result<(), CliError> { + ensure!( + args.number_addresses <= 20, + CliError::InvalidInput("Cannot generate more than 20 addresses".into()) + ); + + let (root_key, seed_phrase) = root_key_and_mnemonic(&args.mnemonic)?; + + let chain_config = Builder::new(args.network.into()).build(); + + let receive_funds_pkey = to_receiving_pub_key(&chain_config, root_key)?; + + let addresses = generate_addresses(args.number_addresses, receive_funds_pkey, chain_config)?; + + if let Some(mnemonic) = args.mnemonic { + println!("Using your seed phrase: \"{}\"", mnemonic); + } else { + println!("Your new generated seed phrase is: \"{}\"", seed_phrase); + } + + println!("And your new addresses are:"); + for addr in addresses { + println!("{}", addr) + } + + Ok(()) +} + +fn generate_addresses( + number_addresses: u8, + receive_funds_pkey: ExtendedPublicKey, + chain_config: ChainConfig, +) -> Result>, wallet::WalletError> { + (0..number_addresses) + .map(|key_index| -> WalletResult> { + let public_key = receive_funds_pkey + .clone() + .derive_child(ChildNumber::from_normal( + U31::from_u32(key_index as u32).expect("MSB bit not set"), + )) + .map_err(KeyChainError::from)? + .into_public_key(); + + let public_key_hash = PublicKeyHash::from(&public_key); + + Ok(Address::new( + &chain_config, + &Destination::Address(public_key_hash), + )?) + }) + .collect::>>() +} + +fn to_receiving_pub_key( + chain_config: &ChainConfig, + root_key: ExtendedPrivateKey, +) -> Result { + let account_index = U31::ZERO; + let account_path = make_account_path(chain_config, account_index); + let account_privkey = + root_key.derive_absolute_path(&account_path).map_err(KeyChainError::from)?; + let receive_funds_pkey = account_privkey + .to_public_key() + .derive_child(KeyPurpose::ReceiveFunds.get_deterministic_index()) + .map_err(KeyChainError::from)?; + Ok(receive_funds_pkey) +} + +/// Generate a new mnemonic and a root private key +fn root_key_and_mnemonic( + mnemonic: &Option, +) -> Result<(ExtendedPrivateKey, String), CliError> { + let language = wallet::wallet::Language::English; + let mnemonic = match mnemonic { + Some(mnemonic) => wallet_controller::mnemonic::parse_mnemonic(language, mnemonic) + .map_err(CliError::InvalidMnemonic)?, + None => wallet_controller::mnemonic::generate_new_mnemonic(language), + }; + let (root_key, _root_vrf_key, _mnemonic) = + MasterKeyChain::mnemonic_to_root_key(&mnemonic.to_string(), None) + .map_err(WalletError::from)?; + + Ok((root_key, mnemonic.to_string())) +} diff --git a/wallet/wallet-address-generator/Cargo.toml b/wallet/wallet-address-generator/Cargo.toml new file mode 100644 index 0000000000..f2e37e40a3 --- /dev/null +++ b/wallet/wallet-address-generator/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "wallet-address-generator" +license.workspace = true +edition.workspace = true +version.workspace = true +rust-version.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +common = { path = "../../common" } +utils = { path = "../../utils" } +wallet-controller = { path = "../wallet-controller" } +wallet-address-generator-lib = { path = "../wallet-address-generator-lib" } +wallet-types = { path = "../types" } +wallet = { path = "../../wallet" } +crypto = { path = "../../crypto" } + +clap = { workspace = true, features = ["derive"] } + +thiserror.workspace = true diff --git a/wallet/wallet-address-generator/src/main.rs b/wallet/wallet-address-generator/src/main.rs new file mode 100644 index 0000000000..782f2286e8 --- /dev/null +++ b/wallet/wallet-address-generator/src/main.rs @@ -0,0 +1,31 @@ +// Copyright (c) 2023 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use clap::Parser; +use wallet_address_generator_lib::CliArgs; + +fn main() { + utils::rust_backtrace::enable(); + + if std::env::var("RUST_LOG").is_err() { + std::env::set_var("RUST_LOG", "info"); + } + + let args = CliArgs::parse(); + wallet_address_generator_lib::run(args).unwrap_or_else(|err| { + eprintln!("{}", err); + std::process::exit(1); + }) +} diff --git a/wallet/wallet-cli/Cargo.toml b/wallet/wallet-cli/Cargo.toml index 33bc9691a4..e33c00b78d 100644 --- a/wallet/wallet-cli/Cargo.toml +++ b/wallet/wallet-cli/Cargo.toml @@ -11,5 +11,5 @@ rust-version.workspace = true utils = { path = "../../utils" } wallet-cli-lib = { path = "../wallet-cli-lib" } -clap = { version = "4", features = ["derive"] } +clap = { workspace = true, features = ["derive"] } tokio = { workspace = true, default-features = false, features = ["io-util", "macros", "net", "rt", "sync"] } From d1074d822b7c158691a45bb4e7ed5ad8e43bf8b0 Mon Sep 17 00:00:00 2001 From: Samer Afach Date: Fri, 6 Oct 2023 12:34:28 +0400 Subject: [PATCH 2/6] Minor refactoring --- wallet/wallet-address-generator-lib/src/lib.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/wallet/wallet-address-generator-lib/src/lib.rs b/wallet/wallet-address-generator-lib/src/lib.rs index c0f12a0591..f26f595779 100644 --- a/wallet/wallet-address-generator-lib/src/lib.rs +++ b/wallet/wallet-address-generator-lib/src/lib.rs @@ -51,13 +51,13 @@ impl From for ChainType { #[derive(Parser, Debug)] #[clap(version)] pub struct CliArgs { - /// Which network to generate the addresses for + /// The network, for which addresses will be generated #[arg(long, value_enum, default_value_t = Network::Mainnet)] pub network: Network, /// Number of addresses to generate and display - #[clap(long, default_value_t = 20)] - pub number_addresses: u8, + #[clap(long, default_value_t = 1)] + pub address_count: u8, /// Mnemonic phrase (12, 15, or 24 words as a single quoted argument). If not specified, a new mnemonic phrase is generated and printed. #[clap(long)] @@ -76,7 +76,7 @@ pub enum CliError { pub fn run(args: CliArgs) -> Result<(), CliError> { ensure!( - args.number_addresses <= 20, + args.address_count <= 20, CliError::InvalidInput("Cannot generate more than 20 addresses".into()) ); @@ -86,7 +86,7 @@ pub fn run(args: CliArgs) -> Result<(), CliError> { let receive_funds_pkey = to_receiving_pub_key(&chain_config, root_key)?; - let addresses = generate_addresses(args.number_addresses, receive_funds_pkey, chain_config)?; + let addresses = generate_addresses(args.address_count, receive_funds_pkey, chain_config)?; if let Some(mnemonic) = args.mnemonic { println!("Using your seed phrase: \"{}\"", mnemonic); From b615759ea7122a0bd88828582892dde6a339b14f Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Fri, 6 Oct 2023 10:51:19 +0200 Subject: [PATCH 3/6] Use LOOKAHEAD_SIZE constant --- wallet/wallet-address-generator-lib/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wallet/wallet-address-generator-lib/src/lib.rs b/wallet/wallet-address-generator-lib/src/lib.rs index f26f595779..e29b8acc75 100644 --- a/wallet/wallet-address-generator-lib/src/lib.rs +++ b/wallet/wallet-address-generator-lib/src/lib.rs @@ -22,6 +22,7 @@ use crypto::key::extended::{ExtendedPrivateKey, ExtendedPublicKey}; use crypto::key::hdkd::u31::U31; use crypto::key::hdkd::{child_number::ChildNumber, derivable::Derivable}; use utils::ensure; +use wallet::key_chain::LOOKAHEAD_SIZE; use wallet::WalletError; use wallet::{ key_chain::{make_account_path, KeyChainError, MasterKeyChain}, @@ -76,7 +77,7 @@ pub enum CliError { pub fn run(args: CliArgs) -> Result<(), CliError> { ensure!( - args.address_count <= 20, + args.address_count as u32 <= LOOKAHEAD_SIZE, CliError::InvalidInput("Cannot generate more than 20 addresses".into()) ); From 9e933b78b4a0fcc326a18f2ba444aef0c30aba2a Mon Sep 17 00:00:00 2001 From: Samer Afach Date: Fri, 6 Oct 2023 12:55:28 +0400 Subject: [PATCH 4/6] Reformat output --- wallet/wallet-address-generator-lib/src/lib.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/wallet/wallet-address-generator-lib/src/lib.rs b/wallet/wallet-address-generator-lib/src/lib.rs index f26f595779..fa96234159 100644 --- a/wallet/wallet-address-generator-lib/src/lib.rs +++ b/wallet/wallet-address-generator-lib/src/lib.rs @@ -88,16 +88,23 @@ pub fn run(args: CliArgs) -> Result<(), CliError> { let addresses = generate_addresses(args.address_count, receive_funds_pkey, chain_config)?; + println!("\n"); if let Some(mnemonic) = args.mnemonic { - println!("Using your seed phrase: \"{}\"", mnemonic); + println!("Using the seed phrase you provided to generate address(es): {mnemonic}"); } else { - println!("Your new generated seed phrase is: \"{}\"", seed_phrase); + println!("No seed phrase provided. Generating a new one."); + println!("WARNING: MAKE SURE TO WRITE DOWN YOUR SEED PHRASE AND KEEP IT SAFE!"); + println!("============================Seed phrase============================="); + println!("{seed_phrase}"); + println!("====================================================================") } - println!("And your new addresses are:"); + println!("\n"); + println!("Your address(es) are:"); for addr in addresses { - println!("{}", addr) + println!("- {}", addr) } + println!("\n"); Ok(()) } From f93f5de96390dd65618ad7560487c6ac79f0c172 Mon Sep 17 00:00:00 2001 From: Samer Afach Date: Fri, 6 Oct 2023 13:21:22 +0400 Subject: [PATCH 5/6] Add printing network to wallet address generator --- wallet/wallet-address-generator-lib/src/lib.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/wallet/wallet-address-generator-lib/src/lib.rs b/wallet/wallet-address-generator-lib/src/lib.rs index b842174318..d459a5d0e8 100644 --- a/wallet/wallet-address-generator-lib/src/lib.rs +++ b/wallet/wallet-address-generator-lib/src/lib.rs @@ -30,7 +30,7 @@ use wallet::{ }; use wallet_types::KeyPurpose; -#[derive(Clone, Debug, ValueEnum)] +#[derive(Copy, Clone, Debug, ValueEnum)] pub enum Network { Mainnet, Testnet, @@ -49,6 +49,17 @@ impl From for ChainType { } } +impl std::fmt::Display for Network { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Network::Mainnet => write!(f, "Mainnet"), + Network::Testnet => write!(f, "Testnet"), + Network::Regtest => write!(f, "Regtest"), + Network::Signet => write!(f, "Signet"), + } + } +} + #[derive(Parser, Debug)] #[clap(version)] pub struct CliArgs { @@ -90,6 +101,11 @@ pub fn run(args: CliArgs) -> Result<(), CliError> { let addresses = generate_addresses(args.address_count, receive_funds_pkey, chain_config)?; println!("\n"); + println!( + "Generating addresses for network: {}", + args.network.to_string().to_uppercase() + ); + if let Some(mnemonic) = args.mnemonic { println!("Using the seed phrase you provided to generate address(es): {mnemonic}"); } else { From 6f231c430647ec7a1c3747a49e212afc404fc3b5 Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Fri, 6 Oct 2023 11:59:19 +0200 Subject: [PATCH 6/6] use LOOKAHEAD_SIZE in the error message and fix the tests --- test/functional/wallet_generate_addresses.py | 17 +++++++++++------ wallet/wallet-address-generator-lib/src/lib.rs | 5 ++++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/test/functional/wallet_generate_addresses.py b/test/functional/wallet_generate_addresses.py index eecf5fdbfd..a62b8e29e8 100644 --- a/test/functional/wallet_generate_addresses.py +++ b/test/functional/wallet_generate_addresses.py @@ -75,12 +75,16 @@ def run_generate_addresses(self, args = []): output = result.stdout.decode() self.log.info(output) - seed_phrase = re.search(r'"(.*?)"', output) - if not seed_phrase: + lines = output.splitlines() + if lines[3].startswith("Using the seed phrase you provided to generate address"): + seed_phrase = lines[3][lines[3].find(':')+2:] + addresses = [addr[2:] for addr in output.splitlines()[7:-2]] + elif lines[3].startswith("No seed phrase provided"): + seed_phrase = lines[6] + addresses = [addr[2:] for addr in output.splitlines()[11:-2]] + else: return None, None - seed_phrase = seed_phrase.group(1) - addresses = output.splitlines()[2:] return seed_phrase, addresses @@ -136,10 +140,11 @@ async def async_test(self): assert_in("Coins amount: 100", await wallet.get_balance()) # use the new CLI tool to create a new seed_phrase and some addresses - seed_phrase, addresses = self.run_generate_addresses() + seed_phrase, addresses = self.run_generate_addresses(["--address-count", "20"]) assert seed_phrase is not None assert addresses is not None + self.log.info(f"addresses '{addresses}'") assert_equal(len(addresses), 20) # send some a coin to each one of the addresses to confirm all of them are valid @@ -154,7 +159,7 @@ async def async_test(self): assert_in(f"Coins amount: {len(addresses)}", await wallet.get_balance()) # check that if we specify the same seed phrase it will generate the same addresses - new_seed_phrase, new_addresses = self.run_generate_addresses(["--mnemonic", seed_phrase]) + new_seed_phrase, new_addresses = self.run_generate_addresses(["--address-count", "20", "--mnemonic", seed_phrase]) assert_equal(seed_phrase, new_seed_phrase) assert_equal(addresses, new_addresses) diff --git a/wallet/wallet-address-generator-lib/src/lib.rs b/wallet/wallet-address-generator-lib/src/lib.rs index d459a5d0e8..0988a07649 100644 --- a/wallet/wallet-address-generator-lib/src/lib.rs +++ b/wallet/wallet-address-generator-lib/src/lib.rs @@ -89,7 +89,10 @@ pub enum CliError { pub fn run(args: CliArgs) -> Result<(), CliError> { ensure!( args.address_count as u32 <= LOOKAHEAD_SIZE, - CliError::InvalidInput("Cannot generate more than 20 addresses".into()) + CliError::InvalidInput(format!( + "Cannot generate more than {} addresses", + LOOKAHEAD_SIZE + )) ); let (root_key, seed_phrase) = root_key_and_mnemonic(&args.mnemonic)?;