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 = [
"ethers-core",
"ethers-providers",
"thiserror",
"tokio",
]
[[package]]

View File

@ -1,5 +1,5 @@
use crate::{
types::{Address, NameOrAddress, Signature, Transaction, TransactionRequest, H256, U256, U64},
types::{Address, NameOrAddress, Signature, Transaction, TransactionRequest, H256, U256},
utils::{hash_message, keccak256},
};
@ -76,12 +76,12 @@ impl PrivateKey {
///
/// # 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.
pub fn sign_transaction(
&self,
tx: TransactionRequest,
chain_id: Option<U64>,
chain_id: Option<u64>,
) -> Result<Transaction, TxError> {
// The nonce, gas and gasprice fields must already be populated
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 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)
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;
if let Some(chain_id) = chain_id {
// 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 {
// Otherwise, convert to 'Electrum' notation.
standard_v + 27
@ -244,7 +244,7 @@ mod tests {
.parse()
.unwrap();
let tx = key.sign_transaction(tx, Some(chain_id.into())).unwrap();
let tx = key.sign_transaction(tx, Some(chain_id)).unwrap();
assert_eq!(
tx.hash,

View File

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

View File

@ -1,21 +1,22 @@
use crate::Signer;
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 thiserror::Error;
#[derive(Clone, Debug)]
/// 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
/// via the same API.
pub struct Client<'a, P, N, S> {
pub(crate) provider: &'a Provider<P, N>,
pub struct Client<P, S> {
pub(crate) provider: Provider<P>,
pub(crate) signer: Option<S>,
}
impl<'a, P, N, S> From<&'a Provider<P, N>> for Client<'a, P, N, S> {
fn from(provider: &'a Provider<P, N>) -> Self {
impl<P, S> From<Provider<P>> for Client<P, S> {
fn from(provider: Provider<P>) -> Self {
Client {
provider,
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
S: Signer,
P: JsonRpcClient,
N: Network,
ProviderError: From<<P as JsonRpcClient>::Error>,
ClientError: From<<S as Signer>::Error>,
{
/// Signs and broadcasts the transaction
pub async fn send_transaction(
&self,
mut tx: TransactionRequest,
block: Option<BlockNumber>,
) -> Result<TxHash, P::Error> {
) -> Result<TxHash, ClientError> {
if let Some(ref to) = tx.to {
if let NameOrAddress::Name(ens_name) = to {
let addr = self
.resolve_name(&ens_name)
.await?
.expect("TODO: Handle ENS name not found");
.ok_or_else(|| ClientError::EnsError(ens_name.to_owned()))?;
tx.to = Some(addr.into())
}
}
@ -50,14 +64,14 @@ where
let signer = if let Some(ref signer) = self.signer {
signer
} else {
return self.provider.send_transaction(tx).await;
return Ok(self.provider.send_transaction(tx).await?);
};
// fill any missing fields
self.fill_transaction(&mut tx, block).await?;
// sign the transaction
let signed_tx = signer.sign_transaction(tx).unwrap(); // TODO
// sign the transaction with the network
let signed_tx = signer.sign_transaction(tx)?;
// broadcast it
self.provider.send_raw_transaction(&signed_tx).await?;
@ -70,7 +84,7 @@ where
&self,
tx: &mut TransactionRequest,
block: Option<BlockNumber>,
) -> Result<(), P::Error> {
) -> Result<(), ClientError> {
// get the gas price
if tx.gas_price.is_none() {
tx.gas_price = Some(self.provider.get_gas_price().await?);
@ -103,8 +117,8 @@ where
}
/// Returns a reference to the client's provider
pub fn provider(&self) -> &Provider<P, N> {
self.provider
pub fn provider(&self) -> &Provider<P> {
&self.provider
}
/// 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.
// This is an anti-pattern and should not be encouraged, but this improves the UX while
// keeping the LoC low
impl<'a, P, N, S> Deref for Client<'a, P, N, S>
where
N: 'a,
{
type Target = &'a Provider<P, N>;
impl<P, S> Deref for Client<P, S> {
type Target = Provider<P>;
fn deref(&self) -> &Self::Target {
&self.provider

View File

@ -2,9 +2,10 @@ mod wallet;
pub use wallet::Wallet;
mod client;
pub use client::Client;
pub use client::{Client, ClientError};
use ethers_core::types::{Address, Signature, Transaction, TransactionRequest};
use ethers_providers::http::Provider;
use std::error::Error;
/// Trait for signing transactions and messages
@ -23,14 +24,5 @@ pub trait Signer {
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
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::{
rand::Rng,
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
#[derive(Clone, Debug)]
pub struct Wallet<N> {
pub struct Wallet {
/// The Wallet's private Key
pub private_key: PrivateKey,
/// The Wallet's public Key
pub public_key: PublicKey,
/// The wallet's 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;
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> {
self.private_key.sign_transaction(tx, N::CHAIN_ID)
self.private_key.sign_transaction(tx, Some(self.chain_id))
}
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
/// Creates a new random keypair seeded with the provided RNG
@ -51,20 +60,26 @@ impl<N: Network> Wallet<N> {
private_key,
public_key,
address,
network: PhantomData,
chain_id: 1,
}
}
/// 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 {
signer: Some(self),
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 {
let public_key = PublicKey::from(&private_key);
let address = Address::from(&private_key);
@ -73,12 +88,12 @@ impl<N: Network> From<PrivateKey> for Wallet<N> {
private_key,
public_key,
address,
network: PhantomData,
chain_id: 1,
}
}
}
impl<N: Network> FromStr for Wallet<N> {
impl FromStr for Wallet {
type Err = secp256k1::Error;
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);
}