Skip to content

Commit

Permalink
"Add support for exporting MultiSig account"
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
biryukovmaxim committed Oct 6, 2023
1 parent 72890ca commit 47fa5a5
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 52 deletions.
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(())
}
6 changes: 3 additions & 3 deletions wallet/core/src/runtime/account/variants/multisig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ use crate::{imports::*, AddressDerivationManagerTrait};

pub struct MultiSig {
inner: Arc<Inner>,
xpub_keys: Arc<Vec<String>>,
pub xpub_keys: Arc<Vec<String>>,
cosigner_index: Option<u8>,
minimum_signatures: u16,
pub minimum_signatures: u16,
ecdsa: bool,
derivation: Arc<AddressDerivationManager>,
prv_key_data_ids: Option<Arc<Vec<PrvKeyDataId>>>,
pub prv_key_data_ids: Option<Arc<Vec<PrvKeyDataId>>>,
}

impl MultiSig {
Expand Down

0 comments on commit 47fa5a5

Please sign in to comment.