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:
parent
a3fa77744e
commit
95fcbe5240
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -2,4 +2,4 @@ mod keys;
|
|||
pub use keys::{PrivateKey, PublicKey, TxError};
|
||||
|
||||
mod signature;
|
||||
pub use signature::Signature;
|
||||
pub use signature::{Signature, SignatureError};
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue