429 lines
15 KiB
Rust
429 lines
15 KiB
Rust
#![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 home;
|
|
use std::{
|
|
convert::TryFrom,
|
|
env, fs,
|
|
io::{Read, Write},
|
|
path,
|
|
path::PathBuf,
|
|
};
|
|
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>,
|
|
cache_dir: PathBuf,
|
|
pub(crate) chain_id: u64,
|
|
pub(crate) address: Address,
|
|
}
|
|
|
|
const FIRMWARE_MIN_VERSION: &str = ">=2.4.2";
|
|
|
|
// https://docs.trezor.io/trezor-firmware/common/communication/sessions.html
|
|
const SESSION_ID_LENGTH: usize = 32;
|
|
const SESSION_FILE_NAME: &str = "trezor.session";
|
|
|
|
impl TrezorEthereum {
|
|
pub async fn new(
|
|
derivation: DerivationType,
|
|
chain_id: u64,
|
|
cache_dir: Option<PathBuf>,
|
|
) -> Result<Self, TrezorError> {
|
|
let cache_dir = (match cache_dir.or_else(home::home_dir) {
|
|
Some(path) => path,
|
|
None => match env::current_dir() {
|
|
Ok(path) => path,
|
|
Err(e) => return Err(TrezorError::CacheError(e.to_string())),
|
|
},
|
|
})
|
|
.join(".ethers-rs")
|
|
.join("trezor")
|
|
.join("cache");
|
|
|
|
let mut blank = Self {
|
|
derivation: derivation.clone(),
|
|
chain_id,
|
|
cache_dir,
|
|
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 get_cached_session(&self) -> Result<Option<Vec<u8>>, TrezorError> {
|
|
let mut session = [0; SESSION_ID_LENGTH];
|
|
|
|
if let Ok(mut file) = fs::File::open(self.cache_dir.join(SESSION_FILE_NAME)) {
|
|
file.read_exact(&mut session).map_err(|e| TrezorError::CacheError(e.to_string()))?;
|
|
Ok(Some(session.to_vec()))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
fn save_session(&mut self, session_id: Vec<u8>) -> Result<(), TrezorError> {
|
|
fs::create_dir_all(&self.cache_dir).map_err(|e| TrezorError::CacheError(e.to_string()))?;
|
|
|
|
let mut file = fs::File::create(self.cache_dir.join(SESSION_FILE_NAME))
|
|
.map_err(|e| TrezorError::CacheError(e.to_string()))?;
|
|
|
|
file.write_all(&session_id).map_err(|e| TrezorError::CacheError(e.to_string()))?;
|
|
|
|
self.session_id = session_id;
|
|
Ok(())
|
|
}
|
|
|
|
fn initate_session(&mut self) -> Result<(), TrezorError> {
|
|
let mut client = trezor_client::unique(false)?;
|
|
client.init_device(self.get_cached_session()?)?;
|
|
|
|
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.save_session(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, Some(PathBuf::from("randomdir")))
|
|
.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, None).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, None).await.unwrap();
|
|
|
|
// invalid data
|
|
let big_data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".to_string()+ &"ff".repeat(1032*2) + "aa").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_empty_txes() {
|
|
// Contract creation (empty `to`), requires data.
|
|
// To test without the data field, we need to specify a `to` address.
|
|
let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap();
|
|
{
|
|
let tx_req = Eip1559TransactionRequest::new()
|
|
.to("2ed7afa17473e17ac59908f088b4371d28585476".parse::<Address>().unwrap())
|
|
.into();
|
|
let tx = trezor.sign_transaction(&tx_req).await.unwrap();
|
|
}
|
|
{
|
|
let tx_req = TransactionRequest::new()
|
|
.to("2ed7afa17473e17ac59908f088b4371d28585476".parse::<Address>().unwrap())
|
|
.into();
|
|
let tx = trezor.sign_transaction(&tx_req).await.unwrap();
|
|
}
|
|
|
|
let data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap();
|
|
|
|
// Contract creation (empty `to`, with data) should show on the trezor device as:
|
|
// ` "0 Wei ETH
|
|
// ` new contract?"
|
|
let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap();
|
|
{
|
|
let tx_req = Eip1559TransactionRequest::new().data(data.clone()).into();
|
|
let tx = trezor.sign_transaction(&tx_req).await.unwrap();
|
|
}
|
|
{
|
|
let tx_req = TransactionRequest::new().data(data.clone()).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, None).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, None).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, None).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();
|
|
}
|
|
}
|