Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 5 additions & 12 deletions src/reports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,47 +54,40 @@ impl ErrorReport {
if !self.details.is_empty() {
let _ = writeln!(stderr, " details:");
for (key, value) in &self.details {
let _ = writeln!(stderr, " ∙ {}: {}", key, value);
let _ = writeln!(stderr, " ∙ {key}: {value}");
}
}

// Print help message if available
if let Some(help) = &self.help {
let _ = writeln!(stderr, "💡 {}", help);
let _ = writeln!(stderr, "💡 {help}");
}

if !self.logs.is_empty() {
let _ = writeln!(stderr, " logs:");
for log in &self.logs {
let _ = writeln!(stderr, " ‣ {}", log);
let _ = writeln!(stderr, " ‣ {log}");
}
}

let _ = writeln!(stderr);
}

/// Print the error report to stdout with JSON formatting
pub fn print_json(&self) {
let json = serde_json::to_string_pretty(self)
.unwrap_or_else(|_| format!("{{\"error\": \"Failed to serialize error report\"}}"));
println!("{}", json);
}
}

impl std::fmt::Display for ErrorReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Error: {} (Type: {})", self.message, self.kind)?;

if let Some(code) = self.code {
write!(f, " [Code: {}]", code)?;
write!(f, " [Code: {code}]")?;
}

if !self.details.is_empty() {
write!(f, " - Details: {:?}", self.details)?;
}

if let Some(help) = &self.help {
write!(f, " - Help: {}", help)?;
write!(f, " - Help: {help}")?;
}

Ok(())
Expand Down
2 changes: 1 addition & 1 deletion src/tx/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ pub fn define_args(
let mut remaining_params = params.clone();

let mut loaded_args =
super::common::load_args(inline_args, file_args.as_deref(), &remaining_params)?;
super::common::load_args(inline_args, file_args, &remaining_params)?;

// remove from the remaining params the args we already managed to load from the
// file or json
Expand Down
4 changes: 2 additions & 2 deletions src/tx/invoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ pub async fn run(args: Args, ctx: &crate::Context) -> Result<()> {
args.tx3_args_json.as_deref(),
args.tx3_args_file.as_deref(),
ctx,
&provider,
provider,
)?;

let TxEnvelope { tx, hash } = super::common::resolve_tx(&prototx, tx_args, &provider).await?;
let TxEnvelope { tx, hash } = super::common::resolve_tx(&prototx, tx_args, provider).await?;

let cbor = hex::decode(tx).unwrap();

Expand Down
4 changes: 2 additions & 2 deletions src/tx/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ pub async fn run(args: Args, ctx: &crate::Context) -> Result<()> {
args.tx3_args_json.as_deref(),
args.tx3_args_file.as_deref(),
ctx,
&provider,
provider,
)?;

let TxEnvelope { tx, hash } = super::common::resolve_tx(&prototx, tx_args, &provider).await?;
let TxEnvelope { tx, hash } = super::common::resolve_tx(&prototx, tx_args, provider).await?;

let cbor = hex::decode(tx).unwrap();

Expand Down
1 change: 1 addition & 0 deletions src/wallet/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ pub async fn run(args: Args, ctx: &mut crate::Context) -> Result<()> {
public_key: wallet.public_key.clone(),
is_default: new_is_default,
is_unsafe: wallet.is_unsafe,
stake_public_key: wallet.stake_public_key.clone(),
};

ctx.store.remove_wallet(wallet.clone())?;
Expand Down
3 changes: 2 additions & 1 deletion src/wallet/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub struct Args {
is_default: Option<bool>,
}

#[instrument(skip_all, name = "edit")]
#[instrument(skip_all, name = "import")]
pub async fn run(args: Args, ctx: &mut crate::Context) -> Result<()> {
let name = match args.name {
Some(name) => Name::try_from(name)?,
Expand Down Expand Up @@ -67,6 +67,7 @@ pub async fn run(args: Args, ctx: &mut crate::Context) -> Result<()> {
name,
modified: Local::now(),
public_key: public_key.as_ref().to_vec(),
stake_public_key: None,
is_default: new_is_default,
is_unsafe: false,
};
Expand Down
119 changes: 91 additions & 28 deletions src/wallet/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,44 @@ const VERSION_SIZE: usize = 1;
const SALT_SIZE: usize = 16;
const NONCE_SIZE: usize = 12;
const TAG_SIZE: usize = 16;
const HARDENED_KEY_START: u32 = 2147483648;

pub type NewWallet = (String, Wallet);

pub struct DerivationIndexes(Vec<u32>);
impl Default for DerivationIndexes {
fn default() -> Self {
Self(vec![
HARDENED_KEY_START + 1852, // purpose
HARDENED_KEY_START + 1815, // coin type
HARDENED_KEY_START, // account
0, // payment
0, // key index
])
}
}

impl DerivationIndexes {
fn stake() -> Self {
Self(vec![
HARDENED_KEY_START + 1852, // purpose
HARDENED_KEY_START + 1815, // coin type
HARDENED_KEY_START, // account
2, // stake
0, // key index
])
}
}

impl IntoIterator for DerivationIndexes {
type Item = u32;
type IntoIter = std::vec::IntoIter<u32>;

fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}

#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct Wallet {
pub name: Name,
Expand All @@ -44,6 +79,9 @@ pub struct Wallet {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[serde(with = "utils::option_hex_vec_u8")]
pub private_key: Option<Vec<u8>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[serde(with = "utils::option_hex_vec_u8")]
pub stake_public_key: Option<Vec<u8>>,
pub created: DateTime<Local>,
pub modified: DateTime<Local>,
pub is_default: bool,
Expand All @@ -58,22 +96,36 @@ impl Wallet {
is_default: bool,
is_unsafe: bool,
) -> Result<NewWallet> {
let (private_key, mnemonic) =
Bip32PrivateKey::generate_with_mnemonic(OsRng, password.to_string());
let public_key = private_key.to_public().as_bytes();
let (root, mnemonic) = Bip32PrivateKey::generate_with_mnemonic(OsRng);

let mut payment = root.clone();
let mut stake = root.clone();

let private_key = private_key.to_ed25519_private_key();
for idx in DerivationIndexes::default() {
payment = payment.derive(idx);
}

for idx in DerivationIndexes::stake() {
stake = stake.derive(idx);
}

let public_key = payment.to_public().as_bytes();

let private_key = payment.to_ed25519_private_key();
let private_key = match is_unsafe {
true => private_key.as_bytes(),
false => encrypt_private_key(OsRng, private_key, &password.to_string()),
};

let stake_public_key = Some(stake.to_public().as_bytes());

Ok((
mnemonic.to_string(),
Self {
name: Name::try_from(name)?,
private_key: Some(private_key),
public_key,
stake_public_key,
created: Local::now(),
modified: Local::now(),
is_default,
Expand All @@ -89,11 +141,20 @@ impl Wallet {
is_default: bool,
is_unsafe: bool,
) -> Result<Self> {
let private_key =
Bip32PrivateKey::from_bip39_mnenomic(mnemonic.to_string(), password.to_string())?;
let public_key = private_key.to_public().as_bytes();
let mut payment = Bip32PrivateKey::from_bip39_mnenomic(mnemonic.to_string())?;
let mut stake = Bip32PrivateKey::from_bip39_mnenomic(mnemonic.to_string())?;

let private_key = private_key.to_ed25519_private_key();
for idx in DerivationIndexes::default() {
payment = payment.derive(idx);
}

for idx in DerivationIndexes::stake() {
stake = stake.derive(idx);
}
let public_key = payment.to_public().as_bytes();
let stake_public_key = stake.to_public().as_bytes();

let private_key = payment.to_ed25519_private_key();
let private_key = match is_unsafe {
true => private_key.as_bytes(),
false => encrypt_private_key(OsRng, private_key, &password.to_string()),
Expand All @@ -103,6 +164,7 @@ impl Wallet {
name: Name::try_from(name)?,
private_key: Some(private_key),
public_key,
stake_public_key: Some(stake_public_key),
created: Local::now(),
modified: Local::now(),
is_default,
Expand All @@ -116,19 +178,27 @@ impl Wallet {
.to_ed25519_pubkey(),
None => PublicKey::from_str(&hex::encode(&self.public_key)).unwrap(),
};
let delegation_part = match self.stake_public_key.as_ref() {
Some(pk) => ShelleyDelegationPart::key_hash(
Bip32PublicKey::from_bytes(pk.clone().try_into().unwrap())
.to_ed25519_pubkey()
.compute_hash(),
),
None => ShelleyDelegationPart::Null,
};

if is_testnet {
ShelleyAddress::new(
Network::Testnet,
ShelleyPaymentPart::key_hash(pk.compute_hash()),
ShelleyDelegationPart::Null,
delegation_part,
)
.into()
} else {
ShelleyAddress::new(
Network::Mainnet,
ShelleyPaymentPart::key_hash(pk.compute_hash()),
ShelleyDelegationPart::Null,
delegation_part,
)
.into()
}
Expand Down Expand Up @@ -159,11 +229,8 @@ impl Wallet {
.map(|x| x.clone().to_vec())
.unwrap_or_default();

let public_key = Bip32PublicKey::from_bytes(self.public_key.clone().try_into().unwrap())
.to_ed25519_pubkey();

vkey_witnesses.push(VKeyWitness {
vkey: public_key.as_ref().to_vec().into(),
vkey: private_key.public_key().as_ref().to_vec().into(),
signature: signature.as_ref().to_vec().into(),
});

Expand Down Expand Up @@ -370,7 +437,7 @@ impl TryFrom<&[u8]> for PrivateKey {
}

/// Ed25519-BIP32 HD Private Key
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Bip32PrivateKey(ed25519_bip32::XPrv);
impl Bip32PrivateKey {
const BECH32_HRP: &'static str = "xprv";
Expand All @@ -383,10 +450,7 @@ impl Bip32PrivateKey {
Self(xprv)
}

pub fn generate_with_mnemonic<T: RngCore + CryptoRng>(
mut rng: T,
password: String,
) -> (Self, Mnemonic) {
pub fn generate_with_mnemonic<T: RngCore + CryptoRng>(mut rng: T) -> (Self, Mnemonic) {
let mut buf = [0u8; 64];
rng.fill_bytes(&mut buf);

Expand All @@ -396,9 +460,9 @@ impl Bip32PrivateKey {

let mut pbkdf2_result = [0; XPRV_SIZE];

const ITER: u32 = 4096; // TODO: BIP39 says 2048, CML uses 4096?
const ITER: u32 = 4096;

let mut mac = Hmac::new(Sha512::new(), password.as_bytes());
let mut mac = Hmac::new(Sha512::new(), &[]);
pbkdf2(&mut mac, &entropy, ITER, &mut pbkdf2_result);

(Self(XPrv::normalize_bytes_force3rd(pbkdf2_result)), bip39)
Expand All @@ -414,15 +478,15 @@ impl Bip32PrivateKey {
self.0.as_ref().to_vec()
}

pub fn from_bip39_mnenomic(mnemonic: String, password: String) -> Result<Self> {
let bip39 = Mnemonic::parse(mnemonic).context("Error parsing mnemonic")?;
pub fn from_bip39_mnenomic(mnemonic: String) -> Result<Self> {
let bip39 = Mnemonic::parse(&mnemonic).context("Error parsing mnemonic")?;
let entropy = bip39.to_entropy();

let mut pbkdf2_result = [0; XPRV_SIZE];

const ITER: u32 = 4096; // TODO: BIP39 says 2048, CML uses 4096?
const ITER: u32 = 4096;

let mut mac = Hmac::new(Sha512::new(), password.as_bytes());
let mut mac = Hmac::new(Sha512::new(), &[]);
pbkdf2(&mut mac, &entropy, ITER, &mut pbkdf2_result);

Ok(Self(XPrv::normalize_bytes_force3rd(pbkdf2_result)))
Expand Down Expand Up @@ -648,10 +712,9 @@ mod tests {

#[test]
fn mnemonic_roundtrip() {
let (xprv, mne) = Bip32PrivateKey::generate_with_mnemonic(OsRng, "".into());
let (xprv, mne) = Bip32PrivateKey::generate_with_mnemonic(OsRng);

let xprv_from_mne =
Bip32PrivateKey::from_bip39_mnenomic(mne.to_string(), "".into()).unwrap();
let xprv_from_mne = Bip32PrivateKey::from_bip39_mnenomic(mne.to_string()).unwrap();

assert_eq!(xprv, xprv_from_mne)
}
Expand Down