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
|
# so that we do not get rate limited by Etherscan (and it's free to generate as
|
||||||
# many as you want)
|
# many as you want)
|
||||||
env:
|
env:
|
||||||
ETHERSCAN_API_KEY: 76XKCZ4QKZYTJS8PBFUDZ292JBKEKS4974
|
ETHERSCAN_API_KEY: I5BXNZYP5GEDWFINGVEZKYIVU2695NPQZB
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
|
@ -47,7 +47,10 @@ jobs:
|
||||||
- name: cargo test (Celo)
|
- name: cargo test (Celo)
|
||||||
run: |
|
run: |
|
||||||
export PATH=$HOME/bin:$PATH
|
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
|
- name: cargo fmt
|
||||||
run: cargo fmt --all -- --check
|
run: cargo fmt --all -- --check
|
||||||
|
|
|
@ -91,9 +91,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.31"
|
version = "0.1.40"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "26c4f3195085c36ea8d24d32b2f828d23296a9370a28aa39d111f6f16bef9f3b"
|
checksum = "687c230d85c0a52504709705fc8a53e4a692b83a2184f03dae73e38e1e93a783"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -162,6 +162,17 @@ dependencies = [
|
||||||
"radium",
|
"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]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
|
@ -255,6 +266,29 @@ dependencies = [
|
||||||
"time",
|
"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]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
|
@ -264,6 +298,12 @@ dependencies = [
|
||||||
"cache-padded",
|
"cache-padded",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "constant_time_eq"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
|
@ -520,10 +560,13 @@ dependencies = [
|
||||||
name = "ethers-signers"
|
name = "ethers-signers"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"coins-ledger",
|
||||||
"ethers",
|
"ethers",
|
||||||
"ethers-core",
|
"ethers-core",
|
||||||
"ethers-providers",
|
"ethers-providers",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"rustc-hex",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
@ -746,6 +789,17 @@ dependencies = [
|
||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "hmac"
|
name = "hmac"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
|
@ -975,9 +1029,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.8"
|
version = "0.4.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
|
checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
@ -1070,6 +1124,19 @@ dependencies = [
|
||||||
"winapi 0.3.8",
|
"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]]
|
[[package]]
|
||||||
name = "num-integer"
|
name = "num-integer"
|
||||||
version = "0.1.43"
|
version = "0.1.43"
|
||||||
|
@ -1882,6 +1949,12 @@ version = "0.9.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
|
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "void"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "waker-fn"
|
name = "waker-fn"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
|
|
@ -266,6 +266,7 @@ where
|
||||||
pub fn at<T: Into<Address>>(&self, address: T) -> Self
|
pub fn at<T: Into<Address>>(&self, address: T) -> Self
|
||||||
where
|
where
|
||||||
P: Clone,
|
P: Clone,
|
||||||
|
S: Clone,
|
||||||
{
|
{
|
||||||
let mut this = self.clone();
|
let mut this = self.clone();
|
||||||
this.address = address.into();
|
this.address = address.into();
|
||||||
|
@ -278,6 +279,7 @@ where
|
||||||
pub fn connect(&self, client: Arc<Client<P, S>>) -> Self
|
pub fn connect(&self, client: Arc<Client<P, S>>) -> Self
|
||||||
where
|
where
|
||||||
P: Clone,
|
P: Clone,
|
||||||
|
S: Clone,
|
||||||
{
|
{
|
||||||
let mut this = self.clone();
|
let mut this = self.clone();
|
||||||
this.client = client;
|
this.client = client;
|
||||||
|
|
|
@ -138,6 +138,11 @@ impl TransactionRequest {
|
||||||
|
|
||||||
/// Hashes the transaction's data with the provided chain id
|
/// Hashes the transaction's data with the provided chain id
|
||||||
pub fn sighash<T: Into<U64>>(&self, chain_id: Option<T>) -> H256 {
|
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();
|
let mut rlp = RlpStream::new();
|
||||||
// "If [..] CHAIN_ID is available, then when computing the hash of a
|
// "If [..] CHAIN_ID is available, then when computing the hash of a
|
||||||
// transaction for the purposes of signing, instead of hashing only
|
// transaction for the purposes of signing, instead of hashing only
|
||||||
|
@ -162,7 +167,7 @@ impl TransactionRequest {
|
||||||
rlp.append(&0u8);
|
rlp.append(&0u8);
|
||||||
}
|
}
|
||||||
|
|
||||||
keccak256(rlp.out().as_ref()).into()
|
rlp.out().into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Produces the RLP encoding of the transaction with the provided signature
|
/// Produces the RLP encoding of the transaction with the provided signature
|
||||||
|
|
|
@ -2,4 +2,4 @@ mod keys;
|
||||||
pub use keys::{PrivateKey, PublicKey, TxError};
|
pub use keys::{PrivateKey, PublicKey, TxError};
|
||||||
|
|
||||||
mod signature;
|
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" }
|
ethers-providers = { version = "0.1.3", path = "../ethers-providers" }
|
||||||
thiserror = { version = "1.0.15", default-features = false }
|
thiserror = { version = "1.0.15", default-features = false }
|
||||||
futures-util = { version = "0.3.5", 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]
|
[dev-dependencies]
|
||||||
ethers = { version = "0.1.3", path = "../ethers" }
|
ethers = { version = "0.1.3", path = "../ethers" }
|
||||||
|
@ -28,3 +32,5 @@ serde_json = "1.0.55"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
celo = ["ethers-core/celo", "ethers-providers/celo"]
|
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"
|
/// let wallet: Wallet = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"
|
||||||
/// .parse()?;
|
/// .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
|
/// // since it derefs to `Provider`, we can just call any of the JSON-RPC API methods
|
||||||
/// let block = client.get_block(100u64).await?;
|
/// let block = client.get_block(100u64).await?;
|
||||||
|
@ -105,22 +105,22 @@ where
|
||||||
S: Signer,
|
S: Signer,
|
||||||
{
|
{
|
||||||
/// Creates a new client from the provider and signer.
|
/// Creates a new client from the provider and signer.
|
||||||
pub fn new(provider: Provider<P>, signer: S) -> Self {
|
pub async fn new(provider: Provider<P>, signer: S) -> Result<Self, ClientError> {
|
||||||
let address = signer.address();
|
let address = signer.address().await.map_err(Into::into)?;
|
||||||
Client {
|
Ok(Client {
|
||||||
provider,
|
provider,
|
||||||
signer: Some(signer),
|
signer: Some(signer),
|
||||||
address,
|
address,
|
||||||
gas_oracle: None,
|
gas_oracle: None,
|
||||||
nonce_manager: None,
|
nonce_manager: None,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Signs a message with the internal signer, or if none is present it will make a call to
|
/// 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.
|
/// the connected node's `eth_call` API.
|
||||||
pub async fn sign_message<T: Into<Bytes>>(&self, msg: T) -> Result<Signature, ClientError> {
|
pub async fn sign_message<T: Into<Bytes>>(&self, msg: T) -> Result<Signature, ClientError> {
|
||||||
Ok(if let Some(ref signer) = self.signer {
|
Ok(if let Some(ref signer) = self.signer {
|
||||||
signer.sign_message(msg.into())
|
signer.sign_message(msg.into()).await.map_err(Into::into)?
|
||||||
} else {
|
} else {
|
||||||
self.provider.sign(msg, &self.address()).await?
|
self.provider.sign(msg, &self.address()).await?
|
||||||
})
|
})
|
||||||
|
@ -173,7 +173,7 @@ where
|
||||||
|
|
||||||
async fn submit_transaction(&self, tx: TransactionRequest) -> Result<TxHash, ClientError> {
|
async fn submit_transaction(&self, tx: TransactionRequest) -> Result<TxHash, ClientError> {
|
||||||
Ok(if let Some(ref signer) = self.signer {
|
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?
|
self.provider.send_raw_transaction(&signed_tx).await?
|
||||||
} else {
|
} else {
|
||||||
self.provider.send_transaction(tx).await?
|
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
|
/// Sets the signer and returns a mutable reference to self so that it can be used in chained
|
||||||
/// calls.
|
/// calls.
|
||||||
///
|
|
||||||
/// Clones internally.
|
|
||||||
pub fn with_signer(&mut self, signer: S) -> &Self {
|
pub fn with_signer(&mut self, signer: S) -> &Self {
|
||||||
self.address = signer.address();
|
|
||||||
self.signer = Some(signer);
|
self.signer = Some(signer);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the provider and returns a mutable reference to self so that it can be used in chained
|
/// Sets the provider and returns a mutable reference to self so that it can be used in chained
|
||||||
/// calls.
|
/// calls.
|
||||||
///
|
|
||||||
/// Clones internally.
|
|
||||||
pub fn with_provider(&mut self, provider: Provider<P>) -> &Self {
|
pub fn with_provider(&mut self, provider: Provider<P>) -> &Self {
|
||||||
self.provider = provider;
|
self.provider = provider;
|
||||||
self
|
self
|
||||||
|
@ -278,21 +273,8 @@ where
|
||||||
|
|
||||||
/// Sets the address which will be used for interacting with the blockchain.
|
/// 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
|
/// Useful if no signer is set and you want to specify a default sender for
|
||||||
/// your transactions
|
/// your transactions or if you have changed the signer manually.
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
///
|
|
||||||
/// If the signer is Some. It is forbidden to switch the sender if a private
|
|
||||||
/// key is already specified.
|
|
||||||
pub fn with_sender<T: Into<Address>>(mut self, address: T) -> Self {
|
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.address = address.into();
|
||||||
self
|
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;
|
mod wallet;
|
||||||
pub use wallet::Wallet;
|
pub use wallet::Wallet;
|
||||||
|
|
||||||
|
#[cfg(feature = "ledger")]
|
||||||
|
pub mod ledger;
|
||||||
|
|
||||||
mod nonce_manager;
|
mod nonce_manager;
|
||||||
pub(crate) use nonce_manager::NonceManager;
|
pub(crate) use nonce_manager::NonceManager;
|
||||||
|
|
||||||
mod client;
|
mod client;
|
||||||
pub use client::{Client, ClientError};
|
pub use client::{Client, ClientError};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
use ethers_core::types::{Address, Signature, Transaction, TransactionRequest};
|
use ethers_core::types::{Address, Signature, Transaction, TransactionRequest};
|
||||||
use ethers_providers::Http;
|
use ethers_providers::Http;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
@ -53,17 +57,23 @@ use std::error::Error;
|
||||||
/// Trait for signing transactions and messages
|
/// Trait for signing transactions and messages
|
||||||
///
|
///
|
||||||
/// Implement this trait to support different signing modes, e.g. Ledger, hosted etc.
|
/// Implement this trait to support different signing modes, e.g. Ledger, hosted etc.
|
||||||
// TODO: We might need a `SignerAsync` trait for HSM use cases?
|
#[async_trait(?Send)]
|
||||||
pub trait Signer: Clone + Send + Sync {
|
pub trait Signer {
|
||||||
type Error: Error + Into<ClientError>;
|
type Error: Error + Into<ClientError>;
|
||||||
/// Signs the hash of the provided message after prefixing it
|
/// 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
|
/// 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
|
/// 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
|
/// 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},
|
types::{Address, PrivateKey, PublicKey, Signature, Transaction, TransactionRequest, TxError},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
@ -26,6 +27,7 @@ use std::str::FromStr;
|
||||||
/// use ethers_core::rand::thread_rng;
|
/// use ethers_core::rand::thread_rng;
|
||||||
/// use ethers_signers::{Wallet, Signer};
|
/// use ethers_signers::{Wallet, Signer};
|
||||||
///
|
///
|
||||||
|
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let wallet = Wallet::new(&mut thread_rng());
|
/// let wallet = Wallet::new(&mut thread_rng());
|
||||||
///
|
///
|
||||||
/// // Optionally, the wallet's chain id can be set, in order to use EIP-155
|
/// // 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
|
/// // The wallet can be used to sign messages
|
||||||
/// let message = b"hello";
|
/// let message = b"hello";
|
||||||
/// let signature = wallet.sign_message(message);
|
/// let signature = wallet.sign_message(message).await?;
|
||||||
/// assert_eq!(signature.recover(&message[..]).unwrap(), wallet.address())
|
/// assert_eq!(signature.recover(&message[..]).unwrap(), wallet.address());
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ## Connecting to a Provider
|
/// ## Connecting to a Provider
|
||||||
|
@ -75,19 +79,23 @@ pub struct Wallet {
|
||||||
chain_id: Option<u64>,
|
chain_id: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait(?Send)]
|
||||||
impl Signer for Wallet {
|
impl Signer for Wallet {
|
||||||
type Error = TxError;
|
type Error = TxError;
|
||||||
|
|
||||||
fn sign_message<S: AsRef<[u8]>>(&self, message: S) -> Signature {
|
async fn sign_message<S: Send + Sync + AsRef<[u8]>>(
|
||||||
self.private_key.sign(message)
|
&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)
|
self.private_key.sign_transaction(tx, self.chain_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn address(&self) -> Address {
|
async fn address(&self) -> Result<Address, Self::Error> {
|
||||||
self.address
|
Ok(self.address)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,19 @@
|
||||||
|
use anyhow::Result;
|
||||||
use ethers::prelude::*;
|
use ethers::prelude::*;
|
||||||
|
|
||||||
fn main() {
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
let message = "Some data";
|
let message = "Some data";
|
||||||
let wallet = Wallet::new(&mut rand::thread_rng());
|
let wallet = Wallet::new(&mut rand::thread_rng());
|
||||||
|
|
||||||
// sign a message
|
// sign a message
|
||||||
let signature = wallet.sign_message(message);
|
let signature = wallet.sign_message(message).await?;
|
||||||
println!("Produced signature {}", signature);
|
println!("Produced signature {}", signature);
|
||||||
|
|
||||||
// verify the signature
|
// verify the signature
|
||||||
signature.verify(message, wallet.address()).unwrap();
|
signature.verify(message, wallet.address()).unwrap();
|
||||||
|
|
||||||
println!("Verified signature produced by {:?}!", wallet.address());
|
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
|
// // This test exists to ensure that the abigen macro works "reasonably" well with popular contracts
|
||||||
use ethers::contract::abigen;
|
// use ethers::contract::abigen;
|
||||||
|
//
|
||||||
abigen!(
|
|
||||||
KeepBonding,
|
|
||||||
"etherscan:0x7137701e90C6a80B0dA36922cd83942b32A8fc95"
|
|
||||||
);
|
|
||||||
abigen!(cDAI, "etherscan:0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643");
|
|
||||||
abigen!(
|
|
||||||
Comptroller,
|
|
||||||
"etherscan:0x3d9819210a31b4961b30ef54be2aed79b9c9cd3b"
|
|
||||||
);
|
|
||||||
|
|
||||||
// https://github.com/vyperlang/vyper/issues/1931
|
|
||||||
// abigen!(
|
// abigen!(
|
||||||
// Curve,
|
// KeepBonding,
|
||||||
// "etherscan:0xa2b47e3d5c44877cca798226b7b8118f9bfb7a56"
|
// "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