feat: mnemonic phrase support for wallet (#256)
* feat: mnemonic phrase support for wallet * refactor: better error handling and clippy linting * fix: derive from path and tests * chore: renamed package coins-bip39 * refactor: convenient builder API to setup mnemonic wallet * refactor: re-export coins-bip39 for convenience * clippy: fix warnings for multiple complex types in provider * feat: randomly generated mnemonic phrase can be written to storage
This commit is contained in:
parent
32b4e9e3f5
commit
79862ffda5
File diff suppressed because it is too large
Load Diff
|
@ -13,6 +13,9 @@ pub use transaction::{Transaction, TransactionReceipt, TransactionRequest};
|
||||||
mod address_or_bytes;
|
mod address_or_bytes;
|
||||||
pub use address_or_bytes::AddressOrBytes;
|
pub use address_or_bytes::AddressOrBytes;
|
||||||
|
|
||||||
|
mod path_or_string;
|
||||||
|
pub use path_or_string::PathOrString;
|
||||||
|
|
||||||
mod i256;
|
mod i256;
|
||||||
pub use i256::I256;
|
pub use i256::I256;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
/// A type that can either be a `Path` or a `String`
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum PathOrString {
|
||||||
|
/// A path type
|
||||||
|
Path(PathBuf),
|
||||||
|
/// A string type
|
||||||
|
String(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PathBuf> for PathOrString {
|
||||||
|
fn from(p: PathBuf) -> Self {
|
||||||
|
PathOrString::Path(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for PathOrString {
|
||||||
|
fn from(s: &str) -> Self {
|
||||||
|
let path = Path::new(s);
|
||||||
|
if path.exists() {
|
||||||
|
PathOrString::Path(path.to_owned())
|
||||||
|
} else {
|
||||||
|
PathOrString::String(s.to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PathOrString {
|
||||||
|
/// Reads the contents at path, or simply returns the string.
|
||||||
|
pub fn read(&self) -> Result<String, std::io::Error> {
|
||||||
|
match self {
|
||||||
|
PathOrString::Path(pathbuf) => std::fs::read_to_string(pathbuf),
|
||||||
|
PathOrString::String(s) => Ok(s.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
#![deny(broken_intra_doc_links)]
|
#![deny(broken_intra_doc_links)]
|
||||||
|
#![allow(clippy::type_complexity)]
|
||||||
//! # Clients for interacting with Ethereum nodes
|
//! # Clients for interacting with Ethereum nodes
|
||||||
//!
|
//!
|
||||||
//! This crate provides asynchronous [Ethereum JSON-RPC](https://github.com/ethereum/wiki/wiki/JSON-RPC)
|
//! This crate provides asynchronous [Ethereum JSON-RPC](https://github.com/ethereum/wiki/wiki/JSON-RPC)
|
||||||
|
|
|
@ -16,7 +16,8 @@ rustdoc-args = ["--cfg", "docsrs"]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ethers-core = { version = "0.2.2", path = "../ethers-core" }
|
ethers-core = { version = "0.2.2", path = "../ethers-core" }
|
||||||
thiserror = { version = "1.0.24", default-features = false }
|
thiserror = { version = "1.0.24", default-features = false }
|
||||||
|
coins-bip32 = "0.2.2"
|
||||||
|
coins-bip39 = "0.2.2"
|
||||||
coins-ledger = { version = "0.1.0", default-features = false, optional = true }
|
coins-ledger = { version = "0.1.0", default-features = false, optional = true }
|
||||||
eth-keystore = { version = "0.2.0" }
|
eth-keystore = { version = "0.2.0" }
|
||||||
hex = { version = "0.4.3", default-features = false, features = ["std"] }
|
hex = { version = "0.4.3", default-features = false, features = ["std"] }
|
||||||
|
|
|
@ -39,7 +39,10 @@
|
||||||
//! [`Transaction`]: ethers_core::types::Transaction
|
//! [`Transaction`]: ethers_core::types::Transaction
|
||||||
//! [`TransactionRequest`]: ethers_core::types::TransactionRequest
|
//! [`TransactionRequest`]: ethers_core::types::TransactionRequest
|
||||||
mod wallet;
|
mod wallet;
|
||||||
pub use wallet::Wallet;
|
pub use wallet::{MnemonicBuilder, Wallet, WalletError};
|
||||||
|
|
||||||
|
/// Re-export the BIP-32 crate so that wordlists can be accessed conveniently.
|
||||||
|
pub use coins_bip39;
|
||||||
|
|
||||||
/// A wallet instantiated with a locally stored private key
|
/// A wallet instantiated with a locally stored private key
|
||||||
pub type LocalWallet = Wallet<ethers_core::k256::ecdsa::SigningKey>;
|
pub type LocalWallet = Wallet<ethers_core::k256::ecdsa::SigningKey>;
|
||||||
|
|
|
@ -0,0 +1,279 @@
|
||||||
|
//! Specific helper functions for creating/loading a mnemonic private key following BIP-39
|
||||||
|
//! specifications
|
||||||
|
use crate::{wallet::util::key_to_address, Wallet, WalletError};
|
||||||
|
|
||||||
|
use coins_bip32::path::DerivationPath;
|
||||||
|
use coins_bip39::{Mnemonic, Wordlist};
|
||||||
|
use ethers_core::{k256::ecdsa::SigningKey, types::PathOrString, utils::to_checksum};
|
||||||
|
use rand::Rng;
|
||||||
|
use std::{fs::File, io::Write, marker::PhantomData, path::PathBuf, str::FromStr};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
const DEFAULT_DERIVATION_PATH_PREFIX: &str = "m/44'/60'/0'/0/";
|
||||||
|
|
||||||
|
/// Represents a structure that can resolve into a `Wallet<SigningKey>`.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct MnemonicBuilder<W: Wordlist> {
|
||||||
|
/// The mnemonic phrase can be supplied to the builder as a string or a path to the file whose
|
||||||
|
/// contents are the phrase. A builder that has a valid phrase should `build` the wallet.
|
||||||
|
phrase: Option<PathOrString>,
|
||||||
|
/// The mnemonic builder can also be asked to generate a new random wallet by providing the
|
||||||
|
/// number of words in the phrase. By default this is set to 12.
|
||||||
|
word_count: usize,
|
||||||
|
/// The derivation path at which the extended private key child will be derived at. By default
|
||||||
|
/// the mnemonic builder uses the path: "m/44'/60'/0'/0/0".
|
||||||
|
derivation_path: DerivationPath,
|
||||||
|
/// Optional password for the mnemonic phrase.
|
||||||
|
password: Option<String>,
|
||||||
|
/// Optional field that if enabled, writes the mnemonic phrase to disk storage at the provided
|
||||||
|
/// path.
|
||||||
|
write_to: Option<PathBuf>,
|
||||||
|
/// PhantomData
|
||||||
|
_wordlist: PhantomData<W>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error produced by the mnemonic wallet module
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum MnemonicBuilderError {
|
||||||
|
/// Error suggests that a phrase (path or words) was expected but not found
|
||||||
|
#[error("Expected phrase not found")]
|
||||||
|
ExpectedPhraseNotFound,
|
||||||
|
/// Error suggests that a phrase (path or words) was not expected but found
|
||||||
|
#[error("Unexpected phrase found")]
|
||||||
|
UnexpectedPhraseFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: Wordlist> Default for MnemonicBuilder<W> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
phrase: None,
|
||||||
|
word_count: 12usize,
|
||||||
|
derivation_path: DerivationPath::from_str(&format!(
|
||||||
|
"{}{}",
|
||||||
|
DEFAULT_DERIVATION_PATH_PREFIX, 0
|
||||||
|
))
|
||||||
|
.expect("should parse the default derivation path"),
|
||||||
|
password: None,
|
||||||
|
write_to: None,
|
||||||
|
_wordlist: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: Wordlist> MnemonicBuilder<W> {
|
||||||
|
/// Sets the phrase in the mnemonic builder. The phrase can either be a string or a path to
|
||||||
|
/// the file that contains the phrase. Once a phrase is provided, the key will be generated
|
||||||
|
/// deterministically by calling the `build` method.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use ethers_signers::{MnemonicBuilder, coins_bip39::English};
|
||||||
|
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
///
|
||||||
|
/// let wallet = MnemonicBuilder::<English>::default()
|
||||||
|
/// .phrase("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about")
|
||||||
|
/// .build()?;
|
||||||
|
///
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn phrase<P: Into<PathOrString>>(mut self, phrase: P) -> Self {
|
||||||
|
self.phrase = Some(phrase.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the word count of a mnemonic phrase to be generated at random. If the `phrase` field
|
||||||
|
/// is set, then `word_count` will be ignored.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use ethers_signers::{MnemonicBuilder, coins_bip39::English};
|
||||||
|
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
///
|
||||||
|
/// let mut rng = rand::thread_rng();
|
||||||
|
/// let wallet = MnemonicBuilder::<English>::default()
|
||||||
|
/// .word_count(24)
|
||||||
|
/// .build_random(&mut rng)?;
|
||||||
|
///
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn word_count(mut self, count: usize) -> Self {
|
||||||
|
self.word_count = count;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the derivation path of the child key to be derived. The derivation path is calculated
|
||||||
|
/// using the default derivation path prefix used in Ethereum, i.e. "m/44'/60'/0'/0/{index}".
|
||||||
|
pub fn index<U: Into<u32>>(mut self, index: U) -> Result<Self, WalletError> {
|
||||||
|
self.derivation_path = DerivationPath::from_str(&format!(
|
||||||
|
"{}{}",
|
||||||
|
DEFAULT_DERIVATION_PATH_PREFIX,
|
||||||
|
index.into()
|
||||||
|
))?;
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the derivation path of the child key to be derived.
|
||||||
|
pub fn derivation_path(mut self, path: &str) -> Result<Self, WalletError> {
|
||||||
|
self.derivation_path = DerivationPath::from_str(path)?;
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the password used to construct the seed from the mnemonic phrase.
|
||||||
|
pub fn password(mut self, password: &str) -> Self {
|
||||||
|
self.password = Some(password.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the path to which the randomly generated phrase will be written to. This field is
|
||||||
|
/// ignored when building a wallet from the provided mnemonic phrase.
|
||||||
|
pub fn write_to<P: Into<PathBuf>>(mut self, path: P) -> Self {
|
||||||
|
self.write_to = Some(path.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a `LocalWallet` using the parameters set in mnemonic builder. This method expects
|
||||||
|
/// the phrase field to be set.
|
||||||
|
pub fn build(&self) -> Result<Wallet<SigningKey>, WalletError> {
|
||||||
|
let mnemonic = match &self.phrase {
|
||||||
|
Some(path_or_string) => {
|
||||||
|
let phrase = path_or_string.read()?;
|
||||||
|
Mnemonic::<W>::new_from_phrase(&phrase)?
|
||||||
|
}
|
||||||
|
None => return Err(MnemonicBuilderError::ExpectedPhraseNotFound.into()),
|
||||||
|
};
|
||||||
|
self.mnemonic_to_wallet(&mnemonic)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a `LocalWallet` using the parameters set in the mnemonic builder and constructing
|
||||||
|
/// the phrase using the provided random number generator.
|
||||||
|
pub fn build_random<R: Rng>(&self, rng: &mut R) -> Result<Wallet<SigningKey>, WalletError> {
|
||||||
|
let mnemonic = match &self.phrase {
|
||||||
|
None => Mnemonic::<W>::new_with_count(rng, self.word_count)?,
|
||||||
|
_ => return Err(MnemonicBuilderError::UnexpectedPhraseFound.into()),
|
||||||
|
};
|
||||||
|
let wallet = self.mnemonic_to_wallet(&mnemonic)?;
|
||||||
|
|
||||||
|
// Write the mnemonic phrase to storage if a directory has been provided.
|
||||||
|
if let Some(dir) = &self.write_to {
|
||||||
|
let mut file = File::create(dir.as_path().join(to_checksum(&wallet.address, None)))?;
|
||||||
|
file.write_all(mnemonic.to_phrase()?.as_bytes())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(wallet)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mnemonic_to_wallet(
|
||||||
|
&self,
|
||||||
|
mnemonic: &Mnemonic<W>,
|
||||||
|
) -> Result<Wallet<SigningKey>, WalletError> {
|
||||||
|
let derived_priv_key =
|
||||||
|
mnemonic.derive_key(&self.derivation_path, self.password.as_deref())?;
|
||||||
|
let key: &SigningKey = derived_priv_key.as_ref();
|
||||||
|
let signer = SigningKey::from_bytes(&key.to_bytes())?;
|
||||||
|
let address = key_to_address(&signer);
|
||||||
|
|
||||||
|
Ok(Wallet::<SigningKey> {
|
||||||
|
signer,
|
||||||
|
address,
|
||||||
|
chain_id: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use crate::coins_bip39::English;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
const TEST_DERIVATION_PATH: &str = "m/44'/60'/0'/2/1";
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn mnemonic_deterministic() {
|
||||||
|
// Testcases have been taken from MyCryptoWallet
|
||||||
|
const TESTCASES: [(&str, u32, Option<&str>, &str); 4] = [
|
||||||
|
(
|
||||||
|
"work man father plunge mystery proud hollow address reunion sauce theory bonus",
|
||||||
|
0u32,
|
||||||
|
Some("TREZOR123"),
|
||||||
|
"0x431a00DA1D54c281AeF638A73121B3D153e0b0F6",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"inject danger program federal spice bitter term garbage coyote breeze thought funny",
|
||||||
|
1u32,
|
||||||
|
Some("LEDGER321"),
|
||||||
|
"0x231a3D0a05d13FAf93078C779FeeD3752ea1350C",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"fire evolve buddy tenant talent favorite ankle stem regret myth dream fresh",
|
||||||
|
2u32,
|
||||||
|
None,
|
||||||
|
"0x1D86AD5eBb2380dAdEAF52f61f4F428C485460E9",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"thumb soda tape crunch maple fresh imitate cancel order blind denial giraffe",
|
||||||
|
3u32,
|
||||||
|
None,
|
||||||
|
"0xFB78b25f69A8e941036fEE2A5EeAf349D81D4ccc",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
TESTCASES
|
||||||
|
.iter()
|
||||||
|
.for_each(|(phrase, index, password, expected_addr)| {
|
||||||
|
let wallet = match password {
|
||||||
|
Some(psswd) => MnemonicBuilder::<English>::default()
|
||||||
|
.phrase(*phrase)
|
||||||
|
.index(*index)
|
||||||
|
.unwrap()
|
||||||
|
.password(psswd)
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
None => MnemonicBuilder::<English>::default()
|
||||||
|
.phrase(*phrase)
|
||||||
|
.index(*index)
|
||||||
|
.unwrap()
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
};
|
||||||
|
assert_eq!(&to_checksum(&wallet.address, None), expected_addr);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn mnemonic_write_read() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
|
||||||
|
// Construct a wallet from random mnemonic phrase and write it to the temp dir.
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let wallet1 = MnemonicBuilder::<English>::default()
|
||||||
|
.word_count(24)
|
||||||
|
.derivation_path(TEST_DERIVATION_PATH)
|
||||||
|
.unwrap()
|
||||||
|
.write_to(dir.as_ref())
|
||||||
|
.build_random(&mut rng)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Ensure that only one file has been created.
|
||||||
|
let paths = std::fs::read_dir(dir.as_ref()).unwrap();
|
||||||
|
assert_eq!(paths.count(), 1);
|
||||||
|
|
||||||
|
// Use the newly created file's path to instantiate wallet.
|
||||||
|
let phrase_path = dir.as_ref().join(to_checksum(&wallet1.address, None));
|
||||||
|
let wallet2 = MnemonicBuilder::<English>::default()
|
||||||
|
.phrase(phrase_path.to_str().unwrap())
|
||||||
|
.derivation_path(TEST_DERIVATION_PATH)
|
||||||
|
.unwrap()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Ensure that both wallets belong to the same address.
|
||||||
|
assert_eq!(wallet1.address, wallet2.address);
|
||||||
|
|
||||||
|
dir.close().unwrap();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,12 @@
|
||||||
mod hash;
|
mod hash;
|
||||||
|
|
||||||
|
mod mnemonic;
|
||||||
|
pub use mnemonic::{MnemonicBuilder, MnemonicBuilderError};
|
||||||
|
|
||||||
mod private_key;
|
mod private_key;
|
||||||
|
pub use private_key::WalletError;
|
||||||
|
|
||||||
|
mod util;
|
||||||
|
|
||||||
#[cfg(feature = "yubihsm")]
|
#[cfg(feature = "yubihsm")]
|
||||||
mod yubi;
|
mod yubi;
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
//! Specific helper functions for loading an offline K256 Private Key stored on disk
|
//! Specific helper functions for loading an offline K256 Private Key stored on disk
|
||||||
use super::Wallet;
|
use super::Wallet;
|
||||||
|
|
||||||
|
use crate::wallet::{mnemonic::MnemonicBuilderError, util::key_to_address};
|
||||||
|
use coins_bip32::Bip32Error;
|
||||||
|
use coins_bip39::MnemonicError;
|
||||||
use eth_keystore::KeystoreError;
|
use eth_keystore::KeystoreError;
|
||||||
use ethers_core::{
|
use ethers_core::{
|
||||||
k256::{
|
k256::ecdsa::{self, SigningKey},
|
||||||
ecdsa::SigningKey, elliptic_curve::error::Error as K256Error, EncodedPoint as K256PublicKey,
|
|
||||||
},
|
|
||||||
rand::{CryptoRng, Rng},
|
rand::{CryptoRng, Rng},
|
||||||
types::Address,
|
|
||||||
utils::keccak256,
|
|
||||||
};
|
};
|
||||||
use std::{path::Path, str::FromStr};
|
use std::{path::Path, str::FromStr};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
@ -16,9 +15,27 @@ use thiserror::Error;
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
/// Error thrown by the Wallet module
|
/// Error thrown by the Wallet module
|
||||||
pub enum WalletError {
|
pub enum WalletError {
|
||||||
|
/// Error propagated from the BIP-32 crate
|
||||||
|
#[error(transparent)]
|
||||||
|
Bip32Error(#[from] Bip32Error),
|
||||||
|
/// Error propagated from the BIP-39 crate
|
||||||
|
#[error(transparent)]
|
||||||
|
Bip39Error(#[from] MnemonicError),
|
||||||
/// Underlying eth keystore error
|
/// Underlying eth keystore error
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
EthKeystoreError(#[from] KeystoreError),
|
EthKeystoreError(#[from] KeystoreError),
|
||||||
|
/// Error propagated from k256's ECDSA module
|
||||||
|
#[error(transparent)]
|
||||||
|
EcdsaError(#[from] ecdsa::Error),
|
||||||
|
/// Error propagated from the hex crate.
|
||||||
|
#[error(transparent)]
|
||||||
|
HexError(#[from] hex::FromHexError),
|
||||||
|
/// Error propagated by IO operations
|
||||||
|
#[error(transparent)]
|
||||||
|
IoError(#[from] std::io::Error),
|
||||||
|
/// Error propagated from the mnemonic builder module.
|
||||||
|
#[error(transparent)]
|
||||||
|
MnemonicBuilderError(#[from] MnemonicBuilderError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for Wallet<SigningKey> {
|
impl Clone for Wallet<SigningKey> {
|
||||||
|
@ -33,8 +50,6 @@ impl Clone for Wallet<SigningKey> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Wallet<SigningKey> {
|
impl Wallet<SigningKey> {
|
||||||
// TODO: Add support for mnemonic
|
|
||||||
|
|
||||||
/// Creates a new random encrypted JSON with the provided password and stores it in the
|
/// Creates a new random encrypted JSON with the provided password and stores it in the
|
||||||
/// provided directory
|
/// provided directory
|
||||||
pub fn new_keystore<P, R, S>(dir: P, rng: &mut R, password: S) -> Result<Self, WalletError>
|
pub fn new_keystore<P, R, S>(dir: P, rng: &mut R, password: S) -> Result<Self, WalletError>
|
||||||
|
@ -44,8 +59,7 @@ impl Wallet<SigningKey> {
|
||||||
S: AsRef<[u8]>,
|
S: AsRef<[u8]>,
|
||||||
{
|
{
|
||||||
let (secret, _) = eth_keystore::new(dir, rng, password)?;
|
let (secret, _) = eth_keystore::new(dir, rng, password)?;
|
||||||
let signer = SigningKey::from_bytes(secret.as_slice())
|
let signer = SigningKey::from_bytes(secret.as_slice())?;
|
||||||
.expect("private key should always be convertible to signing key");
|
|
||||||
let address = key_to_address(&signer);
|
let address = key_to_address(&signer);
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
signer,
|
signer,
|
||||||
|
@ -61,8 +75,7 @@ impl Wallet<SigningKey> {
|
||||||
S: AsRef<[u8]>,
|
S: AsRef<[u8]>,
|
||||||
{
|
{
|
||||||
let secret = eth_keystore::decrypt_key(keypath, password)?;
|
let secret = eth_keystore::decrypt_key(keypath, password)?;
|
||||||
let signer = SigningKey::from_bytes(secret.as_slice())
|
let signer = SigningKey::from_bytes(secret.as_slice())?;
|
||||||
.expect("private key should always be convertible to signing key");
|
|
||||||
let address = key_to_address(&signer);
|
let address = key_to_address(&signer);
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
signer,
|
signer,
|
||||||
|
@ -83,15 +96,6 @@ impl Wallet<SigningKey> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn key_to_address(secret_key: &SigningKey) -> Address {
|
|
||||||
// TODO: Can we do this in a better way?
|
|
||||||
let uncompressed_pub_key = K256PublicKey::from(&secret_key.verify_key()).decompress();
|
|
||||||
let public_key = uncompressed_pub_key.unwrap().to_bytes();
|
|
||||||
debug_assert_eq!(public_key[0], 0x04);
|
|
||||||
let hash = keccak256(&public_key[1..]);
|
|
||||||
Address::from_slice(&hash[12..])
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for Wallet<SigningKey> {
|
impl PartialEq for Wallet<SigningKey> {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.signer.to_bytes().eq(&other.signer.to_bytes())
|
self.signer.to_bytes().eq(&other.signer.to_bytes())
|
||||||
|
@ -129,11 +133,11 @@ impl From<K256SecretKey> for Wallet<SigningKey> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for Wallet<SigningKey> {
|
impl FromStr for Wallet<SigningKey> {
|
||||||
type Err = K256Error;
|
type Err = WalletError;
|
||||||
|
|
||||||
fn from_str(src: &str) -> Result<Self, Self::Err> {
|
fn from_str(src: &str) -> Result<Self, Self::Err> {
|
||||||
let src = hex::decode(src).expect("invalid hex when reading PrivateKey");
|
let src = hex::decode(src)?;
|
||||||
let sk = SigningKey::from_bytes(&src).unwrap(); // TODO
|
let sk = SigningKey::from_bytes(&src)?;
|
||||||
Ok(sk.into())
|
Ok(sk.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -142,6 +146,7 @@ impl FromStr for Wallet<SigningKey> {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::Signer;
|
use crate::Signer;
|
||||||
|
use ethers_core::types::Address;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
use ethers_core::{
|
||||||
|
k256::{ecdsa::SigningKey, EncodedPoint as K256PublicKey},
|
||||||
|
types::Address,
|
||||||
|
utils::keccak256,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn key_to_address(secret_key: &SigningKey) -> Address {
|
||||||
|
// TODO: Can we do this in a better way?
|
||||||
|
let uncompressed_pub_key = K256PublicKey::from(&secret_key.verify_key()).decompress();
|
||||||
|
let public_key = uncompressed_pub_key.unwrap().to_bytes();
|
||||||
|
debug_assert_eq!(public_key[0], 0x04);
|
||||||
|
let hash = keccak256(&public_key[1..]);
|
||||||
|
Address::from_slice(&hash[12..])
|
||||||
|
}
|
Loading…
Reference in New Issue