diff --git a/CHANGELOG.md b/CHANGELOG.md index a1e41accb..e58a14a2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### Unreleased +- `Transaction::from` will default to `Address::zero()`. Add `recover_from` and + `recover_from_mut` methods for recovering the sender from signature, and also + setting the same on tx [1075](https://github.com/gakonst/ethers-rs/pull/1075). - Add Etherscan account API endpoints [939](https://github.com/gakonst/ethers-rs/pull/939) - Add FTM Mainet and testnet to parse method "try_from" from Chain.rs and add cronos mainet and testnet to "from_str" - Add FTM mainnet and testnet Multicall addresses [927](https://github.com/gakonst/ethers-rs/pull/927) diff --git a/ethers-core/src/types/signature.rs b/ethers-core/src/types/signature.rs index b3369bd64..e322a9874 100644 --- a/ethers-core/src/types/signature.rs +++ b/ethers-core/src/types/signature.rs @@ -60,7 +60,7 @@ pub struct Signature { pub r: U256, /// S Value pub s: U256, - /// V value in 'Electrum' notation. + /// V value pub v: u64, } diff --git a/ethers-core/src/types/transaction/eip1559.rs b/ethers-core/src/types/transaction/eip1559.rs index 1b0f9a47a..df42c7edc 100644 --- a/ethers-core/src/types/transaction/eip1559.rs +++ b/ethers-core/src/types/transaction/eip1559.rs @@ -1,6 +1,6 @@ use super::{eip2930::AccessList, normalize_v, rlp_opt}; use crate::{ - types::{Address, Bytes, NameOrAddress, Signature, H256, U256, U64}, + types::{Address, Bytes, NameOrAddress, Signature, Transaction, H256, U256, U64}, utils::keccak256, }; use rlp::{Decodable, DecoderError, RlpStream}; @@ -244,3 +244,20 @@ impl From for super::request::TransactionRequest { } } } + +impl From<&Transaction> for Eip1559TransactionRequest { + fn from(tx: &Transaction) -> Eip1559TransactionRequest { + Eip1559TransactionRequest { + from: Some(tx.from), + to: tx.to.map(NameOrAddress::Address), + gas: Some(tx.gas), + value: Some(tx.value), + data: Some(Bytes(tx.input.0.clone())), + nonce: Some(tx.nonce), + access_list: tx.access_list.clone().unwrap_or_default(), + max_priority_fee_per_gas: tx.max_priority_fee_per_gas, + max_fee_per_gas: tx.max_fee_per_gas, + chain_id: tx.chain_id.map(|x| U64::from(x.as_u64())), + } + } +} diff --git a/ethers-core/src/types/transaction/eip2718.rs b/ethers-core/src/types/transaction/eip2718.rs index 00a387b7d..86ffa64af 100644 --- a/ethers-core/src/types/transaction/eip2718.rs +++ b/ethers-core/src/types/transaction/eip2718.rs @@ -3,7 +3,9 @@ use super::{ eip2930::{AccessList, Eip2930TransactionRequest}, }; use crate::{ - types::{Address, Bytes, NameOrAddress, Signature, TransactionRequest, H256, U256, U64}, + types::{ + Address, Bytes, NameOrAddress, Signature, Transaction, TransactionRequest, H256, U256, U64, + }, utils::keccak256, }; use serde::{Deserialize, Serialize}; @@ -312,6 +314,28 @@ impl From for TypedTransaction { } } +impl From<&Transaction> for TypedTransaction { + fn from(tx: &Transaction) -> TypedTransaction { + match tx.transaction_type { + // EIP-2930 (0x01) + Some(x) if x == U64::from(1) => { + let request: Eip2930TransactionRequest = tx.into(); + request.into() + } + // EIP-1559 (0x02) + Some(x) if x == U64::from(2) => { + let request: Eip1559TransactionRequest = tx.into(); + request.into() + } + // Legacy (0x00) + _ => { + let request: TransactionRequest = tx.into(); + request.into() + } + } + } +} + #[cfg(test)] mod tests { use rlp::Decodable; diff --git a/ethers-core/src/types/transaction/eip2930.rs b/ethers-core/src/types/transaction/eip2930.rs index 5e78b75fc..1662382e1 100644 --- a/ethers-core/src/types/transaction/eip2930.rs +++ b/ethers-core/src/types/transaction/eip2930.rs @@ -1,5 +1,5 @@ use super::{normalize_v, request::TransactionRequest}; -use crate::types::{Address, Bytes, Signature, H256, U256, U64}; +use crate::types::{Address, Bytes, Signature, Transaction, H256, U256, U64}; use rlp::{Decodable, DecoderError, RlpStream}; use rlp_derive::{RlpDecodable, RlpDecodableWrapper, RlpEncodable, RlpEncodableWrapper}; @@ -144,6 +144,15 @@ impl Decodable for Eip2930TransactionRequest { } } +impl From<&Transaction> for Eip2930TransactionRequest { + fn from(tx: &Transaction) -> Eip2930TransactionRequest { + Eip2930TransactionRequest { + tx: tx.into(), + access_list: tx.access_list.clone().unwrap_or_default(), + } + } +} + #[cfg(test)] mod tests { diff --git a/ethers-core/src/types/transaction/request.rs b/ethers-core/src/types/transaction/request.rs index a581eb4cd..347eaa617 100644 --- a/ethers-core/src/types/transaction/request.rs +++ b/ethers-core/src/types/transaction/request.rs @@ -1,7 +1,7 @@ //! Transaction types use super::{extract_chain_id, rlp_opt, NUM_TX_FIELDS}; use crate::{ - types::{Address, Bytes, NameOrAddress, Signature, H256, U256, U64}, + types::{Address, Bytes, NameOrAddress, Signature, Transaction, H256, U256, U64}, utils::keccak256, }; @@ -274,6 +274,30 @@ impl Decodable for TransactionRequest { } } +impl From<&Transaction> for TransactionRequest { + fn from(tx: &Transaction) -> TransactionRequest { + TransactionRequest { + from: Some(tx.from), + to: tx.to.map(NameOrAddress::Address), + gas: Some(tx.gas), + gas_price: tx.gas_price, + value: Some(tx.value), + data: Some(Bytes(tx.input.0.clone())), + nonce: Some(tx.nonce), + chain_id: tx.chain_id.map(|x| U64::from(x.as_u64())), + + #[cfg(feature = "celo")] + fee_currency: tx.fee_currency, + + #[cfg(feature = "celo")] + gateway_fee_recipient: tx.gateway_fee_recipient, + + #[cfg(feature = "celo")] + gateway_fee: tx.gateway_fee, + } + } +} + // Separate impl block for the celo-specific fields #[cfg(feature = "celo")] impl TransactionRequest { diff --git a/ethers-core/src/types/transaction/response.rs b/ethers-core/src/types/transaction/response.rs index c59f87ccc..84b9dfde8 100644 --- a/ethers-core/src/types/transaction/response.rs +++ b/ethers-core/src/types/transaction/response.rs @@ -1,7 +1,9 @@ //! Transaction types -use super::{decode_signature, eip2930::AccessList, normalize_v, rlp_opt}; +use super::{ + decode_signature, eip2718::TypedTransaction, eip2930::AccessList, normalize_v, rlp_opt, +}; use crate::{ - types::{Address, Bloom, Bytes, Log, H256, U256, U64}, + types::{Address, Bloom, Bytes, Log, Signature, SignatureError, H256, U256, U64}, utils::keccak256, }; use rlp::{Decodable, DecoderError, RlpStream}; @@ -32,6 +34,7 @@ pub struct Transaction { pub transaction_index: Option, /// Sender + #[serde(default = "crate::types::Address::zero")] pub from: Address, /// Recipient (None when contract creation) @@ -311,6 +314,20 @@ impl Transaction { *offset += 1; Ok(()) } + + /// Recover the sender of the tx from signature + pub fn recover_from(&self) -> Result { + let signature = Signature { r: self.r, s: self.s, v: self.v.as_u64() }; + let typed_tx: TypedTransaction = self.into(); + signature.recover(typed_tx.sighash()) + } + + /// Recover the sender of the tx from signature and set the from field + pub fn recover_from_mut(&mut self) -> Result { + let from = self.recover_from()?; + self.from = from; + Ok(from) + } } /// Get a TransactionReceipt directly from an rlp encoded byte stream @@ -636,4 +653,46 @@ mod tests { // we compare hash because the hash depends on the rlp encoding assert_eq!(decoded_transaction.hash(), tx.hash()); } + + #[test] + fn recover_from() { + let tx = Transaction { + hash: H256::from_str( + "5e2fc091e15119c97722e9b63d5d32b043d077d834f377b91f80d32872c78109", + ) + .unwrap(), + nonce: 65.into(), + block_hash: Some( + H256::from_str("f43869e67c02c57d1f9a07bb897b54bec1cfa1feb704d91a2ee087566de5df2c") + .unwrap(), + ), + block_number: Some(6203173.into()), + transaction_index: Some(10.into()), + from: Address::from_str("e66b278fa9fbb181522f6916ec2f6d66ab846e04").unwrap(), + to: Some(Address::from_str("11d7c2ab0d4aa26b7d8502f6a7ef6844908495c2").unwrap()), + value: 0.into(), + gas_price: Some(1500000007.into()), + gas: 106703.into(), + input: hex::decode("e5225381").unwrap().into(), + v: 1.into(), + r: U256::from_str_radix( + "12010114865104992543118914714169554862963471200433926679648874237672573604889", + 10, + ) + .unwrap(), + s: U256::from_str_radix( + "22830728216401371437656932733690354795366167672037272747970692473382669718804", + 10, + ) + .unwrap(), + transaction_type: Some(2.into()), + access_list: Some(AccessList::default()), + max_priority_fee_per_gas: Some(1500000000.into()), + max_fee_per_gas: Some(1500000009.into()), + chain_id: Some(5.into()), + }; + + assert_eq!(tx.hash, tx.hash()); + assert_eq!(tx.from, tx.recover_from().unwrap()); + } }