feat: trezor support (#663)
* added trezor signer * linting * TrezorHDPath instead of HDPath * update trezor_client rev. added compatible hidapi backend * remove unused variables * keep track of the client session_id * add to Other derivation paths to trezor * remove commented macro * remove unnecessary drops * no ens * added TrezorTransaction that loads from TypedTransaction * enforce minimum firmware version * add big data test to trezor app * clippy * replace trezor-client git with published crate * change one char string to char * bump trezor-client, with ethereum feature only
This commit is contained in:
parent
4c677933ce
commit
6bf325dcab
|
@ -1333,6 +1333,7 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-futures",
|
"tracing-futures",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"trezor-client",
|
||||||
"yubihsm",
|
"yubihsm",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -2452,6 +2453,12 @@ dependencies = [
|
||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protobuf"
|
||||||
|
version = "2.25.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47c327e191621a2158159df97cdbc2e7074bb4e940275e35abf38eb3d2595754"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.10"
|
version = "1.0.10"
|
||||||
|
@ -3648,6 +3655,21 @@ dependencies = [
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "trezor-client"
|
||||||
|
version = "0.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff94fab279e0d429d762c289f9727f37a0f1b8207ea4795f09c11caad009046f"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"hex",
|
||||||
|
"hidapi",
|
||||||
|
"log",
|
||||||
|
"primitive-types",
|
||||||
|
"protobuf",
|
||||||
|
"rusb",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "try-lock"
|
name = "try-lock"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
|
|
|
@ -67,6 +67,7 @@ openssl = ["ethers-providers/openssl"]
|
||||||
dev-rpc = ["ethers-providers/dev-rpc"]
|
dev-rpc = ["ethers-providers/dev-rpc"]
|
||||||
## signers
|
## signers
|
||||||
ledger = ["ethers-signers/ledger"]
|
ledger = ["ethers-signers/ledger"]
|
||||||
|
trezor = ["ethers-signers/trezor"]
|
||||||
yubi = ["ethers-signers/yubi"]
|
yubi = ["ethers-signers/yubi"]
|
||||||
## contracts
|
## contracts
|
||||||
abigen = ["ethers-contract/abigen"]
|
abigen = ["ethers-contract/abigen"]
|
||||||
|
|
|
@ -28,6 +28,7 @@ yubihsm = { version = "0.39.0", features = ["secp256k1", "http", "usb"], optiona
|
||||||
futures-util = "0.3.18"
|
futures-util = "0.3.18"
|
||||||
futures-executor = "0.3.18"
|
futures-executor = "0.3.18"
|
||||||
semver = "1.0.4"
|
semver = "1.0.4"
|
||||||
|
trezor-client = { version = "0.0.3", optional = true, default-features = false, features = ["f_ethereum"] }
|
||||||
|
|
||||||
# aws
|
# aws
|
||||||
rusoto_core = { version = "0.47.0", optional = true }
|
rusoto_core = { version = "0.47.0", optional = true }
|
||||||
|
@ -56,3 +57,4 @@ celo = ["ethers-core/celo"]
|
||||||
ledger = ["coins-ledger"]
|
ledger = ["coins-ledger"]
|
||||||
yubi = ["yubihsm"]
|
yubi = ["yubihsm"]
|
||||||
aws = ["rusoto_core", "rusoto_kms", "tracing", "tracing-futures", "spki"]
|
aws = ["rusoto_core", "rusoto_kms", "tracing", "tracing-futures", "spki"]
|
||||||
|
trezor = ["trezor-client"]
|
||||||
|
|
|
@ -20,6 +20,14 @@ pub use ledger::{
|
||||||
types::{DerivationType as HDPath, LedgerError},
|
types::{DerivationType as HDPath, LedgerError},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "trezor")]
|
||||||
|
mod trezor;
|
||||||
|
#[cfg(feature = "trezor")]
|
||||||
|
pub use trezor::{
|
||||||
|
app::TrezorEthereum as Trezor,
|
||||||
|
types::{DerivationType as TrezorHDPath, TrezorError},
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(feature = "yubi")]
|
#[cfg(feature = "yubi")]
|
||||||
pub use yubihsm;
|
pub use yubihsm;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,339 @@
|
||||||
|
#![allow(unused)]
|
||||||
|
use trezor_client::client::{AccessListItem as Trezor_AccessListItem, Trezor};
|
||||||
|
|
||||||
|
use futures_executor::block_on;
|
||||||
|
use futures_util::lock::Mutex;
|
||||||
|
|
||||||
|
use ethers_core::{
|
||||||
|
types::{
|
||||||
|
transaction::{eip2718::TypedTransaction, eip712::Eip712},
|
||||||
|
Address, NameOrAddress, Signature, Transaction, TransactionRequest, TxHash, H256, U256,
|
||||||
|
},
|
||||||
|
utils::keccak256,
|
||||||
|
};
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use super::types::*;
|
||||||
|
|
||||||
|
/// A Trezor Ethereum App.
|
||||||
|
///
|
||||||
|
/// This is a simple wrapper around the [Trezor transport](Trezor)
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TrezorEthereum {
|
||||||
|
derivation: DerivationType,
|
||||||
|
session_id: Vec<u8>,
|
||||||
|
pub(crate) chain_id: u64,
|
||||||
|
pub(crate) address: Address,
|
||||||
|
}
|
||||||
|
|
||||||
|
const FIRMWARE_MIN_VERSION: &str = ">=2.4.2";
|
||||||
|
|
||||||
|
impl TrezorEthereum {
|
||||||
|
pub async fn new(derivation: DerivationType, chain_id: u64) -> Result<Self, TrezorError> {
|
||||||
|
let mut blank = Self {
|
||||||
|
derivation: derivation.clone(),
|
||||||
|
chain_id,
|
||||||
|
address: Address::from([0_u8; 20]),
|
||||||
|
session_id: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if reachable
|
||||||
|
blank.initate_session()?;
|
||||||
|
blank.address = blank.get_address_with_path(&derivation).await?;
|
||||||
|
Ok(blank)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_version(version: String) -> Result<(), TrezorError> {
|
||||||
|
let req = semver::VersionReq::parse(FIRMWARE_MIN_VERSION)?;
|
||||||
|
let version = semver::Version::parse(&version)?;
|
||||||
|
|
||||||
|
// Enforce firmware version is greater than FIRMWARE_MIN_VERSION
|
||||||
|
if !req.matches(&version) {
|
||||||
|
return Err(TrezorError::UnsupportedFirmwareVersion(FIRMWARE_MIN_VERSION.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initate_session(&mut self) -> Result<(), TrezorError> {
|
||||||
|
let mut client = trezor_client::unique(false)?;
|
||||||
|
client.init_device(None)?;
|
||||||
|
|
||||||
|
let features = client.features().ok_or(TrezorError::FeaturesError)?;
|
||||||
|
|
||||||
|
Self::check_version(format!(
|
||||||
|
"{}.{}.{}",
|
||||||
|
features.get_major_version(),
|
||||||
|
features.get_minor_version(),
|
||||||
|
features.get_patch_version()
|
||||||
|
))?;
|
||||||
|
|
||||||
|
self.session_id = features.get_session_id().to_vec();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// You need to drop(client) once you're done with it
|
||||||
|
fn get_client(&self, session_id: Vec<u8>) -> Result<Trezor, TrezorError> {
|
||||||
|
let mut client = trezor_client::unique(false)?;
|
||||||
|
client.init_device(Some(session_id))?;
|
||||||
|
Ok(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the account which corresponds to our derivation path
|
||||||
|
pub async fn get_address(&self) -> Result<Address, TrezorError> {
|
||||||
|
Ok(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, TrezorError> {
|
||||||
|
let mut client = self.get_client(self.session_id.clone())?;
|
||||||
|
|
||||||
|
let address_str = client.ethereum_get_address(Self::convert_path(derivation))?;
|
||||||
|
|
||||||
|
let mut address = [0; 20];
|
||||||
|
address.copy_from_slice(&hex::decode(&address_str[2..])?);
|
||||||
|
|
||||||
|
Ok(Address::from(address))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signs an Ethereum transaction (requires confirmation on the Trezor)
|
||||||
|
pub async fn sign_tx(&self, tx: &TypedTransaction) -> Result<Signature, TrezorError> {
|
||||||
|
let mut client = self.get_client(self.session_id.clone())?;
|
||||||
|
|
||||||
|
let arr_path = Self::convert_path(&self.derivation);
|
||||||
|
|
||||||
|
let transaction = TrezorTransaction::load(tx)?;
|
||||||
|
|
||||||
|
let signature = match tx {
|
||||||
|
TypedTransaction::Eip2930(_) | TypedTransaction::Legacy(_) => client.ethereum_sign_tx(
|
||||||
|
arr_path,
|
||||||
|
transaction.nonce,
|
||||||
|
transaction.gas_price,
|
||||||
|
transaction.gas,
|
||||||
|
transaction.to,
|
||||||
|
transaction.value,
|
||||||
|
transaction.data,
|
||||||
|
self.chain_id,
|
||||||
|
)?,
|
||||||
|
TypedTransaction::Eip1559(eip1559_tx) => client.ethereum_sign_eip1559_tx(
|
||||||
|
arr_path,
|
||||||
|
transaction.nonce,
|
||||||
|
transaction.gas,
|
||||||
|
transaction.to,
|
||||||
|
transaction.value,
|
||||||
|
transaction.data,
|
||||||
|
self.chain_id,
|
||||||
|
transaction.max_fee_per_gas,
|
||||||
|
transaction.max_priority_fee_per_gas,
|
||||||
|
transaction.access_list,
|
||||||
|
)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Signature { r: signature.r, s: signature.s, v: signature.v })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signs an ethereum personal message
|
||||||
|
pub async fn sign_message<S: AsRef<[u8]>>(&self, message: S) -> Result<Signature, TrezorError> {
|
||||||
|
let message = message.as_ref();
|
||||||
|
let mut client = self.get_client(self.session_id.clone())?;
|
||||||
|
let apath = Self::convert_path(&self.derivation);
|
||||||
|
|
||||||
|
let signature = client.ethereum_sign_message(message.into(), apath)?;
|
||||||
|
|
||||||
|
Ok(Signature { r: signature.r, s: signature.s, v: signature.v })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signs an EIP712 encoded domain separator and message
|
||||||
|
pub async fn sign_typed_struct<T>(&self, payload: &T) -> Result<Signature, TrezorError>
|
||||||
|
where
|
||||||
|
T: Eip712,
|
||||||
|
{
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper which converts a derivation path to [u32]
|
||||||
|
fn convert_path(derivation: &DerivationType) -> Vec<u32> {
|
||||||
|
let derivation = derivation.to_string();
|
||||||
|
let elements = derivation.split('/').skip(1).collect::<Vec<_>>();
|
||||||
|
let depth = elements.len();
|
||||||
|
|
||||||
|
let mut path = vec![];
|
||||||
|
for derivation_index in elements {
|
||||||
|
let hardened = derivation_index.contains('\'');
|
||||||
|
let mut index = derivation_index.replace('\'', "").parse::<u32>().unwrap();
|
||||||
|
if hardened {
|
||||||
|
index |= 0x80000000;
|
||||||
|
}
|
||||||
|
path.push(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "trezor"))]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::Signer;
|
||||||
|
use ethers_contract::EthAbiType;
|
||||||
|
use ethers_core::types::{
|
||||||
|
transaction::{
|
||||||
|
eip2930::{AccessList, AccessListItem},
|
||||||
|
eip712::Eip712,
|
||||||
|
},
|
||||||
|
Address, Eip1559TransactionRequest, TransactionRequest, I256, U256,
|
||||||
|
};
|
||||||
|
use ethers_derive_eip712::*;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eip712, EthAbiType)]
|
||||||
|
#[eip712(
|
||||||
|
name = "Eip712Test",
|
||||||
|
version = "1",
|
||||||
|
chain_id = 1,
|
||||||
|
verifying_contract = "0x0000000000000000000000000000000000000001",
|
||||||
|
salt = "eip712-test-75F0CCte"
|
||||||
|
)]
|
||||||
|
struct FooBar {
|
||||||
|
foo: I256,
|
||||||
|
bar: U256,
|
||||||
|
fizz: Vec<u8>,
|
||||||
|
buzz: [u8; 32],
|
||||||
|
far: String,
|
||||||
|
out: Address,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore]
|
||||||
|
// Replace this with your ETH addresses.
|
||||||
|
async fn test_get_address() {
|
||||||
|
// Instantiate it with the default trezor derivation path
|
||||||
|
let trezor = TrezorEthereum::new(DerivationType::TrezorLive(1), 1).await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
trezor.get_address().await.unwrap(),
|
||||||
|
"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
trezor.get_address_with_path(&DerivationType::TrezorLive(0)).await.unwrap(),
|
||||||
|
"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore]
|
||||||
|
async fn test_sign_tx() {
|
||||||
|
let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1).await.unwrap();
|
||||||
|
|
||||||
|
// approve uni v2 router 0xff
|
||||||
|
let data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap();
|
||||||
|
|
||||||
|
let tx_req = TransactionRequest::new()
|
||||||
|
.to("2ed7afa17473e17ac59908f088b4371d28585476".parse::<Address>().unwrap())
|
||||||
|
.gas(1000000)
|
||||||
|
.gas_price(400e9 as u64)
|
||||||
|
.nonce(5)
|
||||||
|
.data(data)
|
||||||
|
.value(ethers_core::utils::parse_ether(100).unwrap())
|
||||||
|
.into();
|
||||||
|
let tx = trezor.sign_transaction(&tx_req).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore]
|
||||||
|
async fn test_sign_big_data_tx() {
|
||||||
|
let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1).await.unwrap();
|
||||||
|
|
||||||
|
// invalid data
|
||||||
|
let big_data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".to_string()+ &"ff".repeat(1032*2) + &"aa".to_string()).unwrap();
|
||||||
|
let tx_req = TransactionRequest::new()
|
||||||
|
.to("2ed7afa17473e17ac59908f088b4371d28585476".parse::<Address>().unwrap())
|
||||||
|
.gas(1000000)
|
||||||
|
.gas_price(400e9 as u64)
|
||||||
|
.nonce(5)
|
||||||
|
.data(big_data)
|
||||||
|
.value(ethers_core::utils::parse_ether(100).unwrap())
|
||||||
|
.into();
|
||||||
|
let tx = trezor.sign_transaction(&tx_req).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore]
|
||||||
|
async fn test_sign_eip1559_tx() {
|
||||||
|
let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1).await.unwrap();
|
||||||
|
|
||||||
|
// approve uni v2 router 0xff
|
||||||
|
let data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap();
|
||||||
|
|
||||||
|
let lst = AccessList(vec![
|
||||||
|
AccessListItem {
|
||||||
|
address: "0x8ba1f109551bd432803012645ac136ddd64dba72".parse().unwrap(),
|
||||||
|
storage_keys: vec![
|
||||||
|
"0x0000000000000000000000000000000000000000000000000000000000000000"
|
||||||
|
.parse()
|
||||||
|
.unwrap(),
|
||||||
|
"0x0000000000000000000000000000000000000000000000000000000000000042"
|
||||||
|
.parse()
|
||||||
|
.unwrap(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
AccessListItem {
|
||||||
|
address: "0x2ed7afa17473e17ac59908f088b4371d28585476".parse().unwrap(),
|
||||||
|
storage_keys: vec![
|
||||||
|
"0x0000000000000000000000000000000000000000000000000000000000000000"
|
||||||
|
.parse()
|
||||||
|
.unwrap(),
|
||||||
|
"0x0000000000000000000000000000000000000000000000000000000000000042"
|
||||||
|
.parse()
|
||||||
|
.unwrap(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
let tx_req = Eip1559TransactionRequest::new()
|
||||||
|
.to("2ed7afa17473e17ac59908f088b4371d28585476".parse::<Address>().unwrap())
|
||||||
|
.gas(1000000)
|
||||||
|
.max_fee_per_gas(400e9 as u64)
|
||||||
|
.max_priority_fee_per_gas(400e9 as u64)
|
||||||
|
.nonce(5)
|
||||||
|
.data(data)
|
||||||
|
.access_list(lst)
|
||||||
|
.value(ethers_core::utils::parse_ether(100).unwrap())
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let tx = trezor.sign_transaction(&tx_req).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore]
|
||||||
|
async fn test_sign_message() {
|
||||||
|
let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1).await.unwrap();
|
||||||
|
let message = "hello world";
|
||||||
|
let sig = trezor.sign_message(message).await.unwrap();
|
||||||
|
let addr = trezor.get_address().await.unwrap();
|
||||||
|
sig.verify(message, addr).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore]
|
||||||
|
async fn test_sign_eip712_struct() {
|
||||||
|
let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1u64).await.unwrap();
|
||||||
|
|
||||||
|
let foo_bar = FooBar {
|
||||||
|
foo: I256::from(10),
|
||||||
|
bar: U256::from(20),
|
||||||
|
fizz: b"fizz".to_vec(),
|
||||||
|
buzz: keccak256("buzz"),
|
||||||
|
far: String::from("space"),
|
||||||
|
out: Address::from([0; 20]),
|
||||||
|
};
|
||||||
|
|
||||||
|
let sig = trezor.sign_typed_struct(&foo_bar).await.expect("failed to sign typed data");
|
||||||
|
let foo_bar_hash = foo_bar.encode_eip712().unwrap();
|
||||||
|
sig.verify(foo_bar_hash, trezor.address).unwrap();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
pub mod app;
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
|
use crate::Signer;
|
||||||
|
use app::TrezorEthereum;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use ethers_core::types::{
|
||||||
|
transaction::{eip2718::TypedTransaction, eip712::Eip712},
|
||||||
|
Address, Signature,
|
||||||
|
};
|
||||||
|
use types::TrezorError;
|
||||||
|
|
||||||
|
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||||
|
impl Signer for TrezorEthereum {
|
||||||
|
type Error = TrezorError;
|
||||||
|
|
||||||
|
/// 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: &TypedTransaction) -> Result<Signature, Self::Error> {
|
||||||
|
self.sign_tx(message).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signs a EIP712 derived struct
|
||||||
|
async fn sign_typed_data<T: Eip712 + Send + Sync>(
|
||||||
|
&self,
|
||||||
|
payload: &T,
|
||||||
|
) -> Result<Signature, Self::Error> {
|
||||||
|
self.sign_typed_struct(payload).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the signer's Ethereum Address
|
||||||
|
fn address(&self) -> Address {
|
||||||
|
self.address
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_chain_id<T: Into<u64>>(mut self, chain_id: T) -> Self {
|
||||||
|
self.chain_id = chain_id.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chain_id(&self) -> u64 {
|
||||||
|
self.chain_id
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
#![allow(clippy::upper_case_acronyms)]
|
||||||
|
//! Helpers for interacting with the Ethereum Trezor App
|
||||||
|
//! [Official Docs](https://github.com/TrezorHQ/app-ethereum/blob/master/doc/ethapp.asc)
|
||||||
|
use std::fmt;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use ethers_core::types::{transaction::eip2718::TypedTransaction, NameOrAddress, U256};
|
||||||
|
use trezor_client::client::AccessListItem as Trezor_AccessListItem;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
/// Trezor wallet type
|
||||||
|
pub enum DerivationType {
|
||||||
|
/// Trezor Live-generated HD path
|
||||||
|
TrezorLive(usize),
|
||||||
|
/// Any other path. Attention! Trezor by default forbids custom derivation paths
|
||||||
|
/// Run trezorctl set safety-checks prompt, to allow it
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for DerivationType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match self {
|
||||||
|
DerivationType::TrezorLive(index) => format!("m/44'/60'/{}'/0/0", index),
|
||||||
|
DerivationType::Other(inner) => inner.to_owned(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
/// Error when using the Trezor transport
|
||||||
|
pub enum TrezorError {
|
||||||
|
/// Underlying Trezor transport error
|
||||||
|
#[error(transparent)]
|
||||||
|
TrezorError(#[from] trezor_client::error::Error),
|
||||||
|
#[error("Trezor was not able to retrieve device features")]
|
||||||
|
FeaturesError,
|
||||||
|
#[error("Not able to unpack value for TrezorTransaction.")]
|
||||||
|
DataError,
|
||||||
|
#[error(transparent)]
|
||||||
|
/// Error when converting from a hex string
|
||||||
|
HexError(#[from] hex::FromHexError),
|
||||||
|
#[error(transparent)]
|
||||||
|
/// Error when converting a semver requirement
|
||||||
|
SemVerError(#[from] semver::Error),
|
||||||
|
/// Error when signing EIP712 struct with not compatible Trezor ETH app
|
||||||
|
#[error("Trezor ethereum app requires at least version: {0:?}")]
|
||||||
|
UnsupportedFirmwareVersion(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trezor Transaction Struct
|
||||||
|
pub struct TrezorTransaction {
|
||||||
|
pub nonce: Vec<u8>,
|
||||||
|
pub gas: Vec<u8>,
|
||||||
|
pub gas_price: Vec<u8>,
|
||||||
|
pub value: Vec<u8>,
|
||||||
|
pub to: String,
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
pub max_fee_per_gas: Vec<u8>,
|
||||||
|
pub max_priority_fee_per_gas: Vec<u8>,
|
||||||
|
pub access_list: Vec<Trezor_AccessListItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrezorTransaction {
|
||||||
|
fn to_trimmed_big_endian(_value: &U256) -> Vec<u8> {
|
||||||
|
let mut trimmed_value = [0_u8; 32];
|
||||||
|
_value.to_big_endian(&mut trimmed_value);
|
||||||
|
trimmed_value[_value.leading_zeros() as usize / 8..].to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(tx: &TypedTransaction) -> Result<Self, TrezorError> {
|
||||||
|
let to: String = match tx.to().ok_or(TrezorError::DataError)? {
|
||||||
|
NameOrAddress::Name(_) => unimplemented!(),
|
||||||
|
NameOrAddress::Address(value) => format!("0x{}", hex::encode(value)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let nonce = Self::to_trimmed_big_endian(tx.nonce().ok_or(TrezorError::DataError)?);
|
||||||
|
let gas = Self::to_trimmed_big_endian(tx.gas().ok_or(TrezorError::DataError)?);
|
||||||
|
let gas_price = Self::to_trimmed_big_endian(&tx.gas_price().ok_or(TrezorError::DataError)?);
|
||||||
|
let value = Self::to_trimmed_big_endian(tx.value().ok_or(TrezorError::DataError)?);
|
||||||
|
let data = tx.data().ok_or(TrezorError::DataError)?.to_vec();
|
||||||
|
|
||||||
|
match tx {
|
||||||
|
TypedTransaction::Eip2930(_) | TypedTransaction::Legacy(_) => Ok(Self {
|
||||||
|
nonce,
|
||||||
|
gas,
|
||||||
|
gas_price,
|
||||||
|
value,
|
||||||
|
to,
|
||||||
|
data,
|
||||||
|
max_fee_per_gas: vec![],
|
||||||
|
max_priority_fee_per_gas: vec![],
|
||||||
|
access_list: vec![],
|
||||||
|
}),
|
||||||
|
TypedTransaction::Eip1559(eip1559_tx) => {
|
||||||
|
let max_fee_per_gas = Self::to_trimmed_big_endian(
|
||||||
|
&eip1559_tx.max_fee_per_gas.ok_or(TrezorError::DataError)?,
|
||||||
|
);
|
||||||
|
let max_priority_fee_per_gas = Self::to_trimmed_big_endian(
|
||||||
|
&eip1559_tx.max_priority_fee_per_gas.ok_or(TrezorError::DataError)?,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut access_list: Vec<Trezor_AccessListItem> = Vec::new();
|
||||||
|
for item in &eip1559_tx.access_list.0 {
|
||||||
|
let address: String = format!("0x{}", hex::encode(item.address));
|
||||||
|
let mut storage_keys: Vec<Vec<u8>> = Vec::new();
|
||||||
|
|
||||||
|
for key in &item.storage_keys {
|
||||||
|
storage_keys.push(key.as_bytes().to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
access_list.push(Trezor_AccessListItem { address, storage_keys })
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
nonce,
|
||||||
|
gas,
|
||||||
|
gas_price,
|
||||||
|
value,
|
||||||
|
to,
|
||||||
|
data,
|
||||||
|
max_fee_per_gas,
|
||||||
|
max_priority_fee_per_gas,
|
||||||
|
access_list,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
#[tokio::main]
|
||||||
|
#[cfg(feature = "trezor")]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
use ethers::{prelude::*, utils::parse_ether};
|
||||||
|
|
||||||
|
// Connect over websockets
|
||||||
|
let provider = Provider::new(Ws::connect("ws://localhost:8545").await?);
|
||||||
|
// Instantiate the connection to trezor with Trezor Live derivation path and
|
||||||
|
// the wallet's index. You may also provide the chain_id.
|
||||||
|
// (here: mainnet) for EIP155 support.
|
||||||
|
// EIP1559 support
|
||||||
|
// No EIP712 support yet.
|
||||||
|
let trezor = Trezor::new(TrezorHDPath::TrezorLive(0), 1).await?;
|
||||||
|
let client = SignerMiddleware::new(provider, trezor);
|
||||||
|
|
||||||
|
// Create and broadcast a transaction (ENS disabled!)
|
||||||
|
// (this will require confirming the tx on the device)
|
||||||
|
let tx = TransactionRequest::new()
|
||||||
|
.to("0x99E2B13A8Ea8b00C68FA017ee250E98e870D8241")
|
||||||
|
.value(parse_ether(10)?);
|
||||||
|
let pending_tx = client.send_transaction(tx, None).await?;
|
||||||
|
|
||||||
|
// Get the receipt
|
||||||
|
let _receipt = pending_tx.confirmations(3).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "trezor"))]
|
||||||
|
fn main() {}
|
Loading…
Reference in New Issue