From 1a47e933ae422abce3ebd799565c214ee4b6425a Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Wed, 17 Jun 2020 09:38:04 +0300 Subject: [PATCH] feat(signers): implement Serde and make Wallet API smaller (#20) * feat(signers): implement Serde and make API smaller * fix: add abigen as a dev-dependency feature --- Cargo.lock | 9 +- ethers-contract/Cargo.toml | 5 +- .../ethers-contract-abigen/Cargo.toml | 2 +- .../src/contract/common.rs | 2 + ethers-contract/src/call.rs | 1 + ethers-contract/src/event.rs | 1 + ethers-core/Cargo.toml | 2 +- ethers-core/src/types/crypto/keys.rs | 107 +++++++++++++++++- ethers-providers/Cargo.toml | 2 +- ethers-providers/src/lib.rs | 2 +- ethers-signers/Cargo.toml | 3 +- ethers-signers/src/lib.rs | 2 +- ethers-signers/src/wallet.rs | 32 ++++-- ethers/Cargo.toml | 13 ++- ethers/examples/sign.rs | 4 +- 15 files changed, 154 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 60e65146..efa16188 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -377,6 +377,7 @@ dependencies = [ "ethers-core", "ethers-providers", "futures-util", + "serde", "thiserror", "tokio", ] @@ -1163,18 +1164,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.111" +version = "1.0.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9124df5b40cbd380080b2cc6ab894c040a3070d995f5c9dc77e18c34a8ae37d" +checksum = "736aac72d1eafe8e5962d1d1c3d99b0df526015ba40915cb3c49d042e92ec243" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.111" +version = "1.0.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f2c3ac8e6ca1e9c80b8be1023940162bf81ae3cffbb1809474152f2ce1eb250" +checksum = "bf0343ce212ac0d3d6afd9391ac8e9c9efe06b533c8d33f660f6390cc4093f57" dependencies = [ "proc-macro2", "quote", diff --git a/ethers-contract/Cargo.toml b/ethers-contract/Cargo.toml index 9f7e50b1..eb23e286 100644 --- a/ethers-contract/Cargo.toml +++ b/ethers-contract/Cargo.toml @@ -14,8 +14,8 @@ ethers-core = { version = "0.1.0", path = "../ethers-core" } serde = { version = "1.0.110", default-features = false } rustc-hex = { version = "2.1.0", default-features = false } -thiserror = { version = "1.0.19", default-features = false } -once_cell = { version = "1.4.0", default-features = false } +thiserror = { version = "1.0.15", default-features = false } +once_cell = { version = "1.3.1", default-features = false } futures = "0.3.5" [dev-dependencies] @@ -23,5 +23,4 @@ tokio = { version = "0.2.21", default-features = false, features = ["macros"] } serde_json = "1.0.55" [features] -default = ["abigen"] abigen = ["ethers-contract-abigen", "ethers-contract-derive"] diff --git a/ethers-contract/ethers-contract-abigen/Cargo.toml b/ethers-contract/ethers-contract-abigen/Cargo.toml index 00f39f03..00626a25 100644 --- a/ethers-contract/ethers-contract-abigen/Cargo.toml +++ b/ethers-contract/ethers-contract-abigen/Cargo.toml @@ -17,5 +17,5 @@ quote = "1.0" syn = "1.0.12" url = "2.1" serde_json = "1.0.53" -once_cell = "1.4.0" +once_cell = "1.3.1" rustc-hex = { version = "2.1.0", default-features = false } diff --git a/ethers-contract/ethers-contract-abigen/src/contract/common.rs b/ethers-contract/ethers-contract-abigen/src/contract/common.rs index 4eea841b..4b2f1677 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/common.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/common.rs @@ -6,6 +6,8 @@ use quote::quote; pub(crate) fn imports() -> TokenStream { quote! { + #![allow(dead_code)] + #![allow(unused_imports)] // TODO: Can we make this context aware so that it imports either ethers_contract // or ethers::contract? use ethers::{ diff --git a/ethers-contract/src/call.rs b/ethers-contract/src/call.rs index c76c6194..17cde911 100644 --- a/ethers-contract/src/call.rs +++ b/ethers-contract/src/call.rs @@ -40,6 +40,7 @@ pub enum ContractError { } #[derive(Debug, Clone)] +#[must_use = "contract calls do nothing unless you `send` or `call` them"] /// Helper for managing a transaction before submitting it to a node pub struct ContractCall<'a, P, S, D> { /// The raw transaction object diff --git a/ethers-contract/src/event.rs b/ethers-contract/src/event.rs index 053a474f..c8641fc4 100644 --- a/ethers-contract/src/event.rs +++ b/ethers-contract/src/event.rs @@ -11,6 +11,7 @@ use futures::stream::{Stream, StreamExt}; use std::{collections::HashMap, marker::PhantomData}; /// Helper for managing the event filter before querying or streaming its logs +#[must_use = "event filters do nothing unless you `query` or `stream` them"] pub struct Event<'a: 'b, 'b, P, D> { /// The event filter's state pub filter: Filter, diff --git a/ethers-core/Cargo.toml b/ethers-core/Cargo.toml index 08b11b26..f03e935d 100644 --- a/ethers-core/Cargo.toml +++ b/ethers-core/Cargo.toml @@ -20,7 +20,7 @@ tiny-keccak = { version = "2.0.2", default-features = false } serde = { version = "1.0.110", default-features = false, features = ["derive"] } serde_json = { version = "1.0.53", default-features = false, features = ["alloc"] } rustc-hex = { version = "2.1.0", default-features = false } -thiserror = { version = "1.0.19", default-features = false } +thiserror = { version = "1.0.15", default-features = false } arrayvec = { version = "0.5.1", default-features = false, optional = true } glob = "0.3.0" diff --git a/ethers-core/src/types/crypto/keys.rs b/ethers-core/src/types/crypto/keys.rs index d69131cd..370163e7 100644 --- a/ethers-core/src/types/crypto/keys.rs +++ b/ethers-core/src/types/crypto/keys.rs @@ -6,16 +6,48 @@ use crate::{ use rand::Rng; use rustc_hex::FromHex; use secp256k1::{ - self as Secp256k1, Error as SecpError, Message, PublicKey as PubKey, RecoveryId, SecretKey, + self as Secp256k1, + util::{COMPRESSED_PUBLIC_KEY_SIZE, SECRET_KEY_SIZE}, + Error as SecpError, Message, PublicKey as PubKey, RecoveryId, SecretKey, }; -use std::ops::Deref; -use std::str::FromStr; +use serde::{ + de::Error as DeserializeError, + de::{SeqAccess, Visitor}, + ser::SerializeTuple, + Deserialize, Deserializer, Serialize, Serializer, +}; +use std::{fmt, ops::Deref, str::FromStr}; use thiserror::Error; /// A private key on Secp256k1 #[derive(Clone, Debug, PartialEq, Eq)] pub struct PrivateKey(pub(super) SecretKey); +impl Serialize for PrivateKey { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut seq = serializer.serialize_tuple(SECRET_KEY_SIZE)?; + for e in &self.0.serialize() { + seq.serialize_element(e)?; + } + seq.end() + } +} + +impl<'de> Deserialize<'de> for PrivateKey { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let bytes = <[u8; SECRET_KEY_SIZE]>::deserialize(deserializer)?; + Ok(PrivateKey( + SecretKey::parse(&bytes).map_err(DeserializeError::custom)?, + )) + } +} + impl FromStr for PrivateKey { type Err = SecpError; @@ -133,7 +165,6 @@ impl PrivateKey { let r = H256::from_slice(&signature.r.b32()); let s = H256::from_slice(&signature.s.b32()); - // TODO: Check what happens when using the 1337 Geth chain id Signature { v: v as u8, r, s } } } @@ -214,14 +245,80 @@ impl From for Address { } } +impl Serialize for PublicKey { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut seq = serializer.serialize_tuple(COMPRESSED_PUBLIC_KEY_SIZE)?; + for e in self.0.serialize_compressed().iter() { + seq.serialize_element(e)?; + } + seq.end() + } +} + +impl<'de> Deserialize<'de> for PublicKey { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct ArrayVisitor; + + impl<'de> Visitor<'de> for ArrayVisitor { + type Value = PublicKey; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a valid proof") + } + + fn visit_seq(self, mut seq: S) -> Result + where + S: SeqAccess<'de>, + { + let mut bytes = [0u8; COMPRESSED_PUBLIC_KEY_SIZE]; + for b in &mut bytes[..] { + *b = seq + .next_element()? + .ok_or_else(|| DeserializeError::custom("could not read bytes"))?; + } + + Ok(PublicKey( + PubKey::parse_compressed(&bytes).map_err(DeserializeError::custom)?, + )) + } + } + + deserializer.deserialize_tuple(COMPRESSED_PUBLIC_KEY_SIZE, ArrayVisitor) + } +} + #[cfg(test)] mod tests { use super::*; - use crate::types::Bytes; use rustc_hex::FromHex; + #[test] + fn serde() { + for _ in 0..10 { + let key = PrivateKey::new(&mut rand::thread_rng()); + let serialized = bincode::serialize(&key).unwrap(); + assert_eq!(serialized, &key.0.serialize()); + let de: PrivateKey = bincode::deserialize(&serialized).unwrap(); + assert_eq!(key, de); + + let public = PublicKey::from(&key); + let serialized = bincode::serialize(&public).unwrap(); + assert_eq!(&serialized[..], public.0.serialize_compressed().as_ref()); + let de: PublicKey = bincode::deserialize(&serialized).unwrap(); + assert_eq!(public, de); + } + } + #[test] fn signs_tx() { + use crate::types::{Address, Bytes}; + // retrieved test vector from: // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction let tx = TransactionRequest { diff --git a/ethers-providers/Cargo.toml b/ethers-providers/Cargo.toml index 40c61fa3..e65ad4aa 100644 --- a/ethers-providers/Cargo.toml +++ b/ethers-providers/Cargo.toml @@ -11,7 +11,7 @@ async-trait = { version = "0.1.31", default-features = false } reqwest = { version = "0.10.4", default-features = false, features = ["json", "rustls-tls"] } serde = { version = "1.0.110", default-features = false, features = ["derive"] } serde_json = { version = "1.0.53", default-features = false } -thiserror = { version = "1.0.19", default-features = false } +thiserror = { version = "1.0.15", default-features = false } url = { version = "2.1.1", default-features = false } # required for implementing stream on the filters diff --git a/ethers-providers/src/lib.rs b/ethers-providers/src/lib.rs index 2f9a7c56..be9d9452 100644 --- a/ethers-providers/src/lib.rs +++ b/ethers-providers/src/lib.rs @@ -24,7 +24,7 @@ pub use provider::{Provider, ProviderError}; #[async_trait] /// Trait which must be implemented by data transports to be used with the Ethereum /// JSON-RPC provider. -pub trait JsonRpcClient: Debug + Clone { +pub trait JsonRpcClient: Debug + Clone + Send + Sync { /// A JSON-RPC Error type Error: Error + Into; diff --git a/ethers-signers/Cargo.toml b/ethers-signers/Cargo.toml index f7af48f5..1b44e020 100644 --- a/ethers-signers/Cargo.toml +++ b/ethers-signers/Cargo.toml @@ -7,8 +7,9 @@ edition = "2018" [dependencies] ethers-core = { version = "0.1.0", path = "../ethers-core" } ethers-providers = { version = "0.1.0", path = "../ethers-providers" } -thiserror = { version = "1.0.19", default-features = false } +thiserror = { version = "1.0.15", default-features = false } futures-util = { version = "0.3.5", default-features = false } +serde = "1.0.112" [dev-dependencies] tokio = { version = "0.2.21", features = ["macros"] } diff --git a/ethers-signers/src/lib.rs b/ethers-signers/src/lib.rs index 77b6b5a3..141e6d12 100644 --- a/ethers-signers/src/lib.rs +++ b/ethers-signers/src/lib.rs @@ -12,7 +12,7 @@ use std::error::Error; /// /// Implement this trait to support different signing modes, e.g. Ledger, hosted etc. // TODO: We might need a `SignerAsync` trait for HSM use cases? -pub trait Signer: Clone { +pub trait Signer: Clone + Send + Sync { type Error: Error + Into; /// Signs the hash of the provided message after prefixing it fn sign_message>(&self, message: S) -> Signature; diff --git a/ethers-signers/src/wallet.rs b/ethers-signers/src/wallet.rs index 077c747e..a76aeaad 100644 --- a/ethers-signers/src/wallet.rs +++ b/ethers-signers/src/wallet.rs @@ -8,6 +8,7 @@ use ethers_core::{ types::{Address, PrivateKey, PublicKey, Signature, Transaction, TransactionRequest, TxError}, }; +use serde::{Deserialize, Serialize}; use std::str::FromStr; /// An Ethereum private-public key pair which can be used for signing messages. It can be connected to a provider @@ -29,7 +30,7 @@ use std::str::FromStr; /// /// // Optionally, the wallet's chain id can be set, in order to use EIP-155 /// // replay protection with different chains -/// let wallet = wallet.chain_id(1337u64); +/// let wallet = wallet.set_chain_id(1337u64); /// /// // The wallet can be used to sign messages /// let message = b"hello"; @@ -62,16 +63,16 @@ use std::str::FromStr; /// [`connect`]: ./struct.Wallet.html#method.connect /// [`Signature`]: ../ethers_core/types/struct.Signature.html /// [`hash_message`]: ../ethers_core/utils/fn.hash_message.html -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Wallet { /// The Wallet's private Key - pub private_key: PrivateKey, + private_key: PrivateKey, /// The Wallet's public Key - pub public_key: PublicKey, + public_key: PublicKey, /// The wallet's address - pub address: Address, + address: Address, /// The wallet's chain id (for EIP-155), signs w/o replay protection if left unset - pub chain_id: u64, + chain_id: u64, } impl Signer for Wallet { @@ -123,11 +124,26 @@ impl Wallet { } } - /// Sets the wallet's chain_id - pub fn chain_id>(mut self, chain_id: T) -> Self { + /// Sets the wallet's chain_id, used in conjunction with EIP-155 signing + pub fn set_chain_id>(mut self, chain_id: T) -> Self { self.chain_id = chain_id.into(); self } + + /// Gets the wallet's public key + pub fn public_key(&self) -> &PublicKey { + &self.public_key + } + + /// Gets the wallet's private key + pub fn private_key(&self) -> &PrivateKey { + &self.private_key + } + + /// Gets the wallet's chain id + pub fn chain_id(&self) -> u64 { + self.chain_id + } } impl From for Wallet { diff --git a/ethers/Cargo.toml b/ethers/Cargo.toml index 566b1fb3..d2df300c 100644 --- a/ethers/Cargo.toml +++ b/ethers/Cargo.toml @@ -19,6 +19,7 @@ rustdoc-args = ["--cfg", "docsrs"] features = ["full"] [features] +abigen = ["contract", "ethers-contract/abigen"] default = ["full"] full = [ "contract", @@ -27,20 +28,22 @@ full = [ "core", ] +core = ["ethers-core"] contract = ["ethers-contract"] providers = ["ethers-providers"] signers = ["ethers-signers"] -core = ["ethers-core"] [dependencies] -ethers-contract = { version = "0.1.0", path = "../ethers-contract", features = ["abigen"], optional = true } +ethers-contract = { version = "0.1.0", path = "../ethers-contract", optional = true } +ethers-core = { version = "0.1.0", path = "../ethers-core", optional = true } ethers-providers = { version = "0.1.0", path = "../ethers-providers", optional = true } ethers-signers = { version = "0.1.0", path = "../ethers-signers", optional = true } -ethers-core = { version = "0.1.0", path = "../ethers-core", optional = true } [dev-dependencies] +ethers-contract = { version = "0.1.0", path = "../ethers-contract", features = ["abigen"] } + anyhow = "1.0.31" -tokio = { version = "0.2.21", features = ["macros"] } -serde_json = "1.0.53" rand = "0.7" serde = { version = "1.0.110", features = ["derive"] } +serde_json = "1.0.53" +tokio = { version = "0.2.21", features = ["macros"] } diff --git a/ethers/examples/sign.rs b/ethers/examples/sign.rs index 5897bf70..953b8a1b 100644 --- a/ethers/examples/sign.rs +++ b/ethers/examples/sign.rs @@ -11,7 +11,7 @@ fn main() { // recover the address that signed it let recovered = signature.recover(message).unwrap(); - assert_eq!(recovered, wallet.address); + assert_eq!(recovered, wallet.address()); - println!("Verified signature produced by {:?}!", wallet.address); + println!("Verified signature produced by {:?}!", wallet.address()); }