simplify signers
This commit is contained in:
parent
6181943485
commit
27ca5dd55a
|
@ -372,6 +372,8 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"ethers-core",
|
||||
"ethers-providers",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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);
|
||||
}
|
Loading…
Reference in New Issue