diff --git a/Cargo.lock b/Cargo.lock index 0f79c3d0..5be676e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -372,6 +372,8 @@ version = "0.1.0" dependencies = [ "ethers-core", "ethers-providers", + "thiserror", + "tokio", ] [[package]] diff --git a/ethers-core/src/types/crypto/keys.rs b/ethers-core/src/types/crypto/keys.rs index 11e40b71..d69131cd 100644 --- a/ethers-core/src/types/crypto/keys.rs +++ b/ethers-core/src/types/crypto/keys.rs @@ -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, + chain_id: Option, ) -> Result { // 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) -> Signature { + fn sign_with_eip155(&self, message: &Message, chain_id: Option) -> 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 { +fn to_eip155_v(recovery_id: RecoveryId, chain_id: Option) -> 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, diff --git a/ethers-signers/Cargo.toml b/ethers-signers/Cargo.toml index 78b92c26..f8bd8a52 100644 --- a/ethers-signers/Cargo.toml +++ b/ethers-signers/Cargo.toml @@ -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"] } diff --git a/ethers-signers/src/client.rs b/ethers-signers/src/client.rs index ffce3054..337ae064 100644 --- a/ethers-signers/src/client.rs +++ b/ethers-signers/src/client.rs @@ -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, +pub struct Client { + pub(crate) provider: Provider

, pub(crate) signer: Option, } -impl<'a, P, N, S> From<&'a Provider> for Client<'a, P, N, S> { - fn from(provider: &'a Provider) -> Self { +impl From> for Client { + fn from(provider: Provider

) -> Self { Client { provider, signer: None, @@ -23,24 +24,37 @@ impl<'a, P, N, S> From<&'a Provider> 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), + + #[error("ens name not found: {0}")] + EnsError(String), +} + +impl Client where S: Signer, P: JsonRpcClient, - N: Network, + ProviderError: From<

::Error>, + ClientError: From<::Error>, { /// Signs and broadcasts the transaction pub async fn send_transaction( &self, mut tx: TransactionRequest, block: Option, - ) -> Result { + ) -> Result { 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, - ) -> 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 { - self.provider + pub fn provider(&self) -> &Provider

{ + &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; +impl Deref for Client { + type Target = Provider

; fn deref(&self) -> &Self::Target { &self.provider diff --git a/ethers-signers/src/lib.rs b/ethers-signers/src/lib.rs index 8ac5d631..9d21a1ef 100644 --- a/ethers-signers/src/lib.rs +++ b/ethers-signers/src/lib.rs @@ -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; - -/// A wallet which does not use EIP-155 and does not take the chain id into account -/// when creating transactions -pub type AnyWallet = Wallet; - /// An HTTP client configured to work with ANY blockchain without replay protection -pub type HttpClient<'a> = Client<'a, ethers_providers::http::Provider, Any, Wallet>; +pub type HttpClient = Client; diff --git a/ethers-signers/src/wallet.rs b/ethers-signers/src/wallet.rs index eee13aa4..b912ee4a 100644 --- a/ethers-signers/src/wallet.rs +++ b/ethers-signers/src/wallet.rs @@ -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 { +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, + /// 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 { +impl Signer for Wallet { type Error = TxError; fn sign_message>(&self, message: S) -> Signature { @@ -30,7 +33,7 @@ impl<'a, N: Network> Signer for Wallet { } fn sign_transaction(&self, tx: TransactionRequest) -> Result { - 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 { } } -impl Wallet { +impl From 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 Wallet { private_key, public_key, address, - network: PhantomData, + chain_id: 1, } } /// Connects to a provider and returns a client - pub fn connect(self, provider: &Provider) -> Client> { + pub fn connect(self, provider: Provider

) -> Client { Client { signer: Some(self), provider, } } + + /// Sets the wallet's chain_id + pub fn chain_id>(mut self, chain_id: T) -> Self { + self.chain_id = chain_id.into(); + self + } } -impl From for Wallet { +impl From 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 From for Wallet { private_key, public_key, address, - network: PhantomData, + chain_id: 1, } } } -impl FromStr for Wallet { +impl FromStr for Wallet { type Err = secp256k1::Error; fn from_str(src: &str) -> Result { diff --git a/ethers-signers/tests/send_eth.rs b/ethers-signers/tests/send_eth.rs new file mode 100644 index 00000000..a8e12c07 --- /dev/null +++ b/ethers-signers/tests/send_eth.rs @@ -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::::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); +}