ethers_signers/trezor/
app.rs

1#![allow(unused)]
2use trezor_client::client::{AccessListItem as Trezor_AccessListItem, Trezor};
3
4use futures_executor::block_on;
5use futures_util::lock::Mutex;
6
7use ethers_core::{
8    types::{
9        transaction::{eip2718::TypedTransaction, eip712::Eip712},
10        Address, NameOrAddress, Signature, Transaction, TransactionRequest, TxHash, H256, U256,
11    },
12    utils::keccak256,
13};
14use home;
15use std::{
16    convert::TryFrom,
17    env, fs,
18    io::{Read, Write},
19    path,
20    path::PathBuf,
21};
22use thiserror::Error;
23
24use super::types::*;
25
26/// A Trezor Ethereum App.
27///
28/// This is a simple wrapper around the [Trezor transport](Trezor)
29#[derive(Debug)]
30pub struct TrezorEthereum {
31    derivation: DerivationType,
32    session_id: Vec<u8>,
33    cache_dir: PathBuf,
34    pub(crate) chain_id: u64,
35    pub(crate) address: Address,
36}
37
38// we need firmware that supports EIP-1559 and EIP-712
39const FIRMWARE_1_MIN_VERSION: &str = ">=1.11.1";
40const FIRMWARE_2_MIN_VERSION: &str = ">=2.5.1";
41
42// https://docs.trezor.io/trezor-firmware/common/communication/sessions.html
43const SESSION_ID_LENGTH: usize = 32;
44const SESSION_FILE_NAME: &str = "trezor.session";
45
46impl TrezorEthereum {
47    pub async fn new(
48        derivation: DerivationType,
49        chain_id: u64,
50        cache_dir: Option<PathBuf>,
51    ) -> Result<Self, TrezorError> {
52        let cache_dir = (match cache_dir.or_else(home::home_dir) {
53            Some(path) => path,
54            None => match env::current_dir() {
55                Ok(path) => path,
56                Err(e) => return Err(TrezorError::CacheError(e.to_string())),
57            },
58        })
59        .join(".ethers-rs")
60        .join("trezor")
61        .join("cache");
62
63        let mut blank = Self {
64            derivation: derivation.clone(),
65            chain_id,
66            cache_dir,
67            address: Address::from([0_u8; 20]),
68            session_id: vec![],
69        };
70
71        // Check if reachable
72        blank.initate_session()?;
73        blank.address = blank.get_address_with_path(&derivation).await?;
74        Ok(blank)
75    }
76
77    fn check_version(version: String) -> Result<(), TrezorError> {
78        let version = semver::Version::parse(&version)?;
79
80        let min_version = match version.major {
81            1 => FIRMWARE_1_MIN_VERSION,
82            2 => FIRMWARE_2_MIN_VERSION,
83            // unknown major version, possibly newer models that we don't know about yet
84            // it's probably safe to assume they support EIP-1559 and EIP-712
85            _ => return Ok(()),
86        };
87
88        let req = semver::VersionReq::parse(min_version)?;
89        // Enforce firmware version is greater than "min_version"
90        if !req.matches(&version) {
91            return Err(TrezorError::UnsupportedFirmwareVersion(min_version.to_string()))
92        }
93
94        Ok(())
95    }
96
97    fn get_cached_session(&self) -> Result<Option<Vec<u8>>, TrezorError> {
98        let mut session = [0; SESSION_ID_LENGTH];
99
100        if let Ok(mut file) = fs::File::open(self.cache_dir.join(SESSION_FILE_NAME)) {
101            file.read_exact(&mut session).map_err(|e| TrezorError::CacheError(e.to_string()))?;
102            Ok(Some(session.to_vec()))
103        } else {
104            Ok(None)
105        }
106    }
107
108    fn save_session(&mut self, session_id: Vec<u8>) -> Result<(), TrezorError> {
109        fs::create_dir_all(&self.cache_dir).map_err(|e| TrezorError::CacheError(e.to_string()))?;
110
111        let mut file = fs::File::create(self.cache_dir.join(SESSION_FILE_NAME))
112            .map_err(|e| TrezorError::CacheError(e.to_string()))?;
113
114        file.write_all(&session_id).map_err(|e| TrezorError::CacheError(e.to_string()))?;
115
116        self.session_id = session_id;
117        Ok(())
118    }
119
120    fn initate_session(&mut self) -> Result<(), TrezorError> {
121        let mut client = trezor_client::unique(false)?;
122        client.init_device(self.get_cached_session()?)?;
123
124        let features = client.features().ok_or(TrezorError::FeaturesError)?;
125
126        Self::check_version(format!(
127            "{}.{}.{}",
128            features.major_version(),
129            features.minor_version(),
130            features.patch_version()
131        ))?;
132
133        self.save_session(features.session_id().to_vec())?;
134
135        Ok(())
136    }
137
138    /// You need to drop(client) once you're done with it
139    fn get_client(&self, session_id: Vec<u8>) -> Result<Trezor, TrezorError> {
140        let mut client = trezor_client::unique(false)?;
141        client.init_device(Some(session_id))?;
142        Ok(client)
143    }
144
145    /// Get the account which corresponds to our derivation path
146    pub async fn get_address(&self) -> Result<Address, TrezorError> {
147        self.get_address_with_path(&self.derivation).await
148    }
149
150    /// Gets the account which corresponds to the provided derivation path
151    pub async fn get_address_with_path(
152        &self,
153        derivation: &DerivationType,
154    ) -> Result<Address, TrezorError> {
155        let mut client = self.get_client(self.session_id.clone())?;
156        let address_str = client.ethereum_get_address(Self::convert_path(derivation))?;
157        let mut address_bytes = [0; 20];
158        hex::decode_to_slice(address_str, &mut address_bytes)?;
159        Ok(address_bytes.into())
160    }
161
162    /// Signs an Ethereum transaction (requires confirmation on the Trezor)
163    pub async fn sign_tx(&self, tx: &TypedTransaction) -> Result<Signature, TrezorError> {
164        let mut client = self.get_client(self.session_id.clone())?;
165
166        let arr_path = Self::convert_path(&self.derivation);
167
168        let transaction = TrezorTransaction::load(tx)?;
169
170        let chain_id = Some(tx.chain_id().map(|id| id.as_u64()).unwrap_or(self.chain_id));
171
172        let signature = match tx {
173            TypedTransaction::Eip2930(_) | TypedTransaction::Legacy(_) => client.ethereum_sign_tx(
174                arr_path,
175                transaction.nonce,
176                transaction.gas_price,
177                transaction.gas,
178                transaction.to,
179                transaction.value,
180                transaction.data,
181                chain_id,
182            )?,
183            TypedTransaction::Eip1559(eip1559_tx) => client.ethereum_sign_eip1559_tx(
184                arr_path,
185                transaction.nonce,
186                transaction.gas,
187                transaction.to,
188                transaction.value,
189                transaction.data,
190                chain_id,
191                transaction.max_fee_per_gas,
192                transaction.max_priority_fee_per_gas,
193                transaction.access_list,
194            )?,
195            #[cfg(feature = "optimism")]
196            TypedTransaction::DepositTransaction(tx) => {
197                trezor_client::client::Signature { r: [0; 32], s: [0; 32], v: 0 }
198            }
199        };
200
201        Ok(Signature {
202            r: U256::from_big_endian(&signature.r),
203            s: U256::from_big_endian(&signature.s),
204            v: signature.v,
205        })
206    }
207
208    /// Signs an ethereum personal message
209    pub async fn sign_message<S: AsRef<[u8]>>(&self, message: S) -> Result<Signature, TrezorError> {
210        let message = message.as_ref();
211        let mut client = self.get_client(self.session_id.clone())?;
212        let apath = Self::convert_path(&self.derivation);
213
214        let signature = client.ethereum_sign_message(message.into(), apath)?;
215
216        Ok(Signature {
217            r: U256::from_big_endian(&signature.r),
218            s: U256::from_big_endian(&signature.s),
219            v: signature.v,
220        })
221    }
222
223    /// Signs an EIP712 encoded domain separator and message
224    pub async fn sign_typed_struct<T>(&self, payload: &T) -> Result<Signature, TrezorError>
225    where
226        T: Eip712,
227    {
228        unimplemented!()
229    }
230
231    // helper which converts a derivation path to [u32]
232    fn convert_path(derivation: &DerivationType) -> Vec<u32> {
233        let derivation = derivation.to_string();
234        let elements = derivation.split('/').skip(1).collect::<Vec<_>>();
235        let depth = elements.len();
236
237        let mut path = vec![];
238        for derivation_index in elements {
239            let hardened = derivation_index.contains('\'');
240            let mut index = derivation_index.replace('\'', "").parse::<u32>().unwrap();
241            if hardened {
242                index |= 0x80000000;
243            }
244            path.push(index);
245        }
246
247        path
248    }
249}
250
251#[cfg(all(test, feature = "trezor"))]
252mod tests {
253    use super::*;
254    use crate::Signer;
255    use ethers_core::types::{
256        transaction::eip2930::{AccessList, AccessListItem},
257        Address, Eip1559TransactionRequest, TransactionRequest, I256, U256,
258    };
259    use std::str::FromStr;
260
261    #[tokio::test]
262    #[ignore]
263    // Replace this with your ETH addresses.
264    async fn test_get_address() {
265        // Instantiate it with the default trezor derivation path
266        let trezor =
267            TrezorEthereum::new(DerivationType::TrezorLive(1), 1, Some(PathBuf::from("randomdir")))
268                .await
269                .unwrap();
270        assert_eq!(
271            trezor.get_address().await.unwrap(),
272            "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap()
273        );
274        assert_eq!(
275            trezor.get_address_with_path(&DerivationType::TrezorLive(0)).await.unwrap(),
276            "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap()
277        );
278    }
279
280    #[tokio::test]
281    #[ignore]
282    async fn test_sign_tx() {
283        let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap();
284
285        // approve uni v2 router 0xff
286        let data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap();
287
288        let tx_req = TransactionRequest::new()
289            .to("2ed7afa17473e17ac59908f088b4371d28585476".parse::<Address>().unwrap())
290            .gas(1000000)
291            .gas_price(400e9 as u64)
292            .nonce(5)
293            .data(data)
294            .value(ethers_core::utils::parse_ether(100).unwrap())
295            .into();
296        let tx = trezor.sign_transaction(&tx_req).await.unwrap();
297    }
298
299    #[tokio::test]
300    #[ignore]
301    async fn test_sign_big_data_tx() {
302        let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap();
303
304        // invalid data
305        let big_data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".to_string()+ &"ff".repeat(1032*2) + "aa").unwrap();
306        let tx_req = TransactionRequest::new()
307            .to("2ed7afa17473e17ac59908f088b4371d28585476".parse::<Address>().unwrap())
308            .gas(1000000)
309            .gas_price(400e9 as u64)
310            .nonce(5)
311            .data(big_data)
312            .value(ethers_core::utils::parse_ether(100).unwrap())
313            .into();
314        let tx = trezor.sign_transaction(&tx_req).await.unwrap();
315    }
316
317    #[tokio::test]
318    #[ignore]
319    async fn test_sign_empty_txes() {
320        // Contract creation (empty `to`), requires data.
321        // To test without the data field, we need to specify a `to` address.
322        let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap();
323        {
324            let tx_req = Eip1559TransactionRequest::new()
325                .to("2ed7afa17473e17ac59908f088b4371d28585476".parse::<Address>().unwrap())
326                .into();
327            let tx = trezor.sign_transaction(&tx_req).await.unwrap();
328        }
329        {
330            let tx_req = TransactionRequest::new()
331                .to("2ed7afa17473e17ac59908f088b4371d28585476".parse::<Address>().unwrap())
332                .into();
333            let tx = trezor.sign_transaction(&tx_req).await.unwrap();
334        }
335
336        let data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap();
337
338        // Contract creation (empty `to`, with data) should show on the trezor device as:
339        //  ` "0 Wei ETH
340        //  ` new contract?"
341        let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap();
342        {
343            let tx_req = Eip1559TransactionRequest::new().data(data.clone()).into();
344            let tx = trezor.sign_transaction(&tx_req).await.unwrap();
345        }
346        {
347            let tx_req = TransactionRequest::new().data(data.clone()).into();
348            let tx = trezor.sign_transaction(&tx_req).await.unwrap();
349        }
350    }
351
352    #[tokio::test]
353    #[ignore]
354    async fn test_sign_eip1559_tx() {
355        let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap();
356
357        // approve uni v2 router 0xff
358        let data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap();
359
360        let lst = AccessList(vec![
361            AccessListItem {
362                address: "0x8ba1f109551bd432803012645ac136ddd64dba72".parse().unwrap(),
363                storage_keys: vec![
364                    "0x0000000000000000000000000000000000000000000000000000000000000000"
365                        .parse()
366                        .unwrap(),
367                    "0x0000000000000000000000000000000000000000000000000000000000000042"
368                        .parse()
369                        .unwrap(),
370                ],
371            },
372            AccessListItem {
373                address: "0x2ed7afa17473e17ac59908f088b4371d28585476".parse().unwrap(),
374                storage_keys: vec![
375                    "0x0000000000000000000000000000000000000000000000000000000000000000"
376                        .parse()
377                        .unwrap(),
378                    "0x0000000000000000000000000000000000000000000000000000000000000042"
379                        .parse()
380                        .unwrap(),
381                ],
382            },
383        ]);
384
385        let tx_req = Eip1559TransactionRequest::new()
386            .to("2ed7afa17473e17ac59908f088b4371d28585476".parse::<Address>().unwrap())
387            .gas(1000000)
388            .max_fee_per_gas(400e9 as u64)
389            .max_priority_fee_per_gas(400e9 as u64)
390            .nonce(5)
391            .data(data)
392            .access_list(lst)
393            .value(ethers_core::utils::parse_ether(100).unwrap())
394            .into();
395
396        let tx = trezor.sign_transaction(&tx_req).await.unwrap();
397    }
398
399    #[tokio::test]
400    #[ignore]
401    async fn test_sign_message() {
402        let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap();
403        let message = "hello world";
404        let sig = trezor.sign_message(message).await.unwrap();
405        let addr = trezor.get_address().await.unwrap();
406        sig.verify(message, addr).unwrap();
407    }
408}