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