2020-10-02 08:41:16 +00:00
|
|
|
//! Specific helper functions for loading an offline K256 Private Key stored on disk
|
|
|
|
use super::Wallet;
|
|
|
|
|
2021-04-05 08:24:06 +00:00
|
|
|
use crate::wallet::mnemonic::MnemonicBuilderError;
|
2021-04-05 07:44:58 +00:00
|
|
|
use coins_bip32::Bip32Error;
|
|
|
|
use coins_bip39::MnemonicError;
|
2021-08-23 09:56:44 +00:00
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
2021-07-07 17:18:14 +00:00
|
|
|
use elliptic_curve::rand_core;
|
2021-08-23 09:56:44 +00:00
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
2021-01-11 09:39:14 +00:00
|
|
|
use eth_keystore::KeystoreError;
|
2020-10-02 08:41:16 +00:00
|
|
|
use ethers_core::{
|
2021-04-05 07:44:58 +00:00
|
|
|
k256::ecdsa::{self, SigningKey},
|
2020-10-02 08:41:16 +00:00
|
|
|
rand::{CryptoRng, Rng},
|
2021-04-05 08:24:06 +00:00
|
|
|
utils::secret_key_to_address,
|
2020-10-02 08:41:16 +00:00
|
|
|
};
|
2021-08-23 09:56:44 +00:00
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
use std::path::Path;
|
|
|
|
use std::str::FromStr;
|
2021-01-11 09:39:14 +00:00
|
|
|
use thiserror::Error;
|
|
|
|
|
|
|
|
#[derive(Error, Debug)]
|
|
|
|
/// Error thrown by the Wallet module
|
|
|
|
pub enum WalletError {
|
2021-04-05 07:44:58 +00:00
|
|
|
/// Error propagated from the BIP-32 crate
|
|
|
|
#[error(transparent)]
|
|
|
|
Bip32Error(#[from] Bip32Error),
|
|
|
|
/// Error propagated from the BIP-39 crate
|
|
|
|
#[error(transparent)]
|
|
|
|
Bip39Error(#[from] MnemonicError),
|
2021-01-11 09:39:14 +00:00
|
|
|
/// Underlying eth keystore error
|
2021-08-23 09:56:44 +00:00
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
2021-01-11 09:39:14 +00:00
|
|
|
#[error(transparent)]
|
|
|
|
EthKeystoreError(#[from] KeystoreError),
|
2021-04-05 07:44:58 +00:00
|
|
|
/// 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),
|
derive-eip712: initial implementation of eip712 derive macro (#481)
* derive-eip712: initial implementation of eip712 derive macro
This commit provides an initial implementation for a derive macro
to encode typed data according to EIP-712, https://eips.ethereum.org/EIPS/eip-712
Additionally, this commit introduces a new signer trait method:
async fn sign_typed_data<T: Eip712 + Send + Sync>(
&self,
payload: &T,
) -> Result<Signature, Self::Error>;
And implements the new method for each of the signers (wallet, ledger,
aws).
Additionally, these changes include using `WalletError` for the Wallet
signer error type
At the moment, derive does not recurse the primary type to find nested
Eip712 structs. This is something that is noted in the source and
currently responds with an error regarding custom types.
A subsequent PR should be opened once this issue becomes needed. For the
moment, the current implementation should satisfy non-nested, basic struct types.
* rename to ethers-derive-eip712; move to ethers-core
* refactor of derive-eip712 macro; use ParamType and EthAbiToken
* macro updates; add byte array checker for paramtype; use literal constant for domain type hash
* replace std::convert::Infallible with WalletError as Wallet signer error type
* update workspace members and dev dependencies for examples folder
* add example for eip712 and test against contract
* remove extraneous backward slash in '\x19\x01' prefix; example tests pass
* update unreleased change log
* remove print statements
* use parse_macro_input macro; remove dead code; handle nest struct not implemented error
* move eip712 example to solidity-contract tests folder; update cargo workspace dependencies
* allow optional EIP712Domain parameter when encoding eip712 struct and signing typed data
* add documentation for eip712 feature
* Update ethers-signers/src/ledger/mod.rs
Co-authored-by: Sebastian Martinez <me@sebastinez.dev>
* add error enum for Eip712Error; use sign_payload for ledger signer
* add EIP712WithDomain type for providing a wrapper around custom setting of the domain
* make LedgerWallet sign_payload public
* use optional feature gated dependencies for eip712; add default method for encode_eip712
* add default domain_separator method, pre-compute separator hash
* move derive-eip712 deps to dev deps
* remove invalid sign payload parameter, add await on async method
* remove deprecated comment
* debugging 'bad key handle' error for ledger signer
try using 'sign_message'
* await sign digest for aws signer
* remove extra space, fix fmt warning
* fix test, fmt errors
* use gt 0.6.0 pragma compiler version
* enable ABIEncoderV2 for solidity test contract
* chore: make test constructor public
Co-authored-by: Sebastian Martinez <me@sebastinez.dev>
Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
2021-10-08 15:22:51 +00:00
|
|
|
/// Error type from Eip712Error message
|
|
|
|
#[error("error encoding eip712 struct: {0:?}")]
|
|
|
|
Eip712Error(String),
|
2021-01-11 09:39:14 +00:00
|
|
|
}
|
2020-10-02 08:41:16 +00:00
|
|
|
|
|
|
|
impl Clone for Wallet<SigningKey> {
|
|
|
|
fn clone(&self) -> Self {
|
|
|
|
Self {
|
|
|
|
// TODO: Can we have a better way to clone here?
|
2020-12-16 12:05:16 +00:00
|
|
|
signer: SigningKey::from_bytes(&*self.signer.to_bytes()).unwrap(),
|
2020-10-02 08:41:16 +00:00
|
|
|
address: self.address,
|
|
|
|
chain_id: self.chain_id,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Wallet<SigningKey> {
|
2021-01-11 09:39:14 +00:00
|
|
|
/// Creates a new random encrypted JSON with the provided password and stores it in the
|
|
|
|
/// provided directory
|
2021-08-23 09:56:44 +00:00
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
2021-01-11 09:39:14 +00:00
|
|
|
pub fn new_keystore<P, R, S>(dir: P, rng: &mut R, password: S) -> Result<Self, WalletError>
|
|
|
|
where
|
|
|
|
P: AsRef<Path>,
|
2021-07-07 17:18:14 +00:00
|
|
|
R: Rng + CryptoRng + rand_core::CryptoRng,
|
2021-01-11 09:39:14 +00:00
|
|
|
S: AsRef<[u8]>,
|
|
|
|
{
|
|
|
|
let (secret, _) = eth_keystore::new(dir, rng, password)?;
|
2021-04-05 07:44:58 +00:00
|
|
|
let signer = SigningKey::from_bytes(secret.as_slice())?;
|
2021-04-05 08:24:06 +00:00
|
|
|
let address = secret_key_to_address(&signer);
|
2021-01-11 09:39:14 +00:00
|
|
|
Ok(Self {
|
|
|
|
signer,
|
|
|
|
address,
|
2021-07-29 20:22:25 +00:00
|
|
|
chain_id: 1,
|
2021-01-11 09:39:14 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Decrypts an encrypted JSON from the provided path to construct a Wallet instance
|
2021-08-23 09:56:44 +00:00
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
2021-01-11 09:39:14 +00:00
|
|
|
pub fn decrypt_keystore<P, S>(keypath: P, password: S) -> Result<Self, WalletError>
|
|
|
|
where
|
|
|
|
P: AsRef<Path>,
|
|
|
|
S: AsRef<[u8]>,
|
|
|
|
{
|
|
|
|
let secret = eth_keystore::decrypt_key(keypath, password)?;
|
2021-04-05 07:44:58 +00:00
|
|
|
let signer = SigningKey::from_bytes(secret.as_slice())?;
|
2021-04-05 08:24:06 +00:00
|
|
|
let address = secret_key_to_address(&signer);
|
2021-01-11 09:39:14 +00:00
|
|
|
Ok(Self {
|
|
|
|
signer,
|
|
|
|
address,
|
2021-07-29 20:22:25 +00:00
|
|
|
chain_id: 1,
|
2021-01-11 09:39:14 +00:00
|
|
|
})
|
|
|
|
}
|
2020-10-02 08:41:16 +00:00
|
|
|
|
|
|
|
/// Creates a new random keypair seeded with the provided RNG
|
|
|
|
pub fn new<R: Rng + CryptoRng>(rng: &mut R) -> Self {
|
|
|
|
let signer = SigningKey::random(rng);
|
2021-04-05 08:24:06 +00:00
|
|
|
let address = secret_key_to_address(&signer);
|
2020-10-02 08:41:16 +00:00
|
|
|
Self {
|
|
|
|
signer,
|
|
|
|
address,
|
2021-07-29 20:22:25 +00:00
|
|
|
chain_id: 1,
|
2020-10-02 08:41:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl PartialEq for Wallet<SigningKey> {
|
|
|
|
fn eq(&self, other: &Self) -> bool {
|
|
|
|
self.signer.to_bytes().eq(&other.signer.to_bytes())
|
|
|
|
&& self.address == other.address
|
|
|
|
&& self.chain_id == other.chain_id
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<SigningKey> for Wallet<SigningKey> {
|
|
|
|
fn from(signer: SigningKey) -> Self {
|
2021-04-05 08:24:06 +00:00
|
|
|
let address = secret_key_to_address(&signer);
|
2020-10-02 08:41:16 +00:00
|
|
|
|
|
|
|
Self {
|
|
|
|
signer,
|
|
|
|
address,
|
2021-07-29 20:22:25 +00:00
|
|
|
chain_id: 1,
|
2020-10-02 08:41:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
use ethers_core::k256::SecretKey as K256SecretKey;
|
|
|
|
|
|
|
|
impl From<K256SecretKey> for Wallet<SigningKey> {
|
|
|
|
fn from(key: K256SecretKey) -> Self {
|
2020-12-16 12:05:16 +00:00
|
|
|
let signer = SigningKey::from_bytes(&*key.to_bytes())
|
2020-10-02 08:41:16 +00:00
|
|
|
.expect("private key should always be convertible to signing key");
|
2021-04-05 08:24:06 +00:00
|
|
|
let address = secret_key_to_address(&signer);
|
2020-10-02 08:41:16 +00:00
|
|
|
|
|
|
|
Self {
|
|
|
|
signer,
|
|
|
|
address,
|
2021-07-29 20:22:25 +00:00
|
|
|
chain_id: 1,
|
2020-10-02 08:41:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl FromStr for Wallet<SigningKey> {
|
2021-04-05 07:44:58 +00:00
|
|
|
type Err = WalletError;
|
2020-10-02 08:41:16 +00:00
|
|
|
|
|
|
|
fn from_str(src: &str) -> Result<Self, Self::Err> {
|
2021-04-05 07:44:58 +00:00
|
|
|
let src = hex::decode(src)?;
|
|
|
|
let sk = SigningKey::from_bytes(&src)?;
|
2020-10-02 08:41:16 +00:00
|
|
|
Ok(sk.into())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
2021-08-23 09:56:44 +00:00
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
2020-10-02 08:41:16 +00:00
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
use crate::Signer;
|
2021-04-05 07:44:58 +00:00
|
|
|
use ethers_core::types::Address;
|
2021-01-11 09:39:14 +00:00
|
|
|
use std::fs;
|
|
|
|
use tempfile::tempdir;
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn encrypted_json_keystore() {
|
|
|
|
// create and store a random encrypted JSON keystore in this directory
|
|
|
|
let dir = tempdir().unwrap();
|
|
|
|
let mut rng = rand::thread_rng();
|
|
|
|
let key = Wallet::<SigningKey>::new_keystore(&dir, &mut rng, "randpsswd").unwrap();
|
|
|
|
|
|
|
|
// sign a message using the above key
|
|
|
|
let message = "Some data";
|
|
|
|
let signature = key.sign_message(message).await.unwrap();
|
|
|
|
|
|
|
|
// read from the encrypted JSON keystore and decrypt it, while validating that the
|
|
|
|
// signatures produced by both the keys should match
|
|
|
|
let paths = fs::read_dir(dir).unwrap();
|
|
|
|
for path in paths {
|
|
|
|
let path = path.unwrap().path();
|
|
|
|
let key2 = Wallet::<SigningKey>::decrypt_keystore(&path.clone(), "randpsswd").unwrap();
|
|
|
|
let signature2 = key2.sign_message(message).await.unwrap();
|
|
|
|
assert_eq!(signature, signature2);
|
|
|
|
assert!(std::fs::remove_file(&path).is_ok());
|
|
|
|
}
|
|
|
|
}
|
2020-10-02 08:41:16 +00:00
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn signs_msg() {
|
|
|
|
let message = "Some data";
|
|
|
|
let hash = ethers_core::utils::hash_message(message);
|
|
|
|
let key = Wallet::<SigningKey>::new(&mut rand::thread_rng());
|
|
|
|
let address = key.address;
|
|
|
|
|
|
|
|
// sign a message
|
|
|
|
let signature = key.sign_message(message).await.unwrap();
|
|
|
|
|
|
|
|
// ecrecover via the message will hash internally
|
|
|
|
let recovered = signature.recover(message).unwrap();
|
|
|
|
|
|
|
|
// if provided with a hash, it will skip hashing
|
|
|
|
let recovered2 = signature.recover(hash).unwrap();
|
|
|
|
|
|
|
|
// verifies the signature is produced by `address`
|
|
|
|
signature.verify(message, address).unwrap();
|
|
|
|
|
|
|
|
assert_eq!(recovered, address);
|
|
|
|
assert_eq!(recovered2, address);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
#[cfg(not(feature = "celo"))]
|
|
|
|
async fn signs_tx() {
|
|
|
|
use ethers_core::types::TransactionRequest;
|
|
|
|
// retrieved test vector from:
|
|
|
|
// https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction
|
|
|
|
let tx = TransactionRequest {
|
|
|
|
from: None,
|
|
|
|
to: Some(
|
|
|
|
"F0109fC8DF283027b6285cc889F5aA624EaC1F55"
|
|
|
|
.parse::<Address>()
|
|
|
|
.unwrap()
|
|
|
|
.into(),
|
|
|
|
),
|
|
|
|
value: Some(1_000_000_000.into()),
|
|
|
|
gas: Some(2_000_000.into()),
|
|
|
|
nonce: Some(0.into()),
|
|
|
|
gas_price: Some(21_000_000_000u128.into()),
|
|
|
|
data: None,
|
2021-08-09 00:31:11 +00:00
|
|
|
}
|
|
|
|
.into();
|
2020-10-02 08:41:16 +00:00
|
|
|
let chain_id = 1u64;
|
|
|
|
|
|
|
|
let wallet: Wallet<SigningKey> =
|
|
|
|
"4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318"
|
|
|
|
.parse()
|
|
|
|
.unwrap();
|
2021-07-29 20:22:25 +00:00
|
|
|
let wallet = wallet.with_chain_id(chain_id);
|
2020-10-02 08:41:16 +00:00
|
|
|
|
|
|
|
let sig = wallet.sign_transaction(&tx).await.unwrap();
|
2021-07-29 20:22:25 +00:00
|
|
|
let sighash = tx.sighash(chain_id);
|
2020-10-02 08:41:16 +00:00
|
|
|
assert!(sig.verify(sighash, wallet.address).is_ok());
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn key_to_address() {
|
|
|
|
let wallet: Wallet<SigningKey> =
|
|
|
|
"0000000000000000000000000000000000000000000000000000000000000001"
|
|
|
|
.parse()
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
|
|
wallet.address,
|
|
|
|
Address::from_str("7E5F4552091A69125d5DfCb7b8C2659029395Bdf").expect("Decoding failed")
|
|
|
|
);
|
|
|
|
|
|
|
|
let wallet: Wallet<SigningKey> =
|
|
|
|
"0000000000000000000000000000000000000000000000000000000000000002"
|
|
|
|
.parse()
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
|
|
wallet.address,
|
|
|
|
Address::from_str("2B5AD5c4795c026514f8317c7a215E218DcCD6cF").expect("Decoding failed")
|
|
|
|
);
|
|
|
|
|
|
|
|
let wallet: Wallet<SigningKey> =
|
|
|
|
"0000000000000000000000000000000000000000000000000000000000000003"
|
|
|
|
.parse()
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
|
|
wallet.address,
|
|
|
|
Address::from_str("6813Eb9362372EEF6200f3b1dbC3f819671cBA69").expect("Decoding failed")
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|