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
This commit is contained in:
Georgios Konstantopoulos 2020-06-17 09:38:04 +03:00 committed by GitHub
parent 570b45eb10
commit 1a47e933ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 154 additions and 33 deletions

9
Cargo.lock generated
View File

@ -377,6 +377,7 @@ dependencies = [
"ethers-core", "ethers-core",
"ethers-providers", "ethers-providers",
"futures-util", "futures-util",
"serde",
"thiserror", "thiserror",
"tokio", "tokio",
] ]
@ -1163,18 +1164,18 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.111" version = "1.0.112"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9124df5b40cbd380080b2cc6ab894c040a3070d995f5c9dc77e18c34a8ae37d" checksum = "736aac72d1eafe8e5962d1d1c3d99b0df526015ba40915cb3c49d042e92ec243"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.111" version = "1.0.112"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f2c3ac8e6ca1e9c80b8be1023940162bf81ae3cffbb1809474152f2ce1eb250" checksum = "bf0343ce212ac0d3d6afd9391ac8e9c9efe06b533c8d33f660f6390cc4093f57"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@ -14,8 +14,8 @@ ethers-core = { version = "0.1.0", path = "../ethers-core" }
serde = { version = "1.0.110", default-features = false } serde = { version = "1.0.110", default-features = false }
rustc-hex = { version = "2.1.0", default-features = false } 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 }
once_cell = { version = "1.4.0", default-features = false } once_cell = { version = "1.3.1", default-features = false }
futures = "0.3.5" futures = "0.3.5"
[dev-dependencies] [dev-dependencies]
@ -23,5 +23,4 @@ tokio = { version = "0.2.21", default-features = false, features = ["macros"] }
serde_json = "1.0.55" serde_json = "1.0.55"
[features] [features]
default = ["abigen"]
abigen = ["ethers-contract-abigen", "ethers-contract-derive"] abigen = ["ethers-contract-abigen", "ethers-contract-derive"]

View File

@ -17,5 +17,5 @@ quote = "1.0"
syn = "1.0.12" syn = "1.0.12"
url = "2.1" url = "2.1"
serde_json = "1.0.53" serde_json = "1.0.53"
once_cell = "1.4.0" once_cell = "1.3.1"
rustc-hex = { version = "2.1.0", default-features = false } rustc-hex = { version = "2.1.0", default-features = false }

View File

@ -6,6 +6,8 @@ use quote::quote;
pub(crate) fn imports() -> TokenStream { pub(crate) fn imports() -> TokenStream {
quote! { quote! {
#![allow(dead_code)]
#![allow(unused_imports)]
// TODO: Can we make this context aware so that it imports either ethers_contract // TODO: Can we make this context aware so that it imports either ethers_contract
// or ethers::contract? // or ethers::contract?
use ethers::{ use ethers::{

View File

@ -40,6 +40,7 @@ pub enum ContractError {
} }
#[derive(Debug, Clone)] #[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 /// Helper for managing a transaction before submitting it to a node
pub struct ContractCall<'a, P, S, D> { pub struct ContractCall<'a, P, S, D> {
/// The raw transaction object /// The raw transaction object

View File

@ -11,6 +11,7 @@ use futures::stream::{Stream, StreamExt};
use std::{collections::HashMap, marker::PhantomData}; use std::{collections::HashMap, marker::PhantomData};
/// Helper for managing the event filter before querying or streaming its logs /// 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> { pub struct Event<'a: 'b, 'b, P, D> {
/// The event filter's state /// The event filter's state
pub filter: Filter, pub filter: Filter,

View File

@ -20,7 +20,7 @@ tiny-keccak = { version = "2.0.2", default-features = false }
serde = { version = "1.0.110", default-features = false, features = ["derive"] } serde = { version = "1.0.110", default-features = false, features = ["derive"] }
serde_json = { version = "1.0.53", default-features = false, features = ["alloc"] } serde_json = { version = "1.0.53", default-features = false, features = ["alloc"] }
rustc-hex = { version = "2.1.0", default-features = false } 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 } arrayvec = { version = "0.5.1", default-features = false, optional = true }
glob = "0.3.0" glob = "0.3.0"

View File

@ -6,16 +6,48 @@ use crate::{
use rand::Rng; use rand::Rng;
use rustc_hex::FromHex; use rustc_hex::FromHex;
use secp256k1::{ 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 serde::{
use std::str::FromStr; de::Error as DeserializeError,
de::{SeqAccess, Visitor},
ser::SerializeTuple,
Deserialize, Deserializer, Serialize, Serializer,
};
use std::{fmt, ops::Deref, str::FromStr};
use thiserror::Error; use thiserror::Error;
/// A private key on Secp256k1 /// A private key on Secp256k1
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct PrivateKey(pub(super) SecretKey); pub struct PrivateKey(pub(super) SecretKey);
impl Serialize for PrivateKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<D>(deserializer: D) -> Result<Self, D::Error>
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 { impl FromStr for PrivateKey {
type Err = SecpError; type Err = SecpError;
@ -133,7 +165,6 @@ impl PrivateKey {
let r = H256::from_slice(&signature.r.b32()); let r = H256::from_slice(&signature.r.b32());
let s = H256::from_slice(&signature.s.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 } Signature { v: v as u8, r, s }
} }
} }
@ -214,14 +245,80 @@ impl From<PrivateKey> for Address {
} }
} }
impl Serialize for PublicKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<D>(deserializer: D) -> Result<Self, D::Error>
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<S>(self, mut seq: S) -> Result<PublicKey, S::Error>
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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::types::Bytes;
use rustc_hex::FromHex; 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] #[test]
fn signs_tx() { fn signs_tx() {
use crate::types::{Address, Bytes};
// retrieved test vector from: // retrieved test vector from:
// https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction
let tx = TransactionRequest { let tx = TransactionRequest {

View File

@ -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"] } reqwest = { version = "0.10.4", default-features = false, features = ["json", "rustls-tls"] }
serde = { version = "1.0.110", default-features = false, features = ["derive"] } serde = { version = "1.0.110", default-features = false, features = ["derive"] }
serde_json = { version = "1.0.53", default-features = false } 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 } url = { version = "2.1.1", default-features = false }
# required for implementing stream on the filters # required for implementing stream on the filters

View File

@ -24,7 +24,7 @@ pub use provider::{Provider, ProviderError};
#[async_trait] #[async_trait]
/// Trait which must be implemented by data transports to be used with the Ethereum /// Trait which must be implemented by data transports to be used with the Ethereum
/// JSON-RPC provider. /// JSON-RPC provider.
pub trait JsonRpcClient: Debug + Clone { pub trait JsonRpcClient: Debug + Clone + Send + Sync {
/// A JSON-RPC Error /// A JSON-RPC Error
type Error: Error + Into<ProviderError>; type Error: Error + Into<ProviderError>;

View File

@ -7,8 +7,9 @@ edition = "2018"
[dependencies] [dependencies]
ethers-core = { version = "0.1.0", path = "../ethers-core" } ethers-core = { version = "0.1.0", path = "../ethers-core" }
ethers-providers = { version = "0.1.0", path = "../ethers-providers" } 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 } futures-util = { version = "0.3.5", default-features = false }
serde = "1.0.112"
[dev-dependencies] [dev-dependencies]
tokio = { version = "0.2.21", features = ["macros"] } tokio = { version = "0.2.21", features = ["macros"] }

View File

@ -12,7 +12,7 @@ use std::error::Error;
/// ///
/// Implement this trait to support different signing modes, e.g. Ledger, hosted etc. /// Implement this trait to support different signing modes, e.g. Ledger, hosted etc.
// TODO: We might need a `SignerAsync` trait for HSM use cases? // 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<ClientError>; type Error: Error + Into<ClientError>;
/// Signs the hash of the provided message after prefixing it /// Signs the hash of the provided message after prefixing it
fn sign_message<S: AsRef<[u8]>>(&self, message: S) -> Signature; fn sign_message<S: AsRef<[u8]>>(&self, message: S) -> Signature;

View File

@ -8,6 +8,7 @@ use ethers_core::{
types::{Address, PrivateKey, PublicKey, Signature, Transaction, TransactionRequest, TxError}, types::{Address, PrivateKey, PublicKey, Signature, Transaction, TransactionRequest, TxError},
}; };
use serde::{Deserialize, Serialize};
use std::str::FromStr; use std::str::FromStr;
/// An Ethereum private-public key pair which can be used for signing messages. It can be connected to a provider /// 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 /// // Optionally, the wallet's chain id can be set, in order to use EIP-155
/// // replay protection with different chains /// // 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 /// // The wallet can be used to sign messages
/// let message = b"hello"; /// let message = b"hello";
@ -62,16 +63,16 @@ use std::str::FromStr;
/// [`connect`]: ./struct.Wallet.html#method.connect /// [`connect`]: ./struct.Wallet.html#method.connect
/// [`Signature`]: ../ethers_core/types/struct.Signature.html /// [`Signature`]: ../ethers_core/types/struct.Signature.html
/// [`hash_message`]: ../ethers_core/utils/fn.hash_message.html /// [`hash_message`]: ../ethers_core/utils/fn.hash_message.html
#[derive(Clone, Debug)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Wallet { pub struct Wallet {
/// The Wallet's private Key /// The Wallet's private Key
pub private_key: PrivateKey, private_key: PrivateKey,
/// The Wallet's public Key /// The Wallet's public Key
pub public_key: PublicKey, public_key: PublicKey,
/// The wallet's address /// 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 /// 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 { impl Signer for Wallet {
@ -123,11 +124,26 @@ impl Wallet {
} }
} }
/// Sets the wallet's chain_id /// Sets the wallet's chain_id, used in conjunction with EIP-155 signing
pub fn chain_id<T: Into<u64>>(mut self, chain_id: T) -> Self { pub fn set_chain_id<T: Into<u64>>(mut self, chain_id: T) -> Self {
self.chain_id = chain_id.into(); self.chain_id = chain_id.into();
self 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<PrivateKey> for Wallet { impl From<PrivateKey> for Wallet {

View File

@ -19,6 +19,7 @@ rustdoc-args = ["--cfg", "docsrs"]
features = ["full"] features = ["full"]
[features] [features]
abigen = ["contract", "ethers-contract/abigen"]
default = ["full"] default = ["full"]
full = [ full = [
"contract", "contract",
@ -27,20 +28,22 @@ full = [
"core", "core",
] ]
core = ["ethers-core"]
contract = ["ethers-contract"] contract = ["ethers-contract"]
providers = ["ethers-providers"] providers = ["ethers-providers"]
signers = ["ethers-signers"] signers = ["ethers-signers"]
core = ["ethers-core"]
[dependencies] [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-providers = { version = "0.1.0", path = "../ethers-providers", optional = true }
ethers-signers = { version = "0.1.0", path = "../ethers-signers", 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] [dev-dependencies]
ethers-contract = { version = "0.1.0", path = "../ethers-contract", features = ["abigen"] }
anyhow = "1.0.31" anyhow = "1.0.31"
tokio = { version = "0.2.21", features = ["macros"] }
serde_json = "1.0.53"
rand = "0.7" rand = "0.7"
serde = { version = "1.0.110", features = ["derive"] } serde = { version = "1.0.110", features = ["derive"] }
serde_json = "1.0.53"
tokio = { version = "0.2.21", features = ["macros"] }

View File

@ -11,7 +11,7 @@ fn main() {
// recover the address that signed it // recover the address that signed it
let recovered = signature.recover(message).unwrap(); 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());
} }