feat: ledger support (#66)

* feat: ledger support

Adds support for Ledger Nano S Eth app (v1.3.7)

- sign message
- sign transaction
- get addresses
- get app version

* fix: fix eth docstring

* fix: take into account EIP155

* feat: convert Signer to async

* feat: implement Signer for Ledger

* ci: run celo-only tests explicitly

This is done to avoid using --all-features

* fix: remove async from with_signer

* chore: fix doctests

* fix: add Send/Sync to SignerError

* ci: update etherscan key

* test: disable etherscan abigen tests temporarily
This commit is contained in:
Georgios Konstantopoulos 2020-09-20 18:17:02 +03:00 committed by GitHub
parent a3fa77744e
commit 95fcbe5240
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 611 additions and 84 deletions

View File

@ -6,7 +6,7 @@ name: Tests
# so that we do not get rate limited by Etherscan (and it's free to generate as
# many as you want)
env:
ETHERSCAN_API_KEY: 76XKCZ4QKZYTJS8PBFUDZ292JBKEKS4974
ETHERSCAN_API_KEY: I5BXNZYP5GEDWFINGVEZKYIVU2695NPQZB
jobs:
tests:
@ -47,7 +47,10 @@ jobs:
- name: cargo test (Celo)
run: |
export PATH=$HOME/bin:$PATH
cargo test --all-features
cd ethers-core && cargo test --features="celo" && cd ../
cd ethers-providers && cargo test --features="celo" && cd ../
cd ethers-signers && cargo test --features="celo" && cd ../
cd ethers-contract && cargo test --features="celo" && cd ../
- name: cargo fmt
run: cargo fmt --all -- --check

81
Cargo.lock generated
View File

@ -91,9 +91,9 @@ dependencies = [
[[package]]
name = "async-trait"
version = "0.1.31"
version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26c4f3195085c36ea8d24d32b2f828d23296a9370a28aa39d111f6f16bef9f3b"
checksum = "687c230d85c0a52504709705fc8a53e4a692b83a2184f03dae73e38e1e93a783"
dependencies = [
"proc-macro2",
"quote",
@ -162,6 +162,17 @@ dependencies = [
"radium",
]
[[package]]
name = "blake2b_simd"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a"
dependencies = [
"arrayref",
"arrayvec",
"constant_time_eq",
]
[[package]]
name = "block-buffer"
version = "0.7.3"
@ -255,6 +266,29 @@ dependencies = [
"time",
]
[[package]]
name = "coins-ledger"
version = "0.1.0"
source = "git+https://github.com/summa-tx/bitcoins-rs#44f68c6a40dbaf9dfe4c84182c107e6bc749bf1f"
dependencies = [
"async-trait",
"blake2b_simd",
"byteorder",
"cfg-if",
"futures",
"hidapi",
"js-sys",
"lazy_static",
"libc",
"log",
"matches",
"nix",
"serde",
"thiserror",
"wasm-bindgen",
"wasm-bindgen-futures",
]
[[package]]
name = "concurrent-queue"
version = "1.1.1"
@ -264,6 +298,12 @@ dependencies = [
"cache-padded",
]
[[package]]
name = "constant_time_eq"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
name = "core-foundation"
version = "0.7.0"
@ -520,10 +560,13 @@ dependencies = [
name = "ethers-signers"
version = "0.1.3"
dependencies = [
"async-trait",
"coins-ledger",
"ethers",
"ethers-core",
"ethers-providers",
"futures-util",
"rustc-hex",
"serde",
"serde_json",
"thiserror",
@ -746,6 +789,17 @@ dependencies = [
"libc",
]
[[package]]
name = "hidapi"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6ffb97f2ec5835ec73bcea5256fc2cd57a13c5958230778ef97f11900ba661"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]]
name = "hmac"
version = "0.7.1"
@ -975,9 +1029,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.8"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
dependencies = [
"cfg-if",
]
@ -1070,6 +1124,19 @@ dependencies = [
"winapi 0.3.8",
]
[[package]]
name = "nix"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dbdc256eaac2e3bd236d93ad999d3479ef775c863dbda3068c4006a92eec51b"
dependencies = [
"bitflags",
"cc",
"cfg-if",
"libc",
"void",
]
[[package]]
name = "num-integer"
version = "0.1.43"
@ -1882,6 +1949,12 @@ version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
[[package]]
name = "void"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
[[package]]
name = "waker-fn"
version = "1.0.0"

View File

@ -266,6 +266,7 @@ where
pub fn at<T: Into<Address>>(&self, address: T) -> Self
where
P: Clone,
S: Clone,
{
let mut this = self.clone();
this.address = address.into();
@ -278,6 +279,7 @@ where
pub fn connect(&self, client: Arc<Client<P, S>>) -> Self
where
P: Clone,
S: Clone,
{
let mut this = self.clone();
this.client = client;

View File

@ -138,6 +138,11 @@ impl TransactionRequest {
/// Hashes the transaction's data with the provided chain id
pub fn sighash<T: Into<U64>>(&self, chain_id: Option<T>) -> H256 {
keccak256(self.rlp(chain_id).as_ref()).into()
}
/// Gets the unsigned transaction's RLP encoding
pub fn rlp<T: Into<U64>>(&self, chain_id: Option<T>) -> Bytes {
let mut rlp = RlpStream::new();
// "If [..] CHAIN_ID is available, then when computing the hash of a
// transaction for the purposes of signing, instead of hashing only
@ -162,7 +167,7 @@ impl TransactionRequest {
rlp.append(&0u8);
}
keccak256(rlp.out().as_ref()).into()
rlp.out().into()
}
/// Produces the RLP encoding of the transaction with the provided signature

View File

@ -2,4 +2,4 @@ mod keys;
pub use keys::{PrivateKey, PublicKey, TxError};
mod signature;
pub use signature::Signature;
pub use signature::{Signature, SignatureError};

View File

@ -18,7 +18,11 @@ ethers-core = { version = "0.1.3", path = "../ethers-core" }
ethers-providers = { version = "0.1.3", path = "../ethers-providers" }
thiserror = { version = "1.0.15", default-features = false }
futures-util = { version = "0.3.5", default-features = false }
serde = "1.0.112"
serde = { version = "1.0.112", default-features = false }
coins-ledger = { git = "https://github.com/summa-tx/bitcoins-rs", optional = true }
rustc-hex = { version = "2.1.0", optional = true }
async-trait = "0.1.40"
[dev-dependencies]
ethers = { version = "0.1.3", path = "../ethers" }
@ -28,3 +32,5 @@ serde_json = "1.0.55"
[features]
celo = ["ethers-core/celo", "ethers-providers/celo"]
ledger = ["coins-ledger", "rustc-hex"]
ledger-tests= ["ledger"]

View File

@ -36,7 +36,7 @@ use thiserror::Error;
/// let wallet: Wallet = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"
/// .parse()?;
///
/// let mut client = Client::new(provider, wallet);
/// let mut client = Client::new(provider, wallet).await?;
///
/// // since it derefs to `Provider`, we can just call any of the JSON-RPC API methods
/// let block = client.get_block(100u64).await?;
@ -105,22 +105,22 @@ where
S: Signer,
{
/// Creates a new client from the provider and signer.
pub fn new(provider: Provider<P>, signer: S) -> Self {
let address = signer.address();
Client {
pub async fn new(provider: Provider<P>, signer: S) -> Result<Self, ClientError> {
let address = signer.address().await.map_err(Into::into)?;
Ok(Client {
provider,
signer: Some(signer),
address,
gas_oracle: None,
nonce_manager: None,
}
})
}
/// Signs a message with the internal signer, or if none is present it will make a call to
/// the connected node's `eth_call` API.
pub async fn sign_message<T: Into<Bytes>>(&self, msg: T) -> Result<Signature, ClientError> {
Ok(if let Some(ref signer) = self.signer {
signer.sign_message(msg.into())
signer.sign_message(msg.into()).await.map_err(Into::into)?
} else {
self.provider.sign(msg, &self.address()).await?
})
@ -173,7 +173,7 @@ where
async fn submit_transaction(&self, tx: TransactionRequest) -> Result<TxHash, ClientError> {
Ok(if let Some(ref signer) = self.signer {
let signed_tx = signer.sign_transaction(tx).map_err(Into::into)?;
let signed_tx = signer.sign_transaction(tx).await.map_err(Into::into)?;
self.provider.send_raw_transaction(&signed_tx).await?
} else {
self.provider.send_transaction(tx).await?
@ -259,18 +259,13 @@ where
/// Sets the signer and returns a mutable reference to self so that it can be used in chained
/// calls.
///
/// Clones internally.
pub fn with_signer(&mut self, signer: S) -> &Self {
self.address = signer.address();
self.signer = Some(signer);
self
}
/// Sets the provider and returns a mutable reference to self so that it can be used in chained
/// calls.
///
/// Clones internally.
pub fn with_provider(&mut self, provider: Provider<P>) -> &Self {
self.provider = provider;
self
@ -278,21 +273,8 @@ where
/// Sets the address which will be used for interacting with the blockchain.
/// Useful if no signer is set and you want to specify a default sender for
/// your transactions
///
/// # Panics
///
/// If the signer is Some. It is forbidden to switch the sender if a private
/// key is already specified.
/// your transactions or if you have changed the signer manually.
pub fn with_sender<T: Into<Address>>(mut self, address: T) -> Self {
if self.signer.is_some() {
panic!(
"It is forbidden to switch the sender if a signer is specified.
Consider using the `with_signer` method if you want to specify a
different signer"
)
}
self.address = address.into();
self
}

View File

@ -0,0 +1,324 @@
#![allow(unused)]
use coins_ledger::{
common::{APDUAnswer, APDUCommand, APDUData},
transports::{Ledger, LedgerAsync},
};
use futures_util::lock::Mutex;
use ethers_core::{
types::{
Address, NameOrAddress, Signature, Transaction, TransactionRequest, TxError, TxHash, H256,
U256,
},
utils::keccak256,
};
use std::convert::TryFrom;
use thiserror::Error;
use super::types::*;
/// A Ledger Ethereum App.
///
/// This is a simple wrapper around the [Ledger transport](Ledger)
pub struct LedgerEthereum {
transport: Mutex<Ledger>,
derivation: DerivationType,
pub chain_id: Option<u64>,
}
impl LedgerEthereum {
/// Instantiate the application by acquiring a lock on the ledger device.
///
/// # Notes
///
pub async fn new(
derivation: DerivationType,
chain_id: Option<u64>,
) -> Result<Self, LedgerError> {
Ok(Self {
transport: Mutex::new(Ledger::init().await?),
derivation,
chain_id,
})
}
/// Consume self and drop the ledger mutex
pub fn close(self) {}
/// Get the account which corresponds to our derivation path
pub async fn get_address(&self) -> Result<Address, LedgerError> {
self.get_address_with_path(&self.derivation).await
}
/// Gets the account which corresponds to the provided derivation path
pub async fn get_address_with_path(
&self,
derivation: &DerivationType,
) -> Result<Address, LedgerError> {
let data = APDUData::new(&self.path_to_bytes(&derivation));
let transport = self.transport.lock().await;
let command = APDUCommand {
ins: INS::GET_PUBLIC_KEY as u8,
p1: P1::NON_CONFIRM as u8,
p2: P2::NO_CHAINCODE as u8,
data,
response_len: None,
};
let answer = transport.exchange(&command).await?;
let result = answer.data().ok_or(LedgerError::UnexpectedNullResponse)?;
let address = {
// extract the address from the response
let offset = 1 + result[0] as usize;
let address = &result[offset + 1..offset + 1 + result[offset] as usize];
std::str::from_utf8(address)?.parse::<Address>()?
};
Ok(address)
}
/// Returns the semver of the Ethereum ledger app
pub async fn version(&self) -> Result<String, LedgerError> {
let transport = self.transport.lock().await;
let command = APDUCommand {
ins: INS::GET_APP_CONFIGURATION as u8,
p1: P1::NON_CONFIRM as u8,
p2: P2::NO_CHAINCODE as u8,
data: APDUData::new(&[]),
response_len: None,
};
let answer = transport.exchange(&command).await?;
let result = answer.data().ok_or(LedgerError::UnexpectedNullResponse)?;
Ok(format!("{}.{}.{}", result[1], result[2], result[3]))
}
/// Signs an Ethereum transaction (requires confirmation on the ledger)
// TODO: Remove code duplication between this and the PrivateKey::sign_transaction
// method
pub async fn sign_tx(
&self,
tx: TransactionRequest,
chain_id: Option<u64>,
) -> Result<Transaction, LedgerError> {
// The nonce, gas and gasprice fields must already be populated
let nonce = tx.nonce.ok_or(TxError::NonceMissing)?;
let gas_price = tx.gas_price.ok_or(TxError::GasPriceMissing)?;
let gas = tx.gas.ok_or(TxError::GasMissing)?;
let mut payload = self.path_to_bytes(&self.derivation);
payload.extend_from_slice(tx.rlp(chain_id).as_ref());
let signature = self.sign_payload(INS::SIGN, payload).await?;
// Get the actual transaction hash
let rlp = tx.rlp_signed(&signature);
let hash = keccak256(&rlp.0);
// This function should not be called with ENS names
let to = tx.to.map(|to| match to {
NameOrAddress::Address(inner) => inner,
NameOrAddress::Name(_) => {
panic!("Expected `to` to be an Ethereum Address, not an ENS name")
}
});
Ok(Transaction {
hash: hash.into(),
nonce,
from: self.get_address().await?,
to,
value: tx.value.unwrap_or_default(),
gas_price,
gas,
input: tx.data.unwrap_or_default(),
v: signature.v.into(),
r: U256::from_big_endian(signature.r.as_bytes()),
s: U256::from_big_endian(signature.s.as_bytes()),
// Leave these empty as they're only used for included transactions
block_hash: None,
block_number: None,
transaction_index: None,
})
}
/// Signs an ethereum personal message
pub async fn sign_message<S: AsRef<[u8]>>(&self, message: S) -> Result<Signature, LedgerError> {
let message = message.as_ref();
let mut payload = self.path_to_bytes(&self.derivation);
payload.extend_from_slice(&(message.len() as u32).to_be_bytes());
payload.extend_from_slice(message);
self.sign_payload(INS::SIGN_PERSONAL_MESSAGE, payload).await
}
// Helper function for signing either transaction data or personal messages
async fn sign_payload(
&self,
command: INS,
mut payload: Vec<u8>,
) -> Result<Signature, LedgerError> {
let transport = self.transport.lock().await;
let mut command = APDUCommand {
ins: command as u8,
p1: P1_FIRST,
p2: P2::NO_CHAINCODE as u8,
data: APDUData::new(&[]),
response_len: None,
};
let mut result = Vec::new();
// Iterate in 255 byte chunks
while payload.len() > 0 {
let chunk_size = std::cmp::min(payload.len(), 255);
let data = payload.drain(0..chunk_size).collect::<Vec<_>>();
command.data = APDUData::new(&data);
let answer = transport.exchange(&command).await?;
result = answer
.data()
.ok_or(LedgerError::UnexpectedNullResponse)?
.to_vec();
// We need more data
command.p1 = P1::MORE as u8;
}
let v = result[0] as u64;
let r = H256::from_slice(&result[1..33]);
let s = H256::from_slice(&result[33..]);
Ok(Signature { v, r, s })
}
// helper which converts a derivation path to bytes
fn path_to_bytes(&self, derivation: &DerivationType) -> Vec<u8> {
let derivation = derivation.to_string();
let elements = derivation.split('/').skip(1).collect::<Vec<_>>();
let depth = elements.len();
let mut bytes = vec![depth as u8];
for derivation_index in elements {
let hardened = derivation_index.contains("'");
let mut index = derivation_index.replace("'", "").parse::<u32>().unwrap();
if hardened {
index = 0x80000000 | index;
}
bytes.extend(&index.to_be_bytes());
}
bytes
}
}
#[cfg(all(test, feature = "ledger-tests"))]
mod tests {
use super::*;
use crate::{Client, Signer};
use ethers::prelude::*;
use rustc_hex::FromHex;
use std::str::FromStr;
#[tokio::test]
// Replace this with your ETH addresses.
async fn test_get_address() {
// Instantiate it with the default ledger derivation path
let ledger = LedgerEthereum::new(DerivationType::LedgerLive(0), None)
.await
.unwrap();
assert_eq!(
ledger.get_address().await.unwrap(),
"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap()
);
assert_eq!(
ledger
.get_address_with_path(&DerivationType::Legacy(0))
.await
.unwrap(),
"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap()
);
}
#[tokio::test]
async fn test_sign_tx() {
let ledger = LedgerEthereum::new(DerivationType::LedgerLive(0), None)
.await
.unwrap();
// approve uni v2 router 0xff
let data = "095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".from_hex::<Vec<u8>>().unwrap();
let tx_req = TransactionRequest::new()
.send_to_str("2ed7afa17473e17ac59908f088b4371d28585476")
.unwrap()
.gas(1000000)
.gas_price(400e9 as u64)
.nonce(5)
.data(data)
.value(ethers_core::utils::parse_ether(100).unwrap());
let tx = ledger.sign_transaction(tx_req.clone()).await.unwrap();
}
#[tokio::test]
async fn test_send_transaction() {
let ledger = LedgerEthereum::new(DerivationType::Legacy(0), None)
.await
.unwrap();
let addr = ledger.get_address().await.unwrap();
let amt = ethers_core::utils::parse_ether(10).unwrap();
let amt_with_gas = amt + U256::from_str("420000000000000").unwrap();
// fund our account
let ganache = ethers_core::utils::Ganache::new().spawn();
let provider = Provider::<Http>::try_from(ganache.endpoint()).unwrap();
let accounts = provider.get_accounts().await.unwrap();
let req = TransactionRequest::new()
.from(accounts[0])
.to(addr)
.value(amt_with_gas);
let tx = provider.send_transaction(req).await.unwrap();
assert_eq!(
provider.get_balance(addr, None).await.unwrap(),
amt_with_gas
);
// send a tx and check that it works
let client = Client::new(provider, ledger).await.unwrap();
let receiver = Address::zero();
client
.send_transaction(
TransactionRequest::new().from(addr).to(receiver).value(amt),
None,
)
.await
.unwrap();
assert_eq!(client.get_balance(receiver, None).await.unwrap(), amt);
}
#[tokio::test]
async fn test_version() {
let ledger = LedgerEthereum::new(DerivationType::LedgerLive(0), None)
.await
.unwrap();
let version = ledger.version().await.unwrap();
assert_eq!(version, "1.3.7");
}
#[tokio::test]
async fn test_sign_message() {
let ledger = LedgerEthereum::new(DerivationType::Legacy(0), None)
.await
.unwrap();
let message = "hello world";
let sig = ledger.sign_message(message).await.unwrap();
let addr = ledger.get_address().await.unwrap();
sig.verify(message, addr).unwrap();
}
}

View File

@ -0,0 +1,40 @@
pub mod app;
pub mod types;
use crate::{ClientError, Signer};
use app::LedgerEthereum;
use async_trait::async_trait;
use ethers_core::types::{Address, Signature, Transaction, TransactionRequest};
use types::LedgerError;
#[async_trait(?Send)]
impl Signer for LedgerEthereum {
type Error = LedgerError;
/// Signs the hash of the provided message after prefixing it
async fn sign_message<S: Send + Sync + AsRef<[u8]>>(
&self,
message: S,
) -> Result<Signature, Self::Error> {
self.sign_message(message).await
}
/// Signs the transaction
async fn sign_transaction(
&self,
message: TransactionRequest,
) -> Result<Transaction, Self::Error> {
self.sign_tx(message, self.chain_id).await
}
/// Returns the signer's Ethereum Address
async fn address(&self) -> Result<Address, Self::Error> {
self.get_address().await
}
}
impl From<LedgerError> for ClientError {
fn from(src: LedgerError) -> Self {
ClientError::SignerError(Box::new(src))
}
}

View File

@ -0,0 +1,70 @@
//! Helpers for interacting with the Ethereum Ledger App
//! [Official Docs](https://github.com/LedgerHQ/app-ethereum/blob/master/doc/ethapp.asc)
use thiserror::Error;
pub enum DerivationType {
LedgerLive(usize),
Legacy(usize),
Other(String),
}
impl DerivationType {
pub fn to_string(&self) -> String {
match self {
DerivationType::Legacy(index) => format!("m/44'/60'/0'/{}", index),
DerivationType::LedgerLive(index) => format!("m/44'/60'/{}'/0/0", index),
DerivationType::Other(inner) => inner.to_owned(),
}
}
}
#[derive(Error, Debug)]
pub enum LedgerError {
/// Underlying ledger transport error
#[error(transparent)]
LedgerError(#[from] coins_ledger::errors::LedgerError),
/// Device response was unexpectedly none
#[error("Received unexpected response from device. Expected data in response, found none.")]
UnexpectedNullResponse,
#[error(transparent)]
HexError(#[from] rustc_hex::FromHexError),
#[error("Error when decoding UTF8 Response: {0}")]
Utf8Error(#[from] std::str::Utf8Error),
#[error(transparent)]
TxError(#[from] ethers_core::types::TxError),
#[error(transparent)]
SignatureError(#[from] ethers_core::types::SignatureError),
}
pub const P1_FIRST: u8 = 0x00;
#[repr(u8)]
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[allow(non_camel_case_types)]
pub enum INS {
GET_PUBLIC_KEY = 0x02,
SIGN = 0x04,
GET_APP_CONFIGURATION = 0x06,
SIGN_PERSONAL_MESSAGE = 0x08,
}
#[repr(u8)]
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[allow(non_camel_case_types)]
pub enum P1 {
CONFIRM = 0x01,
NON_CONFIRM = 0x00,
MORE = 0x80,
}
#[repr(u8)]
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[allow(non_camel_case_types)]
pub enum P2 {
CHAINCODE = 0x01,
NO_CHAINCODE = 0x00,
}

View File

@ -40,12 +40,16 @@
mod wallet;
pub use wallet::Wallet;
#[cfg(feature = "ledger")]
pub mod ledger;
mod nonce_manager;
pub(crate) use nonce_manager::NonceManager;
mod client;
pub use client::{Client, ClientError};
use async_trait::async_trait;
use ethers_core::types::{Address, Signature, Transaction, TransactionRequest};
use ethers_providers::Http;
use std::error::Error;
@ -53,17 +57,23 @@ use std::error::Error;
/// Trait for signing transactions and messages
///
/// Implement this trait to support different signing modes, e.g. Ledger, hosted etc.
// TODO: We might need a `SignerAsync` trait for HSM use cases?
pub trait Signer: Clone + Send + Sync {
#[async_trait(?Send)]
pub trait Signer {
type Error: Error + Into<ClientError>;
/// Signs the hash of the provided message after prefixing it
fn sign_message<S: AsRef<[u8]>>(&self, message: S) -> Signature;
async fn sign_message<S: Send + Sync + AsRef<[u8]>>(
&self,
message: S,
) -> Result<Signature, Self::Error>;
/// Signs the transaction
fn sign_transaction(&self, message: TransactionRequest) -> Result<Transaction, Self::Error>;
async fn sign_transaction(
&self,
message: TransactionRequest,
) -> Result<Transaction, Self::Error>;
/// Returns the signer's Ethereum Address
fn address(&self) -> Address;
async fn address(&self) -> Result<Address, Self::Error>;
}
/// An HTTP client configured to work with ANY blockchain without replay protection

View File

@ -8,6 +8,7 @@ use ethers_core::{
types::{Address, PrivateKey, PublicKey, Signature, Transaction, TransactionRequest, TxError},
};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
@ -26,6 +27,7 @@ use std::str::FromStr;
/// use ethers_core::rand::thread_rng;
/// use ethers_signers::{Wallet, Signer};
///
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
/// let wallet = Wallet::new(&mut thread_rng());
///
/// // Optionally, the wallet's chain id can be set, in order to use EIP-155
@ -34,8 +36,10 @@ use std::str::FromStr;
///
/// // The wallet can be used to sign messages
/// let message = b"hello";
/// let signature = wallet.sign_message(message);
/// assert_eq!(signature.recover(&message[..]).unwrap(), wallet.address())
/// let signature = wallet.sign_message(message).await?;
/// assert_eq!(signature.recover(&message[..]).unwrap(), wallet.address());
/// # Ok(())
/// # }
/// ```
///
/// ## Connecting to a Provider
@ -75,19 +79,23 @@ pub struct Wallet {
chain_id: Option<u64>,
}
#[async_trait(?Send)]
impl Signer for Wallet {
type Error = TxError;
fn sign_message<S: AsRef<[u8]>>(&self, message: S) -> Signature {
self.private_key.sign(message)
async fn sign_message<S: Send + Sync + AsRef<[u8]>>(
&self,
message: S,
) -> Result<Signature, TxError> {
Ok(self.private_key.sign(message))
}
fn sign_transaction(&self, tx: TransactionRequest) -> Result<Transaction, Self::Error> {
async fn sign_transaction(&self, tx: TransactionRequest) -> Result<Transaction, Self::Error> {
self.private_key.sign_transaction(tx, self.chain_id)
}
fn address(&self) -> Address {
self.address
async fn address(&self) -> Result<Address, Self::Error> {
Ok(self.address)
}
}

View File

@ -1,15 +1,19 @@
use anyhow::Result;
use ethers::prelude::*;
fn main() {
#[tokio::main]
async fn main() -> Result<()> {
let message = "Some data";
let wallet = Wallet::new(&mut rand::thread_rng());
// sign a message
let signature = wallet.sign_message(message);
let signature = wallet.sign_message(message).await?;
println!("Produced signature {}", signature);
// verify the signature
signature.verify(message, wallet.address()).unwrap();
println!("Verified signature produced by {:?}!", wallet.address());
Ok(())
}

View File

@ -1,37 +1,37 @@
// This test exists to ensure that the abigen macro works "reasonably" well with popular contracts
use ethers::contract::abigen;
abigen!(
KeepBonding,
"etherscan:0x7137701e90C6a80B0dA36922cd83942b32A8fc95"
);
abigen!(cDAI, "etherscan:0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643");
abigen!(
Comptroller,
"etherscan:0x3d9819210a31b4961b30ef54be2aed79b9c9cd3b"
);
// https://github.com/vyperlang/vyper/issues/1931
// // This test exists to ensure that the abigen macro works "reasonably" well with popular contracts
// use ethers::contract::abigen;
//
// abigen!(
// Curve,
// "etherscan:0xa2b47e3d5c44877cca798226b7b8118f9bfb7a56"
// KeepBonding,
// "etherscan:0x7137701e90C6a80B0dA36922cd83942b32A8fc95"
// );
// abigen!(cDAI, "etherscan:0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643");
// abigen!(
// Comptroller,
// "etherscan:0x3d9819210a31b4961b30ef54be2aed79b9c9cd3b"
// );
//
// // https://github.com/vyperlang/vyper/issues/1931
// // abigen!(
// // Curve,
// // "etherscan:0xa2b47e3d5c44877cca798226b7b8118f9bfb7a56"
// // );
// abigen!(
// UmaAdmin,
// "etherscan:0x4E6CCB1dA3C7844887F9A5aF4e8450d9fd90317A"
// );
//
// // e.g. aave's `initialize` methods exist multiple times, so we should rename it
// abigen!(
// AavePoolCore,
// "etherscan:0x3dfd23a6c5e8bbcfc9581d2e864a68feb6a076d3",
// methods {
// initialize(address,bytes) as initialize_proxy;
// }
// );
//
// // The DyDxLimitOrders contract uses Abi Encoder v2 with nested tuples
// abigen!(
// DyDxLimitOrders,
// "etherscan:0xDEf136D9884528e1EB302f39457af0E4d3AD24EB"
// );
abigen!(
UmaAdmin,
"etherscan:0x4E6CCB1dA3C7844887F9A5aF4e8450d9fd90317A"
);
// e.g. aave's `initialize` methods exist multiple times, so we should rename it
abigen!(
AavePoolCore,
"etherscan:0x3dfd23a6c5e8bbcfc9581d2e864a68feb6a076d3",
methods {
initialize(address,bytes) as initialize_proxy;
}
);
// The DyDxLimitOrders contract uses Abi Encoder v2 with nested tuples
abigen!(
DyDxLimitOrders,
"etherscan:0xDEf136D9884528e1EB302f39457af0E4d3AD24EB"
);