Skip to content

Commit

Permalink
update account and wallet import handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
aspect committed Nov 3, 2023
1 parent 6e0cf79 commit 314f453
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 67 deletions.
98 changes: 97 additions & 1 deletion cli/src/modules/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,98 @@ impl Account {
let account_name = account_name.as_deref();
wizards::account::create(&ctx, prv_key_data_info, account_kind, account_name).await?;
}
"import" => {
if argv.is_empty() {
tprintln!(ctx, "usage: 'account import <import-type> <key-type> [extra keys]'");
tprintln!(ctx, "");
tprintln!(ctx, "examples:");
tprintln!(ctx, "");
ctx.term().help(
&[
("account import legacy-data", "Import KDX keydata file or kaspanet web wallet data on the same domain"),
(
"account import mnemonic bip32",
"Import Bip32 (12 or 24 word mnemonics used by kaspawallet, kaspium, onekey, tangem etc.)",
),
(
"account import mnemonic legacy",
"Import accounts 12 word mnemonic used by legacy applications (KDX and kaspanet web wallet)",
),
(
"account import mnemonic multisig [additional keys]",
"Import mnemonic and additional keys for a multisig account",
),
],
None,
)?;

return Ok(());
}

let import_kind = argv.remove(0);
match import_kind.as_ref() {
"legacy-data" => {
if !argv.is_empty() {
tprintln!(ctx, "usage: 'account import legacy-data'");
tprintln!(ctx, "too many arguments: {}\r\n", argv.join(" "));
return Ok(());
}

if exists_legacy_v0_keydata().await? {
let import_secret = Secret::new(
ctx.term()
.ask(true, "Enter the password for the account you are importing: ")
.await?
.trim()
.as_bytes()
.to_vec(),
);
let wallet_secret =
Secret::new(ctx.term().ask(true, "Enter wallet password: ").await?.trim().as_bytes().to_vec());
wallet.import_gen0_keydata(import_secret, wallet_secret, None).await?;
} else if application_runtime::is_web() {
return Err("'kaspanet' web wallet storage not found at this domain name".into());
} else {
return Err("KDX keydata file not found".into());
}
}
"mnemonic" => {
if argv.is_empty() {
tprintln!(ctx, "usage: 'account import mnemonic <bip32|legacy|multisig>'");
tprintln!(ctx, "please specify the mnemonic type");
tprintln!(ctx, "please use 'legacy' for 12-word KDX and kaspanet web wallet mnemonics\r\n");
return Ok(());
}

let account_kind = argv.remove(0);
let account_kind = account_kind.parse::<AccountKind>()?;

match account_kind {
AccountKind::Legacy | AccountKind::Bip32 => {
if !argv.is_empty() {
tprintln!(ctx, "too many arguments: {}\r\n", argv.join(" "));
return Ok(());
}
crate::wizards::import::import_with_mnemonic(&ctx, account_kind, &argv).await?;
}
AccountKind::MultiSig => {
crate::wizards::import::import_with_mnemonic(&ctx, account_kind, &argv).await?;
}
_ => {
tprintln!(ctx, "account import is not supported for this account type: '{account_kind}'\r\n");
return Ok(());
}
}

return Ok(());
}
_ => {
tprintln!(ctx, "unknown account import type: '{import_kind}'");
tprintln!(ctx, "supported import types are: 'mnemonic' or 'legacy-data'\r\n");
return Ok(());
}
}
}
"scan" | "sweep" => {
let len = argv.len();
let mut start = 0;
Expand Down Expand Up @@ -90,7 +182,11 @@ impl Account {
ctx.term().help(
&[
("create [<type>] [<name>]", "Create a new account (types: 'bip32' (default), 'legacy', 'multisig')"),
// ("import", "Import a private key using 24 or 12 word mnemonic"),
(
"import <import-type> [<key-type> [extra keys]]",
"Import accounts from a private key using 24 or 12 word mnemonic or legacy data \
(KDX and kaspanet web wallet). Use 'account import' for additional help.",
),
("name <name>", "Name or rename the selected account (use 'remove' to remove the name"),
("scan [<derivations>] or scan [<start>] [<derivations>]", "Scan extended address derivation chain (legacy accounts)"),
(
Expand Down
6 changes: 3 additions & 3 deletions cli/src/modules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub mod guide;
pub mod halt;
pub mod help;
pub mod history;
pub mod import;
// pub mod import;
pub mod list;
pub mod message;
pub mod miner;
Expand Down Expand Up @@ -52,8 +52,8 @@ pub fn register_handlers(cli: &Arc<KaspaCli>) -> Result<()> {
cli,
cli.handlers(),
[
account, address, close, connect, details, disconnect, estimate, exit, export, guide, help, history, import, rpc, list,
miner, message, monitor, mute, network, node, open, ping, reload, select, send, server, settings, sweep, track, transfer,
account, address, close, connect, details, disconnect, estimate, exit, export, guide, help, history, rpc, list, miner,
message, monitor, mute, network, node, open, ping, reload, select, send, server, settings, sweep, track, transfer,
wallet,
// halt,
// theme, start, stop
Expand Down
18 changes: 14 additions & 4 deletions cli/src/modules/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ impl Wallet {
return self.display_help(ctx, argv).await;
}

match argv.remove(0).as_str() {
let op = argv.remove(0);
match op.as_str() {
"list" => {
let wallets = ctx.store().wallet_list().await?;
if wallets.is_empty() {
Expand All @@ -32,7 +33,7 @@ impl Wallet {
tprintln!(ctx, "");
}
}
"create" => {
"create" | "import" => {
let wallet_name = if argv.is_empty() {
None
} else {
Expand All @@ -46,7 +47,8 @@ impl Wallet {
};

let wallet_name = wallet_name.as_deref();
wizards::wallet::create(&ctx, wallet_name).await?;
let import_with_mnemonic = op.as_str() == "import";
wizards::wallet::create(&ctx, wallet_name, import_with_mnemonic).await?;
}
"open" => {
let name = if let Some(name) = argv.first().cloned() {
Expand Down Expand Up @@ -98,7 +100,15 @@ impl Wallet {
ctx.term().help(
&[
("list", "List available local wallet files"),
("create [<name>]", "Create a new wallet"),
("create [<name>]", "Create a new bip32 wallet"),
(
"import [<name>]",
"Create a wallet from an existing mnemonic (bip32 only). \r\n\r\n\
To import legacy wallets (KDX or kaspanet) please create \
a new bip32 wallet and use the 'account import' command. \
Legacy wallets can only be imported as accounts. \
\r\n",
),
("open [<name>]", "Open an existing wallet (shorthand: 'open [<name>]')"),
("close", "Close an opened wallet (shorthand: 'close')"),
("hint", "Change the wallet phishing hint"),
Expand Down
16 changes: 8 additions & 8 deletions cli/src/wizards/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use kaspa_bip32::{Language, Mnemonic};
use kaspa_wallet_core::storage::AccountKind;
use std::sync::Arc;

pub async fn ask(term: &Arc<Terminal>) -> Result<Vec<String>> {
pub async fn prompt_for_mnemonic(term: &Arc<Terminal>) -> Result<Vec<String>> {
let mut words: Vec<String> = vec![];
loop {
if words.is_empty() {
Expand Down Expand Up @@ -52,7 +52,7 @@ pub(crate) async fn import_with_mnemonic(ctx: &Arc<KaspaCli>, account_kind: Acco
tprintln!(ctx);
let wallet_secret = Secret::new(term.ask(true, "Enter wallet password: ").await?.trim().as_bytes().to_vec());
tprintln!(ctx);
let mnemonic = ask(&term).await?;
let mnemonic = prompt_for_mnemonic(&term).await?;
tprintln!(ctx);
let length = mnemonic.len();
match account_kind {
Expand All @@ -70,15 +70,15 @@ pub(crate) async fn import_with_mnemonic(ctx: &Arc<KaspaCli>, account_kind: Acco
ctx,
"\
\
If your original wallet has a recovery passphrase, please enter it now.\
If your original wallet has a bip39 recovery passphrase, please enter it now.\
\
Specifically, this is not a wallet password. This is a secondary payment password\
used to encrypt your private key. This is known as a 'payment passphrase'\
'mnemonic password', or a 'recovery passphrase'. If your mnemonic was created\
Specifically, this is not a wallet password. This is a secondary mnemonic passphrase\
used to encrypt your mnemonic. This is known as a 'payment passphrase'\
'mnemonic passphrase', or a 'recovery passphrase'. If your mnemonic was created\
with a payment passphrase and you do not enter it now, the import process\
will generate a different private key.\
\
If you do not have a payment password, just press ENTER.\
If you do not have a bip39 recovery passphrase, press ENTER.\
\
",
);
Expand All @@ -103,7 +103,7 @@ pub(crate) async fn import_with_mnemonic(ctx: &Arc<KaspaCli>, account_kind: Acco
"y" | "Y" | "YES" | "yes"
) {
tprintln!(ctx);
let mnemonic = ask(&term).await?;
let mnemonic = prompt_for_mnemonic(&term).await?;
tprintln!(ctx);
let payment_secret = term.ask(true, "Enter payment password (optional): ").await?;
let payment_secret = payment_secret.trim().is_not_empty().then(|| Secret::new(payment_secret.trim().as_bytes().to_vec()));
Expand Down
118 changes: 74 additions & 44 deletions cli/src/wizards/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::result::Result;
use kaspa_wallet_core::runtime::{PrvKeyDataCreateArgs, WalletCreateArgs};
use kaspa_wallet_core::storage::{AccessContextT, AccountKind, Hint};

pub(crate) async fn create(ctx: &Arc<KaspaCli>, name: Option<&str>) -> Result<()> {
pub(crate) async fn create(ctx: &Arc<KaspaCli>, name: Option<&str>, import_with_mnemonic: bool) -> Result<()> {
let term = ctx.term();
let wallet = ctx.wallet();

Expand Down Expand Up @@ -37,14 +37,14 @@ pub(crate) async fn create(ctx: &Arc<KaspaCli>, name: Option<&str>) -> Result<()
tpara!(
ctx,
"\n\
\"Phishing hint\" is a secret word or a phrase that is displayed \
when you open your wallet. If you do not see the hint when opening \
your wallet, you may be accessing a fake wallet designed to steal \
your private key. If this occurs, stop using the wallet immediately, \
check the browser URL domain name and seek help on social networks \
(Kaspa Discord or Telegram). \
\n\
",
\"Phishing hint\" is a secret word or a phrase that is displayed \
when you open your wallet. If you do not see the hint when opening \
your wallet, you may be accessing a fake wallet designed to steal \
your private key. If this occurs, stop using the wallet immediately, \
check the browser URL domain name and seek help on social networks \
(Kaspa Discord or Telegram). \
\n\
",
);

let hint = term.ask(false, "Create phishing hint (optional, press <enter> to skip): ").await?.trim().to_string();
Expand All @@ -62,38 +62,65 @@ pub(crate) async fn create(ctx: &Arc<KaspaCli>, name: Option<&str>) -> Result<()
}

tprintln!(ctx, "");
tpara!(
ctx,
"\
PLEASE NOTE: The optional payment password, if provided, will be required to \
issue transactions. This password will also be required when recovering your wallet \
in addition to your private key or mnemonic. If you loose this password, you will not \
be able to use mnemonic to recover your wallet! \
",
);
if import_with_mnemonic {
tpara!(
ctx,
"\
\
If your original wallet has a bip39 recovery passphrase, please enter it now.\
\
Specifically, this is not a wallet password. This is a secondary mnemonic passphrase\
used to encrypt your mnemonic. This is known as a 'payment passphrase'\
'mnemonic passphrase', or a 'recovery passphrase'. If your mnemonic was created\
with a payment passphrase and you do not enter it now, the import process\
will generate a different private key.\
\
If you do not have a bip39 recovery passphrase, press ENTER.\
\
",
);
} else {
tpara!(
ctx,
"\
PLEASE NOTE: The optional bip39 mnemonic passphrase, if provided, will be required to \
issue transactions. This passphrase will also be required when recovering your wallet \
in addition to your private key or mnemonic. If you loose this passphrase, you will not \
be able to use or recover your wallet! \
\
If you do not want to use bip39 recovery passphrase, press ENTER.\
",
);
}

let payment_secret = term.ask(true, "Enter payment password (optional): ").await?;
let payment_secret = term.ask(true, "Enter bip39 mnemonic passphrase (optional): ").await?;
let payment_secret =
if payment_secret.trim().is_empty() { None } else { Some(Secret::new(payment_secret.trim().as_bytes().to_vec())) };

if let Some(payment_secret) = payment_secret.as_ref() {
let payment_secret_validate = Secret::new(
term.ask(true, "Enter payment (private key encryption) password (optional): ").await?.trim().as_bytes().to_vec(),
);
let payment_secret_validate =
Secret::new(term.ask(true, "Please re-enter mnemonic passphrase: ").await?.trim().as_bytes().to_vec());
if payment_secret_validate.as_ref() != payment_secret.as_ref() {
return Err(Error::PaymentSecretMatch);
}
}

tprintln!(ctx, "");

let prv_key_data_args = if import_with_mnemonic {
let words = crate::wizards::import::prompt_for_mnemonic(&term).await?;
PrvKeyDataCreateArgs::new_with_mnemonic(None, wallet_secret.clone(), payment_secret.clone(), words.join(" "))
} else {
PrvKeyDataCreateArgs::new(None, wallet_secret.clone(), payment_secret.clone())
};

let notifier = ctx.notifier().show(Notification::Processing).await;

// suspend commits for multiple operations
wallet.store().batch().await?;

let account_kind = AccountKind::Bip32;
let wallet_args = WalletCreateArgs::new(name.map(String::from), None, hint, wallet_secret.clone(), true);
let prv_key_data_args = PrvKeyDataCreateArgs::new(None, wallet_secret.clone(), payment_secret.clone());
let account_args = AccountCreateArgs::new(account_name, account_title, account_kind, wallet_secret.clone(), payment_secret);
let descriptor = ctx.wallet().create_wallet(wallet_args).await?;
let (prv_key_data_id, mnemonic) = wallet.create_prv_key_data(prv_key_data_args).await?;
Expand All @@ -102,31 +129,34 @@ pub(crate) async fn create(ctx: &Arc<KaspaCli>, name: Option<&str>) -> Result<()
// flush data to storage
let access_ctx: Arc<dyn AccessContextT> = Arc::new(AccessContext::new(wallet_secret.clone()));
wallet.store().flush(&access_ctx).await?;
notifier.hide();

tprintln!(ctx, "");
tprintln!(ctx, "---");
tprintln!(ctx, "");
tprintln!(ctx, "{}", style("IMPORTANT:").red());
tprintln!(ctx, "");
notifier.hide();

tpara!(
ctx,
"Your mnemonic phrase allows your to re-create your private key. \
The person who has access to this mnemonic will have full control of \
the Kaspa stored in it. Keep your mnemonic safe. Write it down and \
store it in a safe, preferably in a fire-resistant location. Do not \
store your mnemonic on this computer or a mobile device. This wallet \
will never ask you for this mnemonic phrase unless you manually \
initiate a private key recovery. \
",
);
if !import_with_mnemonic {
tprintln!(ctx, "");
tprintln!(ctx, "---");
tprintln!(ctx, "");
tprintln!(ctx, "{}", style("IMPORTANT:").red());
tprintln!(ctx, "");

tpara!(
ctx,
"Your mnemonic phrase allows your to re-create your private key. \
The person who has access to this mnemonic will have full control of \
the Kaspa stored in it. Keep your mnemonic safe. Write it down and \
store it in a safe, preferably in a fire-resistant location. Do not \
store your mnemonic on this computer or a mobile device. This wallet \
will never ask you for this mnemonic phrase unless you manually \
initiate a private key recovery. \
",
);

// descriptor
// descriptor

["", "Never share your mnemonic with anyone!", "---", "", "Your default wallet account mnemonic:", mnemonic.phrase()]
.into_iter()
.for_each(|line| term.writeln(line));
["", "Never share your mnemonic with anyone!", "---", "", "Your default wallet account mnemonic:", mnemonic.phrase()]
.into_iter()
.for_each(|line| term.writeln(line));
}

term.writeln("");
if let Some(descriptor) = descriptor {
Expand Down
Loading

0 comments on commit 314f453

Please sign in to comment.