simplify signers

This commit is contained in:
Georgios Konstantopoulos 2020-06-02 01:27:23 +03:00
parent 6181943485
commit 27ca5dd55a
No known key found for this signature in database
GPG Key ID: FA607837CD26EDBC
7 changed files with 115 additions and 52 deletions

2
Cargo.lock generated
View File

@ -372,6 +372,8 @@ version = "0.1.0"
dependencies = [ dependencies = [
"ethers-core", "ethers-core",
"ethers-providers", "ethers-providers",
"thiserror",
"tokio",
] ]
[[package]] [[package]]

View File

@ -1,5 +1,5 @@
use crate::{ use crate::{
types::{Address, NameOrAddress, Signature, Transaction, TransactionRequest, H256, U256, U64}, types::{Address, NameOrAddress, Signature, Transaction, TransactionRequest, H256, U256},
utils::{hash_message, keccak256}, utils::{hash_message, keccak256},
}; };
@ -76,12 +76,12 @@ impl PrivateKey {
/// ///
/// # Panics /// # Panics
/// ///
/// If `tx.to` is an ENS name. The caller MUST take care of naem resolution before /// If `tx.to` is an ENS name. The caller MUST take care of name resolution before
/// calling this function. /// calling this function.
pub fn sign_transaction( pub fn sign_transaction(
&self, &self,
tx: TransactionRequest, tx: TransactionRequest,
chain_id: Option<U64>, chain_id: Option<u64>,
) -> Result<Transaction, TxError> { ) -> Result<Transaction, TxError> {
// The nonce, gas and gasprice fields must already be populated // The nonce, gas and gasprice fields must already be populated
let nonce = tx.nonce.ok_or(TxError::NonceMissing)?; let nonce = tx.nonce.ok_or(TxError::NonceMissing)?;
@ -126,7 +126,7 @@ impl PrivateKey {
}) })
} }
fn sign_with_eip155(&self, message: &Message, chain_id: Option<U64>) -> Signature { fn sign_with_eip155(&self, message: &Message, chain_id: Option<u64>) -> Signature {
let (signature, recovery_id) = Secp256k1::sign(message, &self.0); let (signature, recovery_id) = Secp256k1::sign(message, &self.0);
let v = to_eip155_v(recovery_id, chain_id); let v = to_eip155_v(recovery_id, chain_id);
@ -139,11 +139,11 @@ impl PrivateKey {
} }
/// Applies [EIP155](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md) /// Applies [EIP155](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md)
fn to_eip155_v(recovery_id: RecoveryId, chain_id: Option<U64>) -> u64 { fn to_eip155_v(recovery_id: RecoveryId, chain_id: Option<u64>) -> u64 {
let standard_v = recovery_id.serialize() as u64; let standard_v = recovery_id.serialize() as u64;
if let Some(chain_id) = chain_id { if let Some(chain_id) = chain_id {
// When signing with a chain ID, add chain replay protection. // When signing with a chain ID, add chain replay protection.
standard_v + 35 + chain_id.as_u64() * 2 standard_v + 35 + chain_id * 2
} else { } else {
// Otherwise, convert to 'Electrum' notation. // Otherwise, convert to 'Electrum' notation.
standard_v + 27 standard_v + 27
@ -244,7 +244,7 @@ mod tests {
.parse() .parse()
.unwrap(); .unwrap();
let tx = key.sign_transaction(tx, Some(chain_id.into())).unwrap(); let tx = key.sign_transaction(tx, Some(chain_id)).unwrap();
assert_eq!( assert_eq!(
tx.hash, tx.hash,

View File

@ -7,3 +7,7 @@ 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 }
[dev-dependencies]
tokio = { version = "0.2.21", features = ["macros"] }

View File

@ -1,21 +1,22 @@
use crate::Signer; use crate::Signer;
use ethers_core::types::{Address, BlockNumber, NameOrAddress, TransactionRequest, TxHash}; use ethers_core::types::{Address, BlockNumber, NameOrAddress, TransactionRequest, TxHash};
use ethers_providers::{networks::Network, JsonRpcClient, Provider}; use ethers_providers::{JsonRpcClient, Provider, ProviderError};
use std::ops::Deref; use std::ops::Deref;
use thiserror::Error;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
/// A client provides an interface for signing and broadcasting locally signed transactions /// A client provides an interface for signing and broadcasting locally signed transactions
/// It Derefs to `Provider`, which allows interacting with the Ethereum JSON-RPC provider /// It Derefs to `Provider`, which allows interacting with the Ethereum JSON-RPC provider
/// via the same API. /// via the same API.
pub struct Client<'a, P, N, S> { pub struct Client<P, S> {
pub(crate) provider: &'a Provider<P, N>, pub(crate) provider: Provider<P>,
pub(crate) signer: Option<S>, pub(crate) signer: Option<S>,
} }
impl<'a, P, N, S> From<&'a Provider<P, N>> for Client<'a, P, N, S> { impl<P, S> From<Provider<P>> for Client<P, S> {
fn from(provider: &'a Provider<P, N>) -> Self { fn from(provider: Provider<P>) -> Self {
Client { Client {
provider, provider,
signer: None, signer: None,
@ -23,24 +24,37 @@ impl<'a, P, N, S> From<&'a Provider<P, N>> for Client<'a, P, N, S> {
} }
} }
impl<'a, P, N, S> Client<'a, P, N, S> #[derive(Debug, Error)]
pub enum ClientError {
#[error(transparent)]
ProviderError(#[from] ProviderError),
#[error(transparent)]
SignerError(#[from] Box<dyn std::error::Error>),
#[error("ens name not found: {0}")]
EnsError(String),
}
impl<P, S> Client<P, S>
where where
S: Signer, S: Signer,
P: JsonRpcClient, P: JsonRpcClient,
N: Network, ProviderError: From<<P as JsonRpcClient>::Error>,
ClientError: From<<S as Signer>::Error>,
{ {
/// Signs and broadcasts the transaction /// Signs and broadcasts the transaction
pub async fn send_transaction( pub async fn send_transaction(
&self, &self,
mut tx: TransactionRequest, mut tx: TransactionRequest,
block: Option<BlockNumber>, block: Option<BlockNumber>,
) -> Result<TxHash, P::Error> { ) -> Result<TxHash, ClientError> {
if let Some(ref to) = tx.to { if let Some(ref to) = tx.to {
if let NameOrAddress::Name(ens_name) = to { if let NameOrAddress::Name(ens_name) = to {
let addr = self let addr = self
.resolve_name(&ens_name) .resolve_name(&ens_name)
.await? .await?
.expect("TODO: Handle ENS name not found"); .ok_or_else(|| ClientError::EnsError(ens_name.to_owned()))?;
tx.to = Some(addr.into()) tx.to = Some(addr.into())
} }
} }
@ -50,14 +64,14 @@ where
let signer = if let Some(ref signer) = self.signer { let signer = if let Some(ref signer) = self.signer {
signer signer
} else { } else {
return self.provider.send_transaction(tx).await; return Ok(self.provider.send_transaction(tx).await?);
}; };
// fill any missing fields // fill any missing fields
self.fill_transaction(&mut tx, block).await?; self.fill_transaction(&mut tx, block).await?;
// sign the transaction // sign the transaction with the network
let signed_tx = signer.sign_transaction(tx).unwrap(); // TODO let signed_tx = signer.sign_transaction(tx)?;
// broadcast it // broadcast it
self.provider.send_raw_transaction(&signed_tx).await?; self.provider.send_raw_transaction(&signed_tx).await?;
@ -70,7 +84,7 @@ where
&self, &self,
tx: &mut TransactionRequest, tx: &mut TransactionRequest,
block: Option<BlockNumber>, block: Option<BlockNumber>,
) -> Result<(), P::Error> { ) -> Result<(), ClientError> {
// get the gas price // get the gas price
if tx.gas_price.is_none() { if tx.gas_price.is_none() {
tx.gas_price = Some(self.provider.get_gas_price().await?); tx.gas_price = Some(self.provider.get_gas_price().await?);
@ -103,8 +117,8 @@ where
} }
/// Returns a reference to the client's provider /// Returns a reference to the client's provider
pub fn provider(&self) -> &Provider<P, N> { pub fn provider(&self) -> &Provider<P> {
self.provider &self.provider
} }
/// Returns a reference to the client's signer, will panic if no signer is set /// Returns a reference to the client's signer, will panic if no signer is set
@ -116,11 +130,8 @@ where
// Abuse Deref to use the Provider's methods without re-writing everything. // Abuse Deref to use the Provider's methods without re-writing everything.
// This is an anti-pattern and should not be encouraged, but this improves the UX while // This is an anti-pattern and should not be encouraged, but this improves the UX while
// keeping the LoC low // keeping the LoC low
impl<'a, P, N, S> Deref for Client<'a, P, N, S> impl<P, S> Deref for Client<P, S> {
where type Target = Provider<P>;
N: 'a,
{
type Target = &'a Provider<P, N>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.provider &self.provider

View File

@ -2,9 +2,10 @@ mod wallet;
pub use wallet::Wallet; pub use wallet::Wallet;
mod client; mod client;
pub use client::Client; pub use client::{Client, ClientError};
use ethers_core::types::{Address, Signature, Transaction, TransactionRequest}; use ethers_core::types::{Address, Signature, Transaction, TransactionRequest};
use ethers_providers::http::Provider;
use std::error::Error; use std::error::Error;
/// Trait for signing transactions and messages /// Trait for signing transactions and messages
@ -23,14 +24,5 @@ pub trait Signer {
fn address(&self) -> Address; fn address(&self) -> Address;
} }
use ethers_providers::networks::{Any, Mainnet};
/// A Wallet instantiated with chain_id = 1 for Ethereum Mainnet.
pub type MainnetWallet = Wallet<Mainnet>;
/// A wallet which does not use EIP-155 and does not take the chain id into account
/// when creating transactions
pub type AnyWallet = Wallet<Any>;
/// An HTTP client configured to work with ANY blockchain without replay protection /// An HTTP client configured to work with ANY blockchain without replay protection
pub type HttpClient<'a> = Client<'a, ethers_providers::http::Provider, Any, Wallet<Any>>; pub type HttpClient = Client<Provider, Wallet>;

View File

@ -1,28 +1,31 @@
use crate::{Client, Signer}; use crate::{Client, ClientError, Signer};
use ethers_providers::{networks::Network, JsonRpcClient, Provider}; use ethers_providers::{JsonRpcClient, Provider};
use ethers_core::{ use ethers_core::{
rand::Rng, rand::Rng,
secp256k1, secp256k1,
types::{Address, PrivateKey, PublicKey, Signature, Transaction, TransactionRequest, TxError}, types::{
Address, PrivateKey, PublicKey, Signature, Transaction, TransactionRequest, TxError,
},
}; };
use std::{marker::PhantomData, str::FromStr}; use std::str::FromStr;
/// An Ethereum keypair /// An Ethereum keypair
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Wallet<N> { pub struct Wallet {
/// The Wallet's private Key /// The Wallet's private Key
pub private_key: PrivateKey, pub private_key: PrivateKey,
/// The Wallet's public Key /// The Wallet's public Key
pub public_key: PublicKey, pub public_key: PublicKey,
/// The wallet's address /// The wallet's address
pub address: Address, pub address: Address,
network: PhantomData<N>, /// The wallet's chain id (for EIP-155), signs w/o replay protection if left unset
pub chain_id: u64,
} }
impl<'a, N: Network> Signer for Wallet<N> { impl Signer for Wallet {
type Error = TxError; type Error = TxError;
fn sign_message<S: AsRef<[u8]>>(&self, message: S) -> Signature { fn sign_message<S: AsRef<[u8]>>(&self, message: S) -> Signature {
@ -30,7 +33,7 @@ impl<'a, N: Network> Signer for Wallet<N> {
} }
fn sign_transaction(&self, tx: TransactionRequest) -> Result<Transaction, Self::Error> { fn sign_transaction(&self, tx: TransactionRequest) -> Result<Transaction, Self::Error> {
self.private_key.sign_transaction(tx, N::CHAIN_ID) self.private_key.sign_transaction(tx, Some(self.chain_id))
} }
fn address(&self) -> Address { fn address(&self) -> Address {
@ -38,7 +41,13 @@ impl<'a, N: Network> Signer for Wallet<N> {
} }
} }
impl<N: Network> Wallet<N> { impl From<TxError> for ClientError {
fn from(src: TxError) -> Self {
ClientError::SignerError(Box::new(src))
}
}
impl Wallet {
// TODO: Add support for mnemonic and encrypted JSON // TODO: Add support for mnemonic and encrypted JSON
/// Creates a new random keypair seeded with the provided RNG /// Creates a new random keypair seeded with the provided RNG
@ -51,20 +60,26 @@ impl<N: Network> Wallet<N> {
private_key, private_key,
public_key, public_key,
address, address,
network: PhantomData, chain_id: 1,
} }
} }
/// Connects to a provider and returns a client /// Connects to a provider and returns a client
pub fn connect<P: JsonRpcClient>(self, provider: &Provider<P, N>) -> Client<P, N, Wallet<N>> { pub fn connect<P: JsonRpcClient>(self, provider: Provider<P>) -> Client<P, Wallet> {
Client { Client {
signer: Some(self), signer: Some(self),
provider, provider,
} }
} }
/// Sets the wallet's chain_id
pub fn chain_id<T: Into<u64>>(mut self, chain_id: T) -> Self {
self.chain_id = chain_id.into();
self
}
} }
impl<N: Network> From<PrivateKey> for Wallet<N> { impl From<PrivateKey> for Wallet {
fn from(private_key: PrivateKey) -> Self { fn from(private_key: PrivateKey) -> Self {
let public_key = PublicKey::from(&private_key); let public_key = PublicKey::from(&private_key);
let address = Address::from(&private_key); let address = Address::from(&private_key);
@ -73,12 +88,12 @@ impl<N: Network> From<PrivateKey> for Wallet<N> {
private_key, private_key,
public_key, public_key,
address, address,
network: PhantomData, chain_id: 1,
} }
} }
} }
impl<N: Network> FromStr for Wallet<N> { impl FromStr for Wallet {
type Err = secp256k1::Error; type Err = secp256k1::Error;
fn from_str(src: &str) -> Result<Self, Self::Err> { fn from_str(src: &str) -> Result<Self, Self::Err> {

View File

@ -0,0 +1,39 @@
use ethers_core::{types::TransactionRequest, utils::GanacheBuilder};
use ethers_providers::{Http, Provider};
use ethers_signers::Wallet;
use std::convert::TryFrom;
#[tokio::test]
async fn send_eth() {
let port = 8545u64;
let url = format!("http://localhost:{}", port).to_string();
let _ganache = GanacheBuilder::new()
.port(port)
.mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle")
.spawn();
// this private key belongs to the above mnemonic
let wallet: Wallet = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"
.parse().unwrap();
// connect to the network
let provider = Provider::<Http>::try_from(url.as_str()).unwrap();
// connect the wallet to the provider
let client = wallet.connect(provider);
// craft the transaction
let tx = TransactionRequest::new()
.send_to_str("986eE0C8B91A58e490Ee59718Cca41056Cf55f24")
.unwrap()
.value(10000);
let balance_before = client.get_balance(client.address(), None).await.unwrap();
// send it!
client.send_transaction(tx, None).await.unwrap();
let balance_after = client.get_balance(client.address(), None).await.unwrap();
assert!(balance_before > balance_after);
}