#![allow(unused)] use coins_ledger::{ common::{APDUAnswer, APDUCommand, APDUData}, transports::{Ledger, LedgerAsync}, }; 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 Ledger Ethereum App. /// /// This is a simple wrapper around the [Ledger transport](Ledger) #[derive(Debug)] pub struct LedgerEthereum { transport: Mutex, derivation: DerivationType, pub(crate) chain_id: u64, pub(crate) address: Address, } const EIP712_MIN_VERSION: &str = ">=1.6.0"; impl LedgerEthereum { /// Instantiate the application by acquiring a lock on the ledger device. /// /// /// ``` /// # async fn foo() -> Result<(), Box> { /// use ethers_signers::{Ledger, HDPath}; /// /// let ledger = Ledger::new(HDPath::LedgerLive(0), 1).await?; /// # Ok(()) /// # } /// ``` pub async fn new(derivation: DerivationType, chain_id: u64) -> Result { let transport = Ledger::init().await?; let address = Self::get_address_with_path_transport(&transport, &derivation).await?; Ok(Self { transport: Mutex::new(transport), derivation, chain_id, address }) } /// 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 { 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 { let data = APDUData::new(&Self::path_to_bytes(derivation)); let transport = self.transport.lock().await; Self::get_address_with_path_transport(&transport, derivation).await } async fn get_address_with_path_transport( transport: &Ledger, derivation: &DerivationType, ) -> Result { let data = APDUData::new(&Self::path_to_bytes(derivation)); 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 = block_on(transport.exchange(&command))?; 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_str = &result[offset + 1..offset + 1 + result[offset] as usize]; let mut address = [0; 20]; address.copy_from_slice(&hex::decode(address_str)?); Address::from(address) }; Ok(address) } /// Returns the semver of the Ethereum ledger app pub async fn version(&self) -> Result { 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 = block_on(transport.exchange(&command))?; 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) pub async fn sign_tx(&self, tx: &TypedTransaction) -> Result { let mut tx_with_chain = tx.clone(); if tx_with_chain.chain_id().is_none() { // in the case we don't have a chain_id, let's use the signer chain id instead tx_with_chain.set_chain_id(self.chain_id); } let mut payload = Self::path_to_bytes(&self.derivation); payload.extend_from_slice(tx_with_chain.rlp().as_ref()); let mut signature = self.sign_payload(INS::SIGN, payload).await?; // modify `v` value of signature to match EIP-155 for chains with large chain ID // The logic is derived from Ledger's library // https://github.com/LedgerHQ/ledgerjs/blob/e78aac4327e78301b82ba58d63a72476ecb842fc/packages/hw-app-eth/src/Eth.ts#L300 let eip155_chain_id = self.chain_id * 2 + 35; if eip155_chain_id + 1 > 255 { let one_byte_chain_id = eip155_chain_id % 256; let ecc_parity = if signature.v > one_byte_chain_id { signature.v - one_byte_chain_id } else { one_byte_chain_id - signature.v }; signature.v = match tx { TypedTransaction::Eip2930(_) | TypedTransaction::Eip1559(_) => { if ecc_parity % 2 == 1 { 0 } else { 1 } } TypedTransaction::Legacy(_) => eip155_chain_id + ecc_parity, }; } Ok(signature) } /// Signs an ethereum personal message pub async fn sign_message>(&self, message: S) -> Result { 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 } /// Signs an EIP712 encoded domain separator and message pub async fn sign_typed_struct(&self, payload: &T) -> Result where T: Eip712, { // See comment for v1.6.0 requirement // https://github.com/LedgerHQ/app-ethereum/issues/105#issuecomment-765316999 let req = semver::VersionReq::parse(EIP712_MIN_VERSION)?; let version = semver::Version::parse(&self.version().await?)?; // Enforce app version is greater than EIP712_MIN_VERSION if !req.matches(&version) { return Err(LedgerError::UnsupportedAppVersion(EIP712_MIN_VERSION.to_string())) } let domain_separator = payload.domain_separator().map_err(|e| LedgerError::Eip712Error(e.to_string()))?; let struct_hash = payload.struct_hash().map_err(|e| LedgerError::Eip712Error(e.to_string()))?; let mut payload = Self::path_to_bytes(&self.derivation); payload.extend_from_slice(&domain_separator); payload.extend_from_slice(&struct_hash); self.sign_payload(INS::SIGN_ETH_EIP_712, payload).await } // Helper function for signing either transaction data, personal messages or EIP712 derived // structs pub async fn sign_payload( &self, command: INS, mut payload: Vec, ) -> Result { 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.is_empty() { let chunk_size = std::cmp::min(payload.len(), 255); let data = payload.drain(0..chunk_size).collect::>(); command.data = APDUData::new(&data); let answer = block_on(transport.exchange(&command))?; 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 = U256::from_big_endian(&result[1..33]); let s = U256::from_big_endian(&result[33..]); Ok(Signature { r, s, v }) } // helper which converts a derivation path to bytes fn path_to_bytes(derivation: &DerivationType) -> Vec { let derivation = derivation.to_string(); let elements = derivation.split('/').skip(1).collect::>(); 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::().unwrap(); if hardened { index |= 0x80000000; } bytes.extend(&index.to_be_bytes()); } bytes } } #[cfg(all(test, feature = "ledger"))] mod tests { use super::*; use crate::Signer; use ethers_contract::EthAbiType; use ethers_core::types::{ transaction::eip712::Eip712, Address, 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, 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 ledger derivation path let ledger = LedgerEthereum::new(DerivationType::LedgerLive(0), 1).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] #[ignore] async fn test_sign_tx() { let ledger = LedgerEthereum::new(DerivationType::LedgerLive(0), 1).await.unwrap(); // approve uni v2 router 0xff let data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(); let tx_req = TransactionRequest::new() .to("2ed7afa17473e17ac59908f088b4371d28585476".parse::
().unwrap()) .gas(1000000) .gas_price(400e9 as u64) .nonce(5) .data(data) .value(ethers_core::utils::parse_ether(100).unwrap()) .into(); let tx = ledger.sign_transaction(&tx_req).await.unwrap(); } #[tokio::test] #[ignore] async fn test_version() { let ledger = LedgerEthereum::new(DerivationType::LedgerLive(0), 1).await.unwrap(); let version = ledger.version().await.unwrap(); assert_eq!(version, "1.3.7"); } #[tokio::test] #[ignore] async fn test_sign_message() { let ledger = LedgerEthereum::new(DerivationType::Legacy(0), 1).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(); } #[tokio::test] #[ignore] async fn test_sign_eip712_struct() { let ledger = LedgerEthereum::new(DerivationType::LedgerLive(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 = ledger.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, ledger.address).unwrap(); } }