From 95fcbe524052eddb928bf0fe5014e96bed5ea9a0 Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Sun, 20 Sep 2020 18:17:02 +0300 Subject: [PATCH] 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 --- .github/workflows/ci.yml | 7 +- Cargo.lock | 81 ++++- ethers-contract/src/contract.rs | 2 + .../src/types/chainstate/transaction.rs | 7 +- ethers-core/src/types/crypto/mod.rs | 2 +- ethers-signers/Cargo.toml | 8 +- ethers-signers/src/client.rs | 34 +- ethers-signers/src/ledger/app.rs | 324 ++++++++++++++++++ ethers-signers/src/ledger/mod.rs | 40 +++ ethers-signers/src/ledger/types.rs | 70 ++++ ethers-signers/src/lib.rs | 20 +- ethers-signers/src/wallet.rs | 22 +- ethers/examples/sign.rs | 8 +- ethers/tests/major_contracts.rs | 70 ++-- 14 files changed, 611 insertions(+), 84 deletions(-) create mode 100644 ethers-signers/src/ledger/app.rs create mode 100644 ethers-signers/src/ledger/mod.rs create mode 100644 ethers-signers/src/ledger/types.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e6a2ab3..ee1396f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 196fcdb4..cbb615d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/ethers-contract/src/contract.rs b/ethers-contract/src/contract.rs index 4c3f4810..cb31a07c 100644 --- a/ethers-contract/src/contract.rs +++ b/ethers-contract/src/contract.rs @@ -266,6 +266,7 @@ where pub fn at>(&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>) -> Self where P: Clone, + S: Clone, { let mut this = self.clone(); this.client = client; diff --git a/ethers-core/src/types/chainstate/transaction.rs b/ethers-core/src/types/chainstate/transaction.rs index acdbadc3..154006e1 100644 --- a/ethers-core/src/types/chainstate/transaction.rs +++ b/ethers-core/src/types/chainstate/transaction.rs @@ -138,6 +138,11 @@ impl TransactionRequest { /// Hashes the transaction's data with the provided chain id pub fn sighash>(&self, chain_id: Option) -> H256 { + keccak256(self.rlp(chain_id).as_ref()).into() + } + + /// Gets the unsigned transaction's RLP encoding + pub fn rlp>(&self, chain_id: Option) -> 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 diff --git a/ethers-core/src/types/crypto/mod.rs b/ethers-core/src/types/crypto/mod.rs index 95d5a113..e7e1b4ee 100644 --- a/ethers-core/src/types/crypto/mod.rs +++ b/ethers-core/src/types/crypto/mod.rs @@ -2,4 +2,4 @@ mod keys; pub use keys::{PrivateKey, PublicKey, TxError}; mod signature; -pub use signature::Signature; +pub use signature::{Signature, SignatureError}; diff --git a/ethers-signers/Cargo.toml b/ethers-signers/Cargo.toml index 6d67aae6..3400aa3c 100644 --- a/ethers-signers/Cargo.toml +++ b/ethers-signers/Cargo.toml @@ -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"] diff --git a/ethers-signers/src/client.rs b/ethers-signers/src/client.rs index 098a2dac..e9f91369 100644 --- a/ethers-signers/src/client.rs +++ b/ethers-signers/src/client.rs @@ -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

, signer: S) -> Self { - let address = signer.address(); - Client { + pub async fn new(provider: Provider

, signer: S) -> Result { + 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>(&self, msg: T) -> Result { 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 { 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

) -> &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>(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 } diff --git a/ethers-signers/src/ledger/app.rs b/ethers-signers/src/ledger/app.rs new file mode 100644 index 00000000..9b7c85ad --- /dev/null +++ b/ethers-signers/src/ledger/app.rs @@ -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, + derivation: DerivationType, + pub chain_id: Option, +} + +impl LedgerEthereum { + /// Instantiate the application by acquiring a lock on the ledger device. + /// + /// # Notes + /// + pub async fn new( + derivation: DerivationType, + chain_id: Option, + ) -> Result { + 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 { + 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 { + 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::

()? + }; + + Ok(address) + } + + /// Returns the semver of the Ethereum ledger app + pub async fn version(&self) -> Result { + 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, + ) -> Result { + // 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>(&self, message: S) -> Result { + 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, + ) -> Result { + 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::>(); + 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 { + let derivation = derivation.to_string(); + let elements = derivation.split('/').skip(1).collect::>(); + 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::().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::>().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::::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(); + } +} diff --git a/ethers-signers/src/ledger/mod.rs b/ethers-signers/src/ledger/mod.rs new file mode 100644 index 00000000..ba6d0b47 --- /dev/null +++ b/ethers-signers/src/ledger/mod.rs @@ -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>( + &self, + message: S, + ) -> Result { + self.sign_message(message).await + } + + /// Signs the transaction + async fn sign_transaction( + &self, + message: TransactionRequest, + ) -> Result { + self.sign_tx(message, self.chain_id).await + } + + /// Returns the signer's Ethereum Address + async fn address(&self) -> Result { + self.get_address().await + } +} + +impl From for ClientError { + fn from(src: LedgerError) -> Self { + ClientError::SignerError(Box::new(src)) + } +} diff --git a/ethers-signers/src/ledger/types.rs b/ethers-signers/src/ledger/types.rs new file mode 100644 index 00000000..6284455b --- /dev/null +++ b/ethers-signers/src/ledger/types.rs @@ -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, +} diff --git a/ethers-signers/src/lib.rs b/ethers-signers/src/lib.rs index 10bf4e41..71d68030 100644 --- a/ethers-signers/src/lib.rs +++ b/ethers-signers/src/lib.rs @@ -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; /// Signs the hash of the provided message after prefixing it - fn sign_message>(&self, message: S) -> Signature; + async fn sign_message>( + &self, + message: S, + ) -> Result; /// Signs the transaction - fn sign_transaction(&self, message: TransactionRequest) -> Result; + async fn sign_transaction( + &self, + message: TransactionRequest, + ) -> Result; /// Returns the signer's Ethereum Address - fn address(&self) -> Address; + async fn address(&self) -> Result; } /// An HTTP client configured to work with ANY blockchain without replay protection diff --git a/ethers-signers/src/wallet.rs b/ethers-signers/src/wallet.rs index 1b239312..c168d79b 100644 --- a/ethers-signers/src/wallet.rs +++ b/ethers-signers/src/wallet.rs @@ -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> { /// 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, } +#[async_trait(?Send)] impl Signer for Wallet { type Error = TxError; - fn sign_message>(&self, message: S) -> Signature { - self.private_key.sign(message) + async fn sign_message>( + &self, + message: S, + ) -> Result { + Ok(self.private_key.sign(message)) } - fn sign_transaction(&self, tx: TransactionRequest) -> Result { + async fn sign_transaction(&self, tx: TransactionRequest) -> Result { self.private_key.sign_transaction(tx, self.chain_id) } - fn address(&self) -> Address { - self.address + async fn address(&self) -> Result { + Ok(self.address) } } diff --git a/ethers/examples/sign.rs b/ethers/examples/sign.rs index df3171c0..6c4d88b3 100644 --- a/ethers/examples/sign.rs +++ b/ethers/examples/sign.rs @@ -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(()) } diff --git a/ethers/tests/major_contracts.rs b/ethers/tests/major_contracts.rs index eb987638..5d6a9f6f 100644 --- a/ethers/tests/major_contracts.rs +++ b/ethers/tests/major_contracts.rs @@ -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" -);