Skip to content

Commit

Permalink
Adding multisig capabilities to wallet (kaspanet#279)
Browse files Browse the repository at this point in the history
* Create MultiSig account functionality

Introduced logic for creating MultiSig accounts including multisig account arguments and logic to execute and create a multisig account, and the update of existing variables for multisig accounts. Removed initial warning which stated MultiSig accounts are not supported. Also added a new file for 'as_slice' functionality. This update provides a valuable addition to wallet functionality by allowing multiple signatures for account access, greatly enhancing security.

* "Add multisig account creation support to wallet core and CLI"

Updated wallet core and CLI to support the creation of multisig accounts. New error types were added to cater to potential failures during this operation. Added a new 'multisig' account type to the CLI. The functionality to filter accounts based on their private key data was reworked to accommodate multisig accounts. Moreover, logic to create a multisig address was incorporated into the derivation module. The use of multisig addresses also required storage updates and changes in the runtime configuration. Also, the creation wizard in the CLI was updated to include promotional messages regarding multisig addresses.

* Improve error handling in key derivation

Errors while getting the manager keys in the wallet derivation module are now properly handled. Instead of a potential panic when a `None` value was encountered, a more error-resilient approach is used. This improves error management and the overall robustness of the key derivation process.

* "Add multi-signature account import functionality to wallet"

This commit extends the import feature of the wallet by adding support for multi-signature accounts. The updated functionality allows users to import multi-signature wallets using mnemonics and an optional payment password, and adjust the number of required signatures to finalize transactions.

* Remove payment secret from multi-signature accounts

The 'payment_secret' field has been removed from the MultisigCreateArgs structure and all associated logic in wallet.rs and account.rs. This change is made to simplify the account creation process as the 'payment_secret' was optional and it was causing confusion. Now the account creation relies solely on prv_key_data_ids, making the flow more straightforward and user-friendly.

* "Add support for exporting MultiSig account"

This commit adds the ability to export MultiSig accounts in the Kaspa CLI. Previously, only SingleKey accounts were supported for export.

This update adds conditions to check the kind of account (via the account_kind() method) before proceeding with the export process. For MultiSig accounts, the export_multisig_account function is invoked which retrieves the private key data IDs, asks for the wallet password and prints the required number of signatures and all associated mnemonics. If additional xpub_keys are found, they will also be printed.

This change allows users to backup their MultiSig account data as conveniently as for SingleKey accounts, improving the CLI's functionality. The MultiSig struct has been updated to expose the necessary fields for this feature.

* Refactor MultiSig creation to factory method

To improve readability and maintainability, MultiSig struct instances creation scattered in different places are now replaced with a new factory method `MultiSig::new()`. Other changes include reduction of redundant struct declaration and removed the Debug trait from the Secret struct.

---------

Co-authored-by: aspect <[email protected]>
  • Loading branch information
biryukovmaxim and aspect authored Oct 26, 2023
1 parent 11cc019 commit 83f840a
Show file tree
Hide file tree
Showing 18 changed files with 561 additions and 144 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 83f840a

Please sign in to comment.