ethers-rs/ethers-signers/src/ledger/app.rs

273 lines
8.7 KiB
Rust
Raw Normal View History

#![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::{
feat: typed txs provider / middleware changes (part 3) (#357) * feat(providers): add eth_feeHistory api * add access list * feat: fill transactions with access list / default sender info * feat: add helpers for operating on the txs enum * feat: send_transaction takes TypedTransaction now * fix(contract): temp wrap all contract txs as Legacy txs * feat(middleware): use TypedTransaction in Transformer * feat(signers): use TypedTransaction in Wallet/Ledger * feat(core): add helpers for setting typed tx fields * feat(signer): use typed transactions * fix(middleware): adjust nonce/gas/escalators for TypedTxs The GPO and the Escalators will throw an error if they are provided an EIP1559 transaction * fix(providers): ensure the correct account's nonce is filled * fix: add .into() to txs until we make the fn call more generic * Revert "fix: add .into() to txs until we make the fn call more generic" This reverts commit 04dc34b26d0e3f418ed3fc69ea35ad538b83dd50. * feat: generalize send_transaction interface * fix: only set the nonce manually in the Signer middleware * fix(transformer): fill the transaction after transformation * chore: fix compilation errors & lints * fix(signer): set the correct account's nonce * feat: make trace_call / call take TypedTransaction * fix: set sender to transaction in signer * chore: ethgasstation broke * chore: cargo fmt / lints * Fix(signer): pass the chain id * fix: final tx encoding fixes 1. Normalize v values for eip1559/2730 2. Make access lists mandatory for 1559 3. do not double-rlp on rlp_signed * fix: set access list only if available * test: check 1559 / 2930 txs * fix: do not prepend a 0 for Legacy txs & test * chore: code review comments * chore: fix aws signer signature
2021-08-09 00:31:11 +00:00
transaction::eip2718::TypedTransaction, 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<Ledger>,
derivation: DerivationType,
pub(crate) chain_id: u64,
pub(crate) address: Address,
}
impl LedgerEthereum {
/// Instantiate the application by acquiring a lock on the ledger device.
///
///
/// ```
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
/// 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<Self, LedgerError> {
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<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;
Self::get_address_with_path_transport(&transport, derivation).await
}
async fn get_address_with_path_transport(
transport: &Ledger,
derivation: &DerivationType,
) -> Result<Address, LedgerError> {
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<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 = 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)
feat: typed txs provider / middleware changes (part 3) (#357) * feat(providers): add eth_feeHistory api * add access list * feat: fill transactions with access list / default sender info * feat: add helpers for operating on the txs enum * feat: send_transaction takes TypedTransaction now * fix(contract): temp wrap all contract txs as Legacy txs * feat(middleware): use TypedTransaction in Transformer * feat(signers): use TypedTransaction in Wallet/Ledger * feat(core): add helpers for setting typed tx fields * feat(signer): use typed transactions * fix(middleware): adjust nonce/gas/escalators for TypedTxs The GPO and the Escalators will throw an error if they are provided an EIP1559 transaction * fix(providers): ensure the correct account's nonce is filled * fix: add .into() to txs until we make the fn call more generic * Revert "fix: add .into() to txs until we make the fn call more generic" This reverts commit 04dc34b26d0e3f418ed3fc69ea35ad538b83dd50. * feat: generalize send_transaction interface * fix: only set the nonce manually in the Signer middleware * fix(transformer): fill the transaction after transformation * chore: fix compilation errors & lints * fix(signer): set the correct account's nonce * feat: make trace_call / call take TypedTransaction * fix: set sender to transaction in signer * chore: ethgasstation broke * chore: cargo fmt / lints * Fix(signer): pass the chain id * fix: final tx encoding fixes 1. Normalize v values for eip1559/2730 2. Make access lists mandatory for 1559 3. do not double-rlp on rlp_signed * fix: set access list only if available * test: check 1559 / 2930 txs * fix: do not prepend a 0 for Legacy txs & test * chore: code review comments * chore: fix aws signer signature
2021-08-09 00:31:11 +00:00
pub async fn sign_tx(&self, tx: &TypedTransaction) -> Result<Signature, LedgerError> {
let mut payload = Self::path_to_bytes(&self.derivation);
payload.extend_from_slice(tx.rlp(self.chain_id).as_ref());
self.sign_payload(INS::SIGN, payload).await
}
/// 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.is_empty() {
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 = 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..]);
2021-07-05 11:03:38 +00:00
Ok(Signature { r, s, v })
}
// helper which converts a derivation path to bytes
fn path_to_bytes(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;
}
bytes.extend(&index.to_be_bytes());
}
bytes
}
}
#[cfg(all(test, feature = "ledger"))]
mod tests {
use super::*;
use crate::Signer;
use ethers::prelude::*;
use std::str::FromStr;
#[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::<Address>()
.unwrap())
.gas(1000000)
.gas_price(400e9 as u64)
.nonce(5)
.data(data)
feat: typed txs provider / middleware changes (part 3) (#357) * feat(providers): add eth_feeHistory api * add access list * feat: fill transactions with access list / default sender info * feat: add helpers for operating on the txs enum * feat: send_transaction takes TypedTransaction now * fix(contract): temp wrap all contract txs as Legacy txs * feat(middleware): use TypedTransaction in Transformer * feat(signers): use TypedTransaction in Wallet/Ledger * feat(core): add helpers for setting typed tx fields * feat(signer): use typed transactions * fix(middleware): adjust nonce/gas/escalators for TypedTxs The GPO and the Escalators will throw an error if they are provided an EIP1559 transaction * fix(providers): ensure the correct account's nonce is filled * fix: add .into() to txs until we make the fn call more generic * Revert "fix: add .into() to txs until we make the fn call more generic" This reverts commit 04dc34b26d0e3f418ed3fc69ea35ad538b83dd50. * feat: generalize send_transaction interface * fix: only set the nonce manually in the Signer middleware * fix(transformer): fill the transaction after transformation * chore: fix compilation errors & lints * fix(signer): set the correct account's nonce * feat: make trace_call / call take TypedTransaction * fix: set sender to transaction in signer * chore: ethgasstation broke * chore: cargo fmt / lints * Fix(signer): pass the chain id * fix: final tx encoding fixes 1. Normalize v values for eip1559/2730 2. Make access lists mandatory for 1559 3. do not double-rlp on rlp_signed * fix: set access list only if available * test: check 1559 / 2930 txs * fix: do not prepend a 0 for Legacy txs & test * chore: code review comments * chore: fix aws signer signature
2021-08-09 00:31:11 +00:00
.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();
}
}