Skip to content

Commit

Permalink
Merge branch 'master' into grpc-server-enhancements
Browse files Browse the repository at this point in the history
  • Loading branch information
tiram88 committed Oct 28, 2023
2 parents 164160a + 83f840a commit 17b6dd6
Show file tree
Hide file tree
Showing 23 changed files with 657 additions and 155 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions cli/src/modules/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,9 @@ impl Account {
};

let prv_key_data_info = ctx.select_private_key().await?;
let prv_key_data_id = prv_key_data_info.id;

let account_name = account_name.as_deref();
wizards::account::create(&ctx, prv_key_data_id, account_kind, account_name).await?;
wizards::account::create(&ctx, prv_key_data_info, account_kind, account_name).await?;
}
"scan" => {
let extent = if argv.is_empty() {
Expand Down Expand Up @@ -100,7 +99,7 @@ impl Account {
async fn display_help(self: Arc<Self>, ctx: Arc<KaspaCli>, _argv: Vec<String>) -> Result<()> {
ctx.term().help(
&[
("create [<type>] [<name>]", "Create a new account (types: 'bip32' (default), 'legacy')"),
("create [<type>] [<name>]", "Create a new account (types: 'bip32' (default), 'legacy', 'multisig')"),
// ("import", "Import a private key using 24 or 12 word mnemonic"),
("name <name>", "Name or rename the selected account (use 'remove' to remove the name"),
("scan [<derivations>]", "Scan extended address derivation chain (legacy accounts)"),
Expand Down
149 changes: 100 additions & 49 deletions cli/src/modules/export.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::imports::*;
use kaspa_wallet_core::runtime::{Account, MultiSig};

#[derive(Default, Handler)]
#[help("Export transactions, a wallet or a private key")]
Expand All @@ -17,60 +18,110 @@ impl Export {
match what.as_str() {
"mnemonic" => {
let account = ctx.account().await?;
let prv_key_data_id = account.prv_key_data_id()?;
if matches!(account.account_kind(), AccountKind::MultiSig) {
let account = account.downcast_arc::<MultiSig>()?;
export_multisig_account(ctx, account).await
} else {
export_single_key_account(ctx, account).await
}
}
_ => Err(format!("Invalid argument: {}", what).into()),
}
}
}

async fn export_multisig_account(ctx: Arc<KaspaCli>, account: Arc<MultiSig>) -> Result<()> {
match &account.prv_key_data_ids {
None => Err(Error::KeyDataNotFound),
Some(v) if v.is_empty() => Err(Error::KeyDataNotFound),
Some(prv_key_data_ids) => {
let wallet_secret = Secret::new(ctx.term().ask(true, "Enter wallet password: ").await?.trim().as_bytes().to_vec());
if wallet_secret.as_ref().is_empty() {
return Err(Error::WalletSecretRequired);
}

tprintln!(ctx, "required signatures: {}", account.minimum_signatures);
tprintln!(ctx, "");

let access_ctx: Arc<dyn AccessContextT> = Arc::new(AccessContext::new(wallet_secret));
let prv_key_data_store = ctx.store().as_prv_key_data_store()?;
let mut generated_xpub_keys = Vec::with_capacity(prv_key_data_ids.len());
for (id, prv_key_data_id) in prv_key_data_ids.iter().enumerate() {
let prv_key_data = prv_key_data_store.load_key_data(&access_ctx, prv_key_data_id).await?.unwrap();
let mnemonic = prv_key_data.as_mnemonic(None).unwrap().unwrap();

tprintln!(ctx, "mnemonic {}:", id + 1);
tprintln!(ctx, "");
tprintln!(ctx, "{}", mnemonic.phrase());
tprintln!(ctx, "");

let wallet_secret = Secret::new(ctx.term().ask(true, "Enter wallet password: ").await?.trim().as_bytes().to_vec());
if wallet_secret.as_ref().is_empty() {
return Err(Error::WalletSecretRequired);
let xpub_key = prv_key_data.create_xpub(None, AccountKind::MultiSig, 0).await?; // todo it can be done concurrently
let xpub_prefix = kaspa_bip32::Prefix::XPUB;
generated_xpub_keys.push(xpub_key.to_string(Some(xpub_prefix)));
}

let additional = account.xpub_keys.iter().filter(|xpub| !generated_xpub_keys.contains(xpub));
additional.enumerate().for_each(|(idx, xpub)| {
if idx == 0 {
tprintln!(ctx, "additional xpubs: ");
}
tprintln!(ctx, "{xpub}");
});
Ok(())
}
}
}

async fn export_single_key_account(ctx: Arc<KaspaCli>, account: Arc<dyn Account>) -> Result<()> {
let prv_key_data_id = account.prv_key_data_id()?;

let wallet_secret = Secret::new(ctx.term().ask(true, "Enter wallet password: ").await?.trim().as_bytes().to_vec());
if wallet_secret.as_ref().is_empty() {
return Err(Error::WalletSecretRequired);
}

let access_ctx: Arc<dyn AccessContextT> = Arc::new(AccessContext::new(wallet_secret));
let prv_key_data = ctx.store().as_prv_key_data_store()?.load_key_data(&access_ctx, prv_key_data_id).await?;
if let Some(keydata) = prv_key_data {
let payment_secret = if keydata.payload.is_encrypted() {
let payment_secret =
Secret::new(ctx.term().ask(true, "Enter payment password: ").await?.trim().as_bytes().to_vec());
if payment_secret.as_ref().is_empty() {
return Err(Error::PaymentSecretRequired);
} else {
Some(payment_secret)
}
} else {
None
};

let prv_key_data = keydata.payload.decrypt(payment_secret.as_ref())?;
let mnemonic = prv_key_data.as_ref().as_mnemonic()?;
if let Some(mnemonic) = mnemonic {
if payment_secret.is_none() {
tprintln!(ctx, "mnemonic:");
tprintln!(ctx, "");
tprintln!(ctx, "{}", mnemonic.phrase());
tprintln!(ctx, "");
} else {
tpara!(
ctx,
"\
let access_ctx: Arc<dyn AccessContextT> = Arc::new(AccessContext::new(wallet_secret));
let prv_key_data = ctx.store().as_prv_key_data_store()?.load_key_data(&access_ctx, prv_key_data_id).await?;
let Some(keydata) = prv_key_data else { return Err(Error::KeyDataNotFound) };
let payment_secret = if keydata.payload.is_encrypted() {
let payment_secret = Secret::new(ctx.term().ask(true, "Enter payment password: ").await?.trim().as_bytes().to_vec());
if payment_secret.as_ref().is_empty() {
return Err(Error::PaymentSecretRequired);
} else {
Some(payment_secret)
}
} else {
None
};

let prv_key_data = keydata.payload.decrypt(payment_secret.as_ref())?;
let mnemonic = prv_key_data.as_ref().as_mnemonic()?;

match mnemonic {
None => {
tprintln!(ctx, "mnemonic is not available for this private key");
}
Some(mnemonic) if payment_secret.is_none() => {
tprintln!(ctx, "mnemonic:");
tprintln!(ctx, "");
tprintln!(ctx, "{}", mnemonic.phrase());
tprintln!(ctx, "");
}
Some(mnemonic) => {
tpara!(
ctx,
"\
IMPORTANT: to recover your private key using this mnemonic in the future \
you will need your payment password. Your payment password is permanently associated with \
this mnemonic.",
);
tprintln!(ctx, "");
tprintln!(ctx, "mnemonic:");
tprintln!(ctx, "");
tprintln!(ctx, "{}", mnemonic.phrase());
tprintln!(ctx, "");
}
} else {
tprintln!(ctx, "mnemonic is not available for this private key");
}

Ok(())
} else {
Err(Error::KeyDataNotFound)
}
}
_ => Err(format!("Invalid argument: {}", what).into()),
);
tprintln!(ctx, "");
tprintln!(ctx, "mnemonic:");
tprintln!(ctx, "");
tprintln!(ctx, "{}", mnemonic.phrase());
tprintln!(ctx, "");
}
}
};

Ok(())
}
14 changes: 12 additions & 2 deletions cli/src/modules/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ impl Import {
let what = argv.get(0).unwrap();
match what.as_str() {
"mnemonic" => {
crate::wizards::import::import_with_mnemonic(&ctx).await?;
let account_kind =
if let Some(account_kind) = argv.get(1) { account_kind.parse::<AccountKind>()? } else { AccountKind::Bip32 };
if argv.len() > 1 {
crate::wizards::import::import_with_mnemonic(&ctx, account_kind, &argv[2..]).await?;
} else {
crate::wizards::import::import_with_mnemonic(&ctx, account_kind, &[]).await?;
}
}
"legacy" => {
if exists_legacy_v0_keydata().await? {
Expand All @@ -32,6 +38,7 @@ impl Import {
return Err("KDX/kaspanet keydata file not found".into());
}
}
// todo "read-only" => {}
// "core" => {}
v => {
tprintln!(ctx, "unknown command: '{v}'\r\n");
Expand All @@ -45,7 +52,10 @@ impl Import {
async fn display_help(self: Arc<Self>, ctx: Arc<KaspaCli>) -> Result<()> {
ctx.term().help(
&[
("mnemonic", "Import a 24 or 12 word mnemonic"),
(
"mnemonic [<type>] [<additional xpub keys>] ",
"Import a 24 or 12 word mnemonic (types: 'bip32' (default), 'legacy', 'multisig'), ",
),
("legacy", "Import a legacy (local KDX) wallet"),
// ("purge", "Purge an account from the wallet"),
],
Expand Down
81 changes: 57 additions & 24 deletions cli/src/wizards/account.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
use crate::cli::KaspaCli;
use crate::imports::*;
use crate::result::Result;
use kaspa_wallet_core::runtime::wallet::MultisigCreateArgs;
use kaspa_wallet_core::runtime::PrvKeyDataCreateArgs;
use kaspa_wallet_core::storage::AccountKind;

pub(crate) async fn create(
ctx: &Arc<KaspaCli>,
prv_key_data_id: PrvKeyDataId,
prv_key_data_info: Arc<PrvKeyDataInfo>,
account_kind: AccountKind,
name: Option<&str>,
) -> Result<()> {
let term = ctx.term();
let wallet = ctx.wallet();

if matches!(account_kind, AccountKind::MultiSig) {
return Err(Error::Custom(
"MultiSig accounts are not currently supported (will be available in the future version)".to_string(),
));
}

let (title, name) = if let Some(name) = name {
(Some(name.to_string()), Some(name.to_string()))
} else {
Expand All @@ -26,32 +22,69 @@ pub(crate) async fn create(
(Some(title), Some(name))
};

if matches!(account_kind, AccountKind::MultiSig) {
return create_multisig(ctx, title, name).await;
}

let wallet_secret = Secret::new(term.ask(true, "Enter wallet password: ").await?.trim().as_bytes().to_vec());
if wallet_secret.as_ref().is_empty() {
return Err(Error::WalletSecretRequired);
}

let prv_key_info = wallet.store().as_prv_key_data_store()?.load_key_info(&prv_key_data_id).await?;
if let Some(keyinfo) = prv_key_info {
let payment_secret = if keyinfo.is_encrypted() {
let payment_secret = Secret::new(term.ask(true, "Enter payment password: ").await?.trim().as_bytes().to_vec());
if payment_secret.as_ref().is_empty() {
return Err(Error::PaymentSecretRequired);
} else {
Some(payment_secret)
}
let payment_secret = if prv_key_data_info.is_encrypted() {
let payment_secret = Secret::new(term.ask(true, "Enter payment password: ").await?.trim().as_bytes().to_vec());
if payment_secret.as_ref().is_empty() {
return Err(Error::PaymentSecretRequired);
} else {
None
};
Some(payment_secret)
}
} else {
None
};

let account_args = AccountCreateArgs::new(name, title, account_kind, wallet_secret, payment_secret);
let account = wallet.create_bip32_account(prv_key_data_id, account_args).await?;
let account_args = AccountCreateArgs::new(name, title, account_kind, wallet_secret, payment_secret);
let account = wallet.create_bip32_account(prv_key_data_info.id, account_args).await?;

tprintln!(ctx, "\naccount created: {}\n", account.get_list_string()?);
wallet.select(Some(&account)).await?;
} else {
return Err(Error::KeyDataNotFound);
tprintln!(ctx, "\naccount created: {}\n", account.get_list_string()?);
wallet.select(Some(&account)).await?;
Ok(())
}

async fn create_multisig(ctx: &Arc<KaspaCli>, title: Option<String>, name: Option<String>) -> Result<()> {
let term = ctx.term();
let wallet = ctx.wallet();
let (wallet_secret, _) = ctx.ask_wallet_secret(None).await?;
let n_required: u16 = term.ask(false, "Enter the minimum number of signatures required: ").await?.parse()?;

let prv_keys_len: usize = term.ask(false, "Enter the number of private keys to generate: ").await?.parse()?;

let mut prv_key_data_ids = Vec::with_capacity(prv_keys_len);
let mut mnemonics = Vec::with_capacity(prv_keys_len);
for _ in 0..prv_keys_len {
let prv_key_data_args = PrvKeyDataCreateArgs::new(None, wallet_secret.clone(), None); // can be optimized with Rc<WalletSecret>
let (prv_key_data_id, mnemonic) = wallet.create_prv_key_data(prv_key_data_args).await?;

prv_key_data_ids.push(prv_key_data_id);
mnemonics.push(mnemonic);
}

let additional_xpub_keys_len: usize = term.ask(false, "Enter the number of additional extended public keys: ").await?.parse()?;
let mut xpub_keys = Vec::with_capacity(additional_xpub_keys_len + prv_keys_len);
for i in 1..=additional_xpub_keys_len {
let xpub_key = term.ask(false, &format!("Enter extended public {i} key: ")).await?;
xpub_keys.push(xpub_key.trim().to_owned());
}
let account = wallet
.create_multisig_account(MultisigCreateArgs {
prv_key_data_ids,
name,
title,
wallet_secret,
additional_xpub_keys: xpub_keys,
minimum_signatures: n_required,
})
.await?;
tprintln!(ctx, "\naccount created: {}\n", account.get_list_string()?);
wallet.select(Some(&account)).await?;
Ok(())
}
Loading

0 comments on commit 17b6dd6

Please sign in to comment.