Implement RLP decoding for transactions (#805)
* Implement RLP decoding for transactions * set chain_id in fill_transaction
This commit is contained in:
parent
44499ae008
commit
01544ec4b7
|
@ -1,5 +1,7 @@
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
use crate::types::Address;
|
use crate::types::Address;
|
||||||
use rlp::{Encodable, RlpStream};
|
use rlp::{Decodable, Encodable, RlpStream};
|
||||||
use serde::{ser::Error as SerializationError, Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{ser::Error as SerializationError, Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
|
||||||
/// ENS name or Ethereum Address. Not RLP encoded/serialized if it's a name
|
/// ENS name or Ethereum Address. Not RLP encoded/serialized if it's a name
|
||||||
|
@ -29,6 +31,25 @@ impl Encodable for NameOrAddress {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Decodable for NameOrAddress {
|
||||||
|
fn decode(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
|
||||||
|
// An address (H160) is 20 bytes, so let's only accept 20 byte rlp string encodings.
|
||||||
|
if !rlp.is_data() {
|
||||||
|
return Err(rlp::DecoderError::RlpExpectedToBeData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the data needs to be 20 bytes long
|
||||||
|
match 20.cmp(&rlp.size()) {
|
||||||
|
Ordering::Less => Err(rlp::DecoderError::RlpIsTooShort),
|
||||||
|
Ordering::Greater => Err(rlp::DecoderError::RlpIsTooBig),
|
||||||
|
Ordering::Equal => {
|
||||||
|
let rlp_data = rlp.data()?;
|
||||||
|
Ok(NameOrAddress::Address(Address::from_slice(rlp_data)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<&str> for NameOrAddress {
|
impl From<&str> for NameOrAddress {
|
||||||
fn from(s: &str) -> Self {
|
fn from(s: &str) -> Self {
|
||||||
NameOrAddress::Name(s.to_owned())
|
NameOrAddress::Name(s.to_owned())
|
||||||
|
@ -71,6 +92,8 @@ impl<'de> Deserialize<'de> for NameOrAddress {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use rlp::Rlp;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -103,6 +126,20 @@ mod tests {
|
||||||
assert_eq!(rlp.as_raw(), expected.as_raw());
|
assert_eq!(rlp.as_raw(), expected.as_raw());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rlp_address_deserialized() {
|
||||||
|
let addr = "3dd6f334b732d23b51dfbee2070b40bbd1a97a8f".parse().unwrap();
|
||||||
|
let expected = NameOrAddress::Address(addr);
|
||||||
|
|
||||||
|
let mut rlp = RlpStream::new();
|
||||||
|
rlp.append(&addr);
|
||||||
|
let rlp_bytes = &rlp.out().freeze()[..];
|
||||||
|
let data = Rlp::new(rlp_bytes);
|
||||||
|
let name = NameOrAddress::decode(&data).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(name, expected);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn serde_name_not_serialized() {
|
fn serde_name_not_serialized() {
|
||||||
let name = NameOrAddress::Name("ens.eth".to_string());
|
let name = NameOrAddress::Name("ens.eth".to_string());
|
||||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
||||||
types::{Address, Bytes, NameOrAddress, Signature, H256, U256, U64},
|
types::{Address, Bytes, NameOrAddress, Signature, H256, U256, U64},
|
||||||
utils::keccak256,
|
utils::keccak256,
|
||||||
};
|
};
|
||||||
use rlp::RlpStream;
|
use rlp::{Decodable, DecoderError, RlpStream};
|
||||||
|
|
||||||
/// EIP-1559 transactions have 9 fields
|
/// EIP-1559 transactions have 9 fields
|
||||||
const NUM_TX_FIELDS: usize = 9;
|
const NUM_TX_FIELDS: usize = 9;
|
||||||
|
@ -57,6 +57,10 @@ pub struct Eip1559TransactionRequest {
|
||||||
/// baseFeePerGas and maxPriorityFeePerGas). The difference between maxFeePerGas and
|
/// baseFeePerGas and maxPriorityFeePerGas). The difference between maxFeePerGas and
|
||||||
/// baseFeePerGas + maxPriorityFeePerGas is “refunded” to the user.
|
/// baseFeePerGas + maxPriorityFeePerGas is “refunded” to the user.
|
||||||
pub max_fee_per_gas: Option<U256>,
|
pub max_fee_per_gas: Option<U256>,
|
||||||
|
|
||||||
|
#[serde(rename = "chainId", default, skip_serializing_if = "Option::is_none")]
|
||||||
|
/// Chain ID (None for mainnet)
|
||||||
|
pub chain_id: Option<U64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Eip1559TransactionRequest {
|
impl Eip1559TransactionRequest {
|
||||||
|
@ -130,25 +134,34 @@ impl Eip1559TransactionRequest {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the `chain_id` field in the transaction to the provided value
|
||||||
|
#[must_use]
|
||||||
|
pub fn chain_id<T: Into<U64>>(mut self, chain_id: T) -> Self {
|
||||||
|
self.chain_id = Some(chain_id.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Hashes the transaction's data with the provided chain id
|
/// Hashes the transaction's data with the provided chain id
|
||||||
pub fn sighash<T: Into<U64>>(&self, chain_id: T) -> H256 {
|
pub fn sighash(&self) -> H256 {
|
||||||
keccak256(self.rlp(chain_id).as_ref()).into()
|
keccak256(self.rlp().as_ref()).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the unsigned transaction's RLP encoding
|
/// Gets the unsigned transaction's RLP encoding
|
||||||
pub fn rlp<T: Into<U64>>(&self, chain_id: T) -> Bytes {
|
pub fn rlp(&self) -> Bytes {
|
||||||
let mut rlp = RlpStream::new();
|
let mut rlp = RlpStream::new();
|
||||||
rlp.begin_list(NUM_TX_FIELDS);
|
rlp.begin_list(NUM_TX_FIELDS);
|
||||||
self.rlp_base(chain_id, &mut rlp);
|
self.rlp_base(&mut rlp);
|
||||||
rlp.out().freeze().into()
|
rlp.out().freeze().into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Produces the RLP encoding of the transaction with the provided signature
|
/// Produces the RLP encoding of the transaction with the provided signature
|
||||||
pub fn rlp_signed<T: Into<U64>>(&self, chain_id: T, signature: &Signature) -> Bytes {
|
pub fn rlp_signed(&self, signature: &Signature) -> Bytes {
|
||||||
let mut rlp = RlpStream::new();
|
let mut rlp = RlpStream::new();
|
||||||
rlp.begin_unbounded_list();
|
rlp.begin_unbounded_list();
|
||||||
let chain_id = chain_id.into();
|
self.rlp_base(&mut rlp);
|
||||||
self.rlp_base(chain_id, &mut rlp);
|
|
||||||
|
// if the chain_id is none we assume mainnet and choose one
|
||||||
|
let chain_id = self.chain_id.unwrap_or_else(U64::one);
|
||||||
|
|
||||||
// append the signature
|
// append the signature
|
||||||
let v = normalize_v(signature.v, chain_id);
|
let v = normalize_v(signature.v, chain_id);
|
||||||
|
@ -159,8 +172,8 @@ impl Eip1559TransactionRequest {
|
||||||
rlp.out().freeze().into()
|
rlp.out().freeze().into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn rlp_base<T: Into<U64>>(&self, chain_id: T, rlp: &mut RlpStream) {
|
pub(crate) fn rlp_base(&self, rlp: &mut RlpStream) {
|
||||||
rlp.append(&chain_id.into());
|
rlp_opt(rlp, &self.chain_id);
|
||||||
rlp_opt(rlp, &self.nonce);
|
rlp_opt(rlp, &self.nonce);
|
||||||
rlp_opt(rlp, &self.max_priority_fee_per_gas);
|
rlp_opt(rlp, &self.max_priority_fee_per_gas);
|
||||||
rlp_opt(rlp, &self.max_fee_per_gas);
|
rlp_opt(rlp, &self.max_fee_per_gas);
|
||||||
|
@ -170,6 +183,44 @@ impl Eip1559TransactionRequest {
|
||||||
rlp_opt(rlp, &self.data.as_ref().map(|d| d.as_ref()));
|
rlp_opt(rlp, &self.data.as_ref().map(|d| d.as_ref()));
|
||||||
rlp.append(&self.access_list);
|
rlp.append(&self.access_list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decodes fields of the request starting at the RLP offset passed. Increments the offset for
|
||||||
|
/// each element parsed.
|
||||||
|
#[inline]
|
||||||
|
fn decode_base_rlp(&mut self, rlp: &rlp::Rlp, offset: &mut usize) -> Result<(), DecoderError> {
|
||||||
|
self.chain_id = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
self.nonce = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
self.max_priority_fee_per_gas = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
self.max_fee_per_gas = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
self.gas = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
self.to = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
self.value = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
let data = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?;
|
||||||
|
self.data = match data.len() {
|
||||||
|
0 => None,
|
||||||
|
_ => Some(Bytes::from(data.to_vec())),
|
||||||
|
};
|
||||||
|
*offset += 1;
|
||||||
|
self.access_list = rlp.val_at(*offset)?;
|
||||||
|
*offset += 1;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decodable for Eip1559TransactionRequest {
|
||||||
|
fn decode(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
|
||||||
|
let mut txn = Eip1559TransactionRequest::new();
|
||||||
|
let mut offset = 0;
|
||||||
|
txn.decode_base_rlp(rlp, &mut offset)?;
|
||||||
|
Ok(txn)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Eip1559TransactionRequest> for super::request::TransactionRequest {
|
impl From<Eip1559TransactionRequest> for super::request::TransactionRequest {
|
||||||
|
@ -188,6 +239,7 @@ impl From<Eip1559TransactionRequest> for super::request::TransactionRequest {
|
||||||
gateway_fee_recipient: None,
|
gateway_fee_recipient: None,
|
||||||
#[cfg(feature = "celo")]
|
#[cfg(feature = "celo")]
|
||||||
gateway_fee: None,
|
gateway_fee: None,
|
||||||
|
chain_id: tx.chain_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -162,6 +162,23 @@ impl TypedTransaction {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn chain_id(&self) -> Option<U64> {
|
||||||
|
match self {
|
||||||
|
Legacy(inner) => inner.chain_id,
|
||||||
|
Eip2930(inner) => inner.tx.chain_id,
|
||||||
|
Eip1559(inner) => inner.chain_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_chain_id<T: Into<U64>>(&mut self, chain_id: T) {
|
||||||
|
let chain_id = chain_id.into();
|
||||||
|
match self {
|
||||||
|
Legacy(inner) => inner.chain_id = Some(chain_id),
|
||||||
|
Eip2930(inner) => inner.tx.chain_id = Some(chain_id),
|
||||||
|
Eip1559(inner) => inner.chain_id = Some(chain_id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn data(&self) -> Option<&Bytes> {
|
pub fn data(&self) -> Option<&Bytes> {
|
||||||
match self {
|
match self {
|
||||||
Legacy(inner) => inner.data.as_ref(),
|
Legacy(inner) => inner.data.as_ref(),
|
||||||
|
@ -194,7 +211,7 @@ impl TypedTransaction {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rlp_signed<T: Into<U64>>(&self, chain_id: T, signature: &Signature) -> Bytes {
|
pub fn rlp_signed(&self, signature: &Signature) -> Bytes {
|
||||||
let mut encoded = vec![];
|
let mut encoded = vec![];
|
||||||
match self {
|
match self {
|
||||||
Legacy(ref tx) => {
|
Legacy(ref tx) => {
|
||||||
|
@ -202,44 +219,71 @@ impl TypedTransaction {
|
||||||
}
|
}
|
||||||
Eip2930(inner) => {
|
Eip2930(inner) => {
|
||||||
encoded.extend_from_slice(&[0x1]);
|
encoded.extend_from_slice(&[0x1]);
|
||||||
encoded.extend_from_slice(inner.rlp_signed(chain_id, signature).as_ref());
|
encoded.extend_from_slice(inner.rlp_signed(signature).as_ref());
|
||||||
}
|
}
|
||||||
Eip1559(inner) => {
|
Eip1559(inner) => {
|
||||||
encoded.extend_from_slice(&[0x2]);
|
encoded.extend_from_slice(&[0x2]);
|
||||||
encoded.extend_from_slice(inner.rlp_signed(chain_id, signature).as_ref());
|
encoded.extend_from_slice(inner.rlp_signed(signature).as_ref());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
encoded.into()
|
encoded.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rlp<T: Into<U64>>(&self, chain_id: T) -> Bytes {
|
pub fn rlp(&self) -> Bytes {
|
||||||
let chain_id = chain_id.into();
|
|
||||||
let mut encoded = vec![];
|
let mut encoded = vec![];
|
||||||
match self {
|
match self {
|
||||||
Legacy(inner) => {
|
Legacy(inner) => {
|
||||||
encoded.extend_from_slice(inner.rlp(chain_id).as_ref());
|
encoded.extend_from_slice(inner.rlp().as_ref());
|
||||||
}
|
}
|
||||||
Eip2930(inner) => {
|
Eip2930(inner) => {
|
||||||
encoded.extend_from_slice(&[0x1]);
|
encoded.extend_from_slice(&[0x1]);
|
||||||
encoded.extend_from_slice(inner.rlp(chain_id).as_ref());
|
encoded.extend_from_slice(inner.rlp().as_ref());
|
||||||
}
|
}
|
||||||
Eip1559(inner) => {
|
Eip1559(inner) => {
|
||||||
encoded.extend_from_slice(&[0x2]);
|
encoded.extend_from_slice(&[0x2]);
|
||||||
encoded.extend_from_slice(inner.rlp(chain_id).as_ref());
|
encoded.extend_from_slice(inner.rlp().as_ref());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
encoded.into()
|
encoded.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hashes the transaction's data with the provided chain id
|
/// Hashes the transaction's data. Does not double-RLP encode
|
||||||
/// Does not double-RLP encode
|
pub fn sighash(&self) -> H256 {
|
||||||
pub fn sighash<T: Into<U64>>(&self, chain_id: T) -> H256 {
|
let encoded = self.rlp();
|
||||||
let encoded = self.rlp(chain_id);
|
|
||||||
keccak256(encoded).into()
|
keccak256(encoded).into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a TypedTransaction directly from an rlp encoded byte stream
|
||||||
|
impl rlp::Decodable for TypedTransaction {
|
||||||
|
fn decode(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
|
||||||
|
let tx_type: Option<U64> = match rlp.is_data() {
|
||||||
|
true => Some(rlp.data().unwrap().into()),
|
||||||
|
false => None,
|
||||||
|
};
|
||||||
|
let rest = rlp::Rlp::new(
|
||||||
|
rlp.as_raw().get(1..).ok_or(rlp::DecoderError::Custom("no transaction payload"))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
match tx_type {
|
||||||
|
Some(x) if x == U64::from(1) => {
|
||||||
|
// EIP-2930 (0x01)
|
||||||
|
Ok(Self::Eip2930(Eip2930TransactionRequest::decode(&rest)?))
|
||||||
|
}
|
||||||
|
Some(x) if x == U64::from(2) => {
|
||||||
|
// EIP-1559 (0x02)
|
||||||
|
Ok(Self::Eip1559(Eip1559TransactionRequest::decode(&rest)?))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Legacy (0x00)
|
||||||
|
// use the original rlp
|
||||||
|
Ok(Self::Legacy(TransactionRequest::decode(rlp)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<TransactionRequest> for TypedTransaction {
|
impl From<TransactionRequest> for TypedTransaction {
|
||||||
fn from(src: TransactionRequest) -> TypedTransaction {
|
fn from(src: TransactionRequest) -> TypedTransaction {
|
||||||
TypedTransaction::Legacy(src)
|
TypedTransaction::Legacy(src)
|
||||||
|
@ -260,8 +304,11 @@ impl From<Eip1559TransactionRequest> for TypedTransaction {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use rlp::Decodable;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::types::{Address, U256};
|
use crate::types::{Address, U256};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn serde_legacy_tx() {
|
fn serde_legacy_tx() {
|
||||||
|
@ -276,4 +323,102 @@ mod tests {
|
||||||
let de: TransactionRequest = serde_json::from_str(&serialized).unwrap();
|
let de: TransactionRequest = serde_json::from_str(&serialized).unwrap();
|
||||||
assert_eq!(tx, TypedTransaction::Legacy(de));
|
assert_eq!(tx, TypedTransaction::Legacy(de));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_typed_tx_without_access_list() {
|
||||||
|
let tx: Eip1559TransactionRequest = serde_json::from_str(
|
||||||
|
r#"{
|
||||||
|
"gas": "0x186a0",
|
||||||
|
"maxFeePerGas": "0x77359400",
|
||||||
|
"maxPriorityFeePerGas": "0x77359400",
|
||||||
|
"data": "0x5544",
|
||||||
|
"nonce": "0x2",
|
||||||
|
"to": "0x96216849c49358B10257cb55b28eA603c874b05E",
|
||||||
|
"value": "0x5af3107a4000",
|
||||||
|
"type": "0x2",
|
||||||
|
"chainId": "0x539",
|
||||||
|
"accessList": [],
|
||||||
|
"v": "0x1",
|
||||||
|
"r": "0xc3000cd391f991169ebfd5d3b9e93c89d31a61c998a21b07a11dc6b9d66f8a8e",
|
||||||
|
"s": "0x22cfe8424b2fbd78b16c9911da1be2349027b0a3c40adf4b6459222323773f74"
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let envelope = TypedTransaction::Eip1559(tx);
|
||||||
|
|
||||||
|
let expected =
|
||||||
|
H256::from_str("0xa1ea3121940930f7e7b54506d80717f14c5163807951624c36354202a8bffda6")
|
||||||
|
.unwrap();
|
||||||
|
let actual = envelope.sighash();
|
||||||
|
assert_eq!(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_typed_tx() {
|
||||||
|
let tx: Eip1559TransactionRequest = serde_json::from_str(
|
||||||
|
r#"{
|
||||||
|
"gas": "0x186a0",
|
||||||
|
"maxFeePerGas": "0x77359400",
|
||||||
|
"maxPriorityFeePerGas": "0x77359400",
|
||||||
|
"data": "0x5544",
|
||||||
|
"nonce": "0x2",
|
||||||
|
"to": "0x96216849c49358B10257cb55b28eA603c874b05E",
|
||||||
|
"value": "0x5af3107a4000",
|
||||||
|
"type": "0x2",
|
||||||
|
"accessList": [
|
||||||
|
{
|
||||||
|
"address": "0x0000000000000000000000000000000000000001",
|
||||||
|
"storageKeys": [
|
||||||
|
"0x0100000000000000000000000000000000000000000000000000000000000000"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"chainId": "0x539",
|
||||||
|
"v": "0x1",
|
||||||
|
"r": "0xc3000cd391f991169ebfd5d3b9e93c89d31a61c998a21b07a11dc6b9d66f8a8e",
|
||||||
|
"s": "0x22cfe8424b2fbd78b16c9911da1be2349027b0a3c40adf4b6459222323773f74"
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let envelope = TypedTransaction::Eip1559(tx);
|
||||||
|
|
||||||
|
let expected =
|
||||||
|
H256::from_str("0x090b19818d9d087a49c3d2ecee4829ee4acea46089c1381ac5e588188627466d")
|
||||||
|
.unwrap();
|
||||||
|
let actual = envelope.sighash();
|
||||||
|
assert_eq!(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_typed_tx_decode() {
|
||||||
|
// this is the same transaction as the above test
|
||||||
|
let typed_tx_hex = hex::decode("02f86b8205390284773594008477359400830186a09496216849c49358b10257cb55b28ea603c874b05e865af3107a4000825544f838f7940000000000000000000000000000000000000001e1a00100000000000000000000000000000000000000000000000000000000000000").unwrap();
|
||||||
|
let tx_rlp = rlp::Rlp::new(typed_tx_hex.as_slice());
|
||||||
|
let actual_tx = TypedTransaction::decode(&tx_rlp).unwrap();
|
||||||
|
|
||||||
|
let expected =
|
||||||
|
H256::from_str("0x090b19818d9d087a49c3d2ecee4829ee4acea46089c1381ac5e588188627466d")
|
||||||
|
.unwrap();
|
||||||
|
let actual = actual_tx.sighash();
|
||||||
|
assert_eq!(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "celo"))]
|
||||||
|
#[test]
|
||||||
|
fn test_eip155_decode() {
|
||||||
|
let tx = TransactionRequest::new()
|
||||||
|
.nonce(9)
|
||||||
|
.to("3535353535353535353535353535353535353535".parse::<Address>().unwrap())
|
||||||
|
.value(1000000000000000000u64)
|
||||||
|
.gas_price(20000000000u64)
|
||||||
|
.gas(21000)
|
||||||
|
.chain_id(1);
|
||||||
|
|
||||||
|
let expected_hex = hex::decode("ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080").unwrap();
|
||||||
|
let expected_rlp = rlp::Rlp::new(expected_hex.as_slice());
|
||||||
|
let decoded_transaction = TypedTransaction::decode(&expected_rlp).unwrap();
|
||||||
|
assert_eq!(tx.sighash(), decoded_transaction.sighash());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use super::{normalize_v, request::TransactionRequest};
|
use super::{normalize_v, request::TransactionRequest};
|
||||||
use crate::types::{Address, Bytes, Signature, H256, U256, U64};
|
use crate::types::{Address, Bytes, Signature, H256, U256, U64};
|
||||||
|
|
||||||
use rlp::RlpStream;
|
use rlp::{Decodable, DecoderError, RlpStream};
|
||||||
use rlp_derive::{RlpEncodable, RlpEncodableWrapper};
|
use rlp_derive::{RlpDecodable, RlpDecodableWrapper, RlpEncodable, RlpEncodableWrapper};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
const NUM_EIP2930_FIELDS: usize = 8;
|
const NUM_EIP2930_FIELDS: usize = 8;
|
||||||
|
@ -10,7 +10,17 @@ const NUM_EIP2930_FIELDS: usize = 8;
|
||||||
/// Access list
|
/// Access list
|
||||||
// NB: Need to use `RlpEncodableWrapper` else we get an extra [] in the output
|
// NB: Need to use `RlpEncodableWrapper` else we get an extra [] in the output
|
||||||
// https://github.com/gakonst/ethers-rs/pull/353#discussion_r680683869
|
// https://github.com/gakonst/ethers-rs/pull/353#discussion_r680683869
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, RlpEncodableWrapper)]
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Default,
|
||||||
|
Clone,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Serialize,
|
||||||
|
Deserialize,
|
||||||
|
RlpEncodableWrapper,
|
||||||
|
RlpDecodableWrapper,
|
||||||
|
)]
|
||||||
pub struct AccessList(pub Vec<AccessListItem>);
|
pub struct AccessList(pub Vec<AccessListItem>);
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
@ -38,7 +48,9 @@ impl TransactionRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Access list item
|
/// Access list item
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, RlpEncodable)]
|
#[derive(
|
||||||
|
Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, RlpEncodable, RlpDecodable,
|
||||||
|
)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct AccessListItem {
|
pub struct AccessListItem {
|
||||||
/// Accessed address
|
/// Accessed address
|
||||||
|
@ -60,10 +72,12 @@ impl Eip2930TransactionRequest {
|
||||||
Self { tx, access_list }
|
Self { tx, access_list }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rlp<T: Into<U64>>(&self, chain_id: T) -> Bytes {
|
pub fn rlp(&self) -> Bytes {
|
||||||
let mut rlp = RlpStream::new();
|
let mut rlp = RlpStream::new();
|
||||||
rlp.begin_list(NUM_EIP2930_FIELDS);
|
rlp.begin_list(NUM_EIP2930_FIELDS);
|
||||||
rlp.append(&chain_id.into());
|
|
||||||
|
let chain_id = self.tx.chain_id.unwrap_or_else(U64::one);
|
||||||
|
rlp.append(&chain_id);
|
||||||
self.tx.rlp_base(&mut rlp);
|
self.tx.rlp_base(&mut rlp);
|
||||||
// append the access list in addition to the base rlp encoding
|
// append the access list in addition to the base rlp encoding
|
||||||
rlp.append(&self.access_list);
|
rlp.append(&self.access_list);
|
||||||
|
@ -72,11 +86,11 @@ impl Eip2930TransactionRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Produces the RLP encoding of the transaction with the provided signature
|
/// Produces the RLP encoding of the transaction with the provided signature
|
||||||
pub fn rlp_signed<T: Into<U64>>(&self, chain_id: T, signature: &Signature) -> Bytes {
|
pub fn rlp_signed(&self, signature: &Signature) -> Bytes {
|
||||||
let mut rlp = RlpStream::new();
|
let mut rlp = RlpStream::new();
|
||||||
rlp.begin_list(NUM_EIP2930_FIELDS + 3);
|
rlp.begin_list(NUM_EIP2930_FIELDS + 3);
|
||||||
|
|
||||||
let chain_id = chain_id.into();
|
let chain_id = self.tx.chain_id.unwrap_or_else(U64::one);
|
||||||
rlp.append(&chain_id);
|
rlp.append(&chain_id);
|
||||||
self.tx.rlp_base(&mut rlp);
|
self.tx.rlp_base(&mut rlp);
|
||||||
// append the access list in addition to the base rlp encoding
|
// append the access list in addition to the base rlp encoding
|
||||||
|
@ -89,10 +103,50 @@ impl Eip2930TransactionRequest {
|
||||||
rlp.append(&signature.s);
|
rlp.append(&signature.s);
|
||||||
rlp.out().freeze().into()
|
rlp.out().freeze().into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decodes fields based on the RLP offset passed
|
||||||
|
fn decode_base_rlp(&mut self, rlp: &rlp::Rlp, offset: &mut usize) -> Result<(), DecoderError> {
|
||||||
|
self.tx.chain_id = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
self.tx.nonce = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
self.tx.gas_price = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
self.tx.gas = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
|
||||||
|
self.tx.gas = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
self.tx.to = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
self.tx.value = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
let data = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?;
|
||||||
|
self.tx.data = match data.len() {
|
||||||
|
0 => None,
|
||||||
|
_ => Some(Bytes::from(data.to_vec())),
|
||||||
|
};
|
||||||
|
*offset += 1;
|
||||||
|
self.access_list = rlp.val_at(*offset)?;
|
||||||
|
*offset += 1;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a Eip2930TransactionRequest from a rlp encoded byte stream
|
||||||
|
impl Decodable for Eip2930TransactionRequest {
|
||||||
|
fn decode(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
|
||||||
|
let mut new_tx = Self::new(TransactionRequest::new(), AccessList::default());
|
||||||
|
let mut offset = 0;
|
||||||
|
new_tx.decode_base_rlp(rlp, &mut offset)?;
|
||||||
|
Ok(new_tx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::types::{transaction::eip2718::TypedTransaction, U256};
|
use crate::types::{transaction::eip2718::TypedTransaction, U256};
|
||||||
|
|
||||||
|
@ -110,14 +164,14 @@ mod tests {
|
||||||
.with_access_list(vec![])
|
.with_access_list(vec![])
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
let hash = tx.sighash(1);
|
let hash = tx.sighash();
|
||||||
let sig: Signature = "c9519f4f2b30335884581971573fadf60c6204f59a911df35ee8a540456b266032f1e8e2c5dd761f9e4f88f41c8310aeaba26a8bfcdacfedfa12ec3862d3752101".parse().unwrap();
|
let sig: Signature = "c9519f4f2b30335884581971573fadf60c6204f59a911df35ee8a540456b266032f1e8e2c5dd761f9e4f88f41c8310aeaba26a8bfcdacfedfa12ec3862d3752101".parse().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
hash,
|
hash,
|
||||||
"49b486f0ec0a60dfbbca2d30cb07c9e8ffb2a2ff41f29a1ab6737475f6ff69f3".parse().unwrap()
|
"49b486f0ec0a60dfbbca2d30cb07c9e8ffb2a2ff41f29a1ab6737475f6ff69f3".parse().unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
let enc = rlp::encode(&tx.rlp_signed(1, &sig).as_ref());
|
let enc = rlp::encode(&tx.rlp_signed(&sig).as_ref());
|
||||||
let expected = "b86601f8630103018261a894b94f5374fce5edbc8e2a8697c15331677e6ebf0b0a825544c001a0c9519f4f2b30335884581971573fadf60c6204f59a911df35ee8a540456b2660a032f1e8e2c5dd761f9e4f88f41c8310aeaba26a8bfcdacfedfa12ec3862d37521";
|
let expected = "b86601f8630103018261a894b94f5374fce5edbc8e2a8697c15331677e6ebf0b0a825544c001a0c9519f4f2b30335884581971573fadf60c6204f59a911df35ee8a540456b2660a032f1e8e2c5dd761f9e4f88f41c8310aeaba26a8bfcdacfedfa12ec3862d37521";
|
||||||
assert_eq!(hex::encode(&enc), expected);
|
assert_eq!(hex::encode(&enc), expected);
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,3 +33,44 @@ pub(crate) fn normalize_v(v: u64, chain_id: crate::types::U64) -> u64 {
|
||||||
v
|
v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// extracts the chainid from the signature v value based on EIP-155
|
||||||
|
pub(crate) fn extract_chain_id(v: u64) -> Option<crate::types::U64> {
|
||||||
|
// https://eips.ethereum.org/EIPS/eip-155
|
||||||
|
// if chainid is available, v = {0, 1} + CHAIN_ID * 2 + 35
|
||||||
|
if v >= 35 {
|
||||||
|
return Some(crate::types::U64::from((v - 35) >> 1))
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodes the signature portion of the RLP encoding based on the RLP offset passed.
|
||||||
|
/// Increments the offset for each element parsed.
|
||||||
|
#[inline]
|
||||||
|
fn decode_signature(
|
||||||
|
rlp: &rlp::Rlp,
|
||||||
|
offset: &mut usize,
|
||||||
|
) -> Result<super::Signature, rlp::DecoderError> {
|
||||||
|
let sig = super::Signature {
|
||||||
|
v: rlp.val_at(*offset)?,
|
||||||
|
r: rlp.val_at(*offset + 1)?,
|
||||||
|
s: rlp.val_at(*offset + 2)?,
|
||||||
|
};
|
||||||
|
*offset += 3;
|
||||||
|
Ok(sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::types::{transaction::rlp_opt, U64};
|
||||||
|
use rlp::RlpStream;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rlp_opt_none() {
|
||||||
|
let mut stream = RlpStream::new_list(1);
|
||||||
|
let empty_chainid: Option<U64> = None;
|
||||||
|
rlp_opt(&mut stream, &empty_chainid);
|
||||||
|
let out = stream.out();
|
||||||
|
assert_eq!(out, vec![0xc1, 0x80]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
//! Transaction types
|
//! Transaction types
|
||||||
use super::{rlp_opt, NUM_TX_FIELDS};
|
use super::{extract_chain_id, rlp_opt, NUM_TX_FIELDS};
|
||||||
use crate::{
|
use crate::{
|
||||||
types::{Address, Bytes, NameOrAddress, Signature, H256, U256, U64},
|
types::{Address, Bytes, NameOrAddress, Signature, H256, U256, U64},
|
||||||
utils::keccak256,
|
utils::keccak256,
|
||||||
};
|
};
|
||||||
|
|
||||||
use rlp::RlpStream;
|
use rlp::{Decodable, RlpStream};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Parameters for sending a transaction
|
/// Parameters for sending a transaction
|
||||||
|
@ -41,6 +41,10 @@ pub struct TransactionRequest {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub nonce: Option<U256>,
|
pub nonce: Option<U256>,
|
||||||
|
|
||||||
|
/// Chain ID (None for mainnet)
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub chain_id: Option<U64>,
|
||||||
|
|
||||||
///////////////// Celo-specific transaction fields /////////////////
|
///////////////// Celo-specific transaction fields /////////////////
|
||||||
/// The currency fees are paid in (None for native currency)
|
/// The currency fees are paid in (None for native currency)
|
||||||
#[cfg(feature = "celo")]
|
#[cfg(feature = "celo")]
|
||||||
|
@ -123,29 +127,49 @@ impl TransactionRequest {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hashes the transaction's data with the provided chain id
|
/// Sets the `chain_id` field in the transaction to the provided value
|
||||||
pub fn sighash<T: Into<U64>>(&self, chain_id: T) -> H256 {
|
#[must_use]
|
||||||
keccak256(self.rlp(chain_id).as_ref()).into()
|
pub fn chain_id<T: Into<U64>>(mut self, chain_id: T) -> Self {
|
||||||
|
self.chain_id = Some(chain_id.into());
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the unsigned transaction's RLP encoding
|
/// Hashes the transaction's data with the provided chain id
|
||||||
pub fn rlp<T: Into<U64>>(&self, chain_id: T) -> Bytes {
|
pub fn sighash(&self) -> H256 {
|
||||||
|
match self.chain_id {
|
||||||
|
Some(_) => keccak256(self.rlp().as_ref()).into(),
|
||||||
|
None => keccak256(self.rlp_unsigned().as_ref()).into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the transaction's RLP encoding, prepared with the chain_id and extra fields for
|
||||||
|
/// signing. Assumes the chainid exists.
|
||||||
|
pub fn rlp(&self) -> Bytes {
|
||||||
let mut rlp = RlpStream::new();
|
let mut rlp = RlpStream::new();
|
||||||
rlp.begin_list(NUM_TX_FIELDS);
|
rlp.begin_list(NUM_TX_FIELDS);
|
||||||
self.rlp_base(&mut rlp);
|
self.rlp_base(&mut rlp);
|
||||||
|
|
||||||
// Only hash the 3 extra fields when preparing the
|
// Only hash the 3 extra fields when preparing the
|
||||||
// data to sign if chain_id is present
|
// data to sign if chain_id is present
|
||||||
rlp.append(&chain_id.into());
|
rlp_opt(&mut rlp, &self.chain_id);
|
||||||
rlp.append(&0u8);
|
rlp.append(&0u8);
|
||||||
rlp.append(&0u8);
|
rlp.append(&0u8);
|
||||||
rlp.out().freeze().into()
|
rlp.out().freeze().into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets the unsigned transaction's RLP encoding
|
||||||
|
pub fn rlp_unsigned(&self) -> Bytes {
|
||||||
|
let mut rlp = RlpStream::new();
|
||||||
|
rlp.begin_list(NUM_TX_FIELDS - 3);
|
||||||
|
self.rlp_base(&mut rlp);
|
||||||
|
rlp.out().freeze().into()
|
||||||
|
}
|
||||||
|
|
||||||
/// Produces the RLP encoding of the transaction with the provided signature
|
/// Produces the RLP encoding of the transaction with the provided signature
|
||||||
pub fn rlp_signed(&self, signature: &Signature) -> Bytes {
|
pub fn rlp_signed(&self, signature: &Signature) -> Bytes {
|
||||||
let mut rlp = RlpStream::new();
|
let mut rlp = RlpStream::new();
|
||||||
rlp.begin_list(NUM_TX_FIELDS);
|
rlp.begin_list(NUM_TX_FIELDS);
|
||||||
|
|
||||||
self.rlp_base(&mut rlp);
|
self.rlp_base(&mut rlp);
|
||||||
|
|
||||||
// append the signature
|
// append the signature
|
||||||
|
@ -167,6 +191,86 @@ impl TransactionRequest {
|
||||||
rlp_opt(rlp, &self.value);
|
rlp_opt(rlp, &self.value);
|
||||||
rlp_opt(rlp, &self.data.as_ref().map(|d| d.as_ref()));
|
rlp_opt(rlp, &self.data.as_ref().map(|d| d.as_ref()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decodes the unsigned rlp, returning the transaction request and incrementing the counter
|
||||||
|
/// passed as we are traversing the rlp list.
|
||||||
|
fn decode_unsigned_rlp_base(
|
||||||
|
rlp: &rlp::Rlp,
|
||||||
|
offset: &mut usize,
|
||||||
|
) -> Result<Self, rlp::DecoderError> {
|
||||||
|
let mut txn = TransactionRequest::new();
|
||||||
|
txn.nonce = Some(rlp.at(*offset)?.as_val()?);
|
||||||
|
*offset += 1;
|
||||||
|
txn.gas_price = Some(rlp.at(*offset)?.as_val()?);
|
||||||
|
*offset += 1;
|
||||||
|
txn.gas = Some(rlp.at(*offset)?.as_val()?);
|
||||||
|
*offset += 1;
|
||||||
|
|
||||||
|
#[cfg(feature = "celo")]
|
||||||
|
{
|
||||||
|
txn.fee_currency = Some(rlp.at(*offset)?.as_val()?);
|
||||||
|
*offset += 1;
|
||||||
|
txn.gateway_fee_recipient = Some(rlp.at(*offset)?.as_val()?);
|
||||||
|
*offset += 1;
|
||||||
|
txn.gateway_fee = Some(rlp.at(*offset)?.as_val()?);
|
||||||
|
*offset += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
txn.to = Some(rlp.at(*offset)?.as_val()?);
|
||||||
|
*offset += 1;
|
||||||
|
txn.value = Some(rlp.at(*offset)?.as_val()?);
|
||||||
|
*offset += 1;
|
||||||
|
|
||||||
|
// finally we need to extract the data which will be encoded as another rlp
|
||||||
|
let txndata = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?;
|
||||||
|
txn.data = match txndata.len() {
|
||||||
|
0 => None,
|
||||||
|
_ => Some(Bytes::from(txndata.to_vec())),
|
||||||
|
};
|
||||||
|
*offset += 1;
|
||||||
|
Ok(txn)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodes RLP into a transaction.
|
||||||
|
pub fn decode_unsigned_rlp(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
|
||||||
|
let mut offset = 0;
|
||||||
|
let mut txn = Self::decode_unsigned_rlp_base(rlp, &mut offset)?;
|
||||||
|
|
||||||
|
// If a signed transaction is passed to this method, the chainid would be set to the v value
|
||||||
|
// of the signature.
|
||||||
|
if let Ok(chainid) = rlp.at(offset)?.as_val() {
|
||||||
|
txn.chain_id = Some(chainid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the last two elements so we return an error if a signed transaction is passed
|
||||||
|
let _first_zero: u8 = rlp.at(offset + 1)?.as_val()?;
|
||||||
|
let _second_zero: u8 = rlp.at(offset + 2)?.as_val()?;
|
||||||
|
Ok(txn)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodes the given RLP into a transaction, attempting to decode its signature as well.
|
||||||
|
pub fn decode_signed_rlp(rlp: &rlp::Rlp) -> Result<(Self, Signature), rlp::DecoderError> {
|
||||||
|
let mut offset = 0;
|
||||||
|
let mut txn = Self::decode_unsigned_rlp_base(rlp, &mut offset)?;
|
||||||
|
|
||||||
|
let v = rlp.at(offset)?.as_val()?;
|
||||||
|
// populate chainid from v
|
||||||
|
txn.chain_id = extract_chain_id(v);
|
||||||
|
offset += 1;
|
||||||
|
let r = rlp.at(offset)?.as_val()?;
|
||||||
|
offset += 1;
|
||||||
|
let s = rlp.at(offset)?.as_val()?;
|
||||||
|
|
||||||
|
let sig = Signature { r, s, v };
|
||||||
|
Ok((txn, sig))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decodable for TransactionRequest {
|
||||||
|
/// Decodes the given RLP into a transaction request, ignoring the signature if populated
|
||||||
|
fn decode(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
|
||||||
|
TransactionRequest::decode_unsigned_rlp(rlp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Separate impl block for the celo-specific fields
|
// Separate impl block for the celo-specific fields
|
||||||
|
@ -207,8 +311,46 @@ impl TransactionRequest {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[cfg(not(feature = "celo"))]
|
#[cfg(not(feature = "celo"))]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use crate::types::Signature;
|
||||||
|
use rlp::{Decodable, Rlp};
|
||||||
|
|
||||||
|
use super::{Address, TransactionRequest, U256, U64};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encode_decode_rlp() {
|
||||||
|
let tx = TransactionRequest::new()
|
||||||
|
.nonce(3)
|
||||||
|
.gas_price(1)
|
||||||
|
.gas(25000)
|
||||||
|
.to("b94f5374fce5edbc8e2a8697c15331677e6ebf0b".parse::<Address>().unwrap())
|
||||||
|
.value(10)
|
||||||
|
.data(vec![0x55, 0x44])
|
||||||
|
.chain_id(1);
|
||||||
|
|
||||||
|
// turn the rlp bytes encoding into a rlp stream and check that the decoding returns the
|
||||||
|
// same struct
|
||||||
|
let rlp_bytes = &tx.rlp().to_vec()[..];
|
||||||
|
let got_rlp = Rlp::new(rlp_bytes);
|
||||||
|
let txn_request = TransactionRequest::decode(&got_rlp).unwrap();
|
||||||
|
|
||||||
|
// We compare the sighash rather than the specific struct
|
||||||
|
assert_eq!(tx.sighash(), txn_request.sighash());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
// test data from https://github.com/ethereum/go-ethereum/blob/b1e72f7ea998ad662166bcf23705ca59cf81e925/core/types/transaction_test.go#L40
|
||||||
|
fn empty_sighash_check() {
|
||||||
|
let tx = TransactionRequest::new()
|
||||||
|
.nonce(0)
|
||||||
|
.to("095e7baea6a6c7c4c2dfeb977efac326af552d87".parse::<Address>().unwrap())
|
||||||
|
.value(0)
|
||||||
|
.gas(0)
|
||||||
|
.gas_price(0);
|
||||||
|
|
||||||
|
let expected_sighash = "c775b99e7ad12f50d819fcd602390467e28141316969f4b57f0626f74fe3b386";
|
||||||
|
let got_sighash = hex::encode(tx.sighash().as_bytes());
|
||||||
|
assert_eq!(expected_sighash, got_sighash);
|
||||||
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn decode_unsigned_transaction() {
|
fn decode_unsigned_transaction() {
|
||||||
let _res: TransactionRequest = serde_json::from_str(
|
let _res: TransactionRequest = serde_json::from_str(
|
||||||
|
@ -226,4 +368,81 @@ mod tests {
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_known_rlp_goerli() {
|
||||||
|
let tx = TransactionRequest::new()
|
||||||
|
.nonce(70272)
|
||||||
|
.from("fab2b4b677a4e104759d378ea25504862150256e".parse::<Address>().unwrap())
|
||||||
|
.to("d1f23226fb4d2b7d2f3bcdd99381b038de705a64".parse::<Address>().unwrap())
|
||||||
|
.value(0)
|
||||||
|
.gas_price(1940000007)
|
||||||
|
.gas(21000);
|
||||||
|
|
||||||
|
let expected_rlp = hex::decode("f866830112808473a20d0782520894d1f23226fb4d2b7d2f3bcdd99381b038de705a6480801ca04bc89d41c954168afb4cbd01fe2e0f9fe12e3aa4665eefcee8c4a208df044b5da05d410fd85a2e31870ea6d6af53fafc8e3c1ae1859717c863cac5cff40fee8da4").unwrap();
|
||||||
|
let (got_tx, _signature) =
|
||||||
|
TransactionRequest::decode_signed_rlp(&Rlp::new(&expected_rlp)).unwrap();
|
||||||
|
|
||||||
|
// intialization of TransactionRequests using new() uses the Default trait, so we just
|
||||||
|
// compare the sighash and signed encoding instead.
|
||||||
|
assert_eq!(got_tx.sighash(), tx.sighash());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_eip155_encode() {
|
||||||
|
let tx = TransactionRequest::new()
|
||||||
|
.nonce(9)
|
||||||
|
.to("3535353535353535353535353535353535353535".parse::<Address>().unwrap())
|
||||||
|
.value(1000000000000000000u64)
|
||||||
|
.gas_price(20000000000u64)
|
||||||
|
.gas(21000)
|
||||||
|
.chain_id(1);
|
||||||
|
|
||||||
|
let expected_rlp = hex::decode("ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080").unwrap();
|
||||||
|
assert_eq!(expected_rlp, tx.rlp().to_vec());
|
||||||
|
|
||||||
|
let expected_sighash =
|
||||||
|
hex::decode("daf5a779ae972f972197303d7b574746c7ef83eadac0f2791ad23db92e4c8e53")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(expected_sighash, tx.sighash().as_bytes().to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_eip155_decode() {
|
||||||
|
let tx = TransactionRequest::new()
|
||||||
|
.nonce(9)
|
||||||
|
.to("3535353535353535353535353535353535353535".parse::<Address>().unwrap())
|
||||||
|
.value(1000000000000000000u64)
|
||||||
|
.gas_price(20000000000u64)
|
||||||
|
.gas(21000)
|
||||||
|
.chain_id(1);
|
||||||
|
|
||||||
|
let expected_hex = hex::decode("ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080").unwrap();
|
||||||
|
let expected_rlp = rlp::Rlp::new(expected_hex.as_slice());
|
||||||
|
let decoded_transaction = TransactionRequest::decode(&expected_rlp).unwrap();
|
||||||
|
assert_eq!(tx, decoded_transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_eip155_decode_signed() {
|
||||||
|
let expected_signed_bytes = hex::decode("f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83").unwrap();
|
||||||
|
let expected_signed_rlp = rlp::Rlp::new(expected_signed_bytes.as_slice());
|
||||||
|
let (decoded_tx, decoded_sig) =
|
||||||
|
TransactionRequest::decode_signed_rlp(&expected_signed_rlp).unwrap();
|
||||||
|
|
||||||
|
let expected_sig = Signature {
|
||||||
|
v: 37,
|
||||||
|
r: U256::from_dec_str(
|
||||||
|
"18515461264373351373200002665853028612451056578545711640558177340181847433846",
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
s: U256::from_dec_str(
|
||||||
|
"46948507304638947509940763649030358759909902576025900602547168820602576006531",
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
};
|
||||||
|
assert_eq!(expected_sig, decoded_sig);
|
||||||
|
assert_eq!(decoded_tx.chain_id, Some(U64::from(1)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
//! Transaction types
|
//! Transaction types
|
||||||
use super::{eip2930::AccessList, normalize_v, rlp_opt};
|
use super::{decode_signature, eip2930::AccessList, normalize_v, rlp_opt};
|
||||||
use crate::{
|
use crate::{
|
||||||
types::{Address, Bloom, Bytes, Log, H256, U256, U64},
|
types::{Address, Bloom, Bytes, Log, H256, U256, U64},
|
||||||
utils::keccak256,
|
utils::keccak256,
|
||||||
};
|
};
|
||||||
use rlp::RlpStream;
|
use rlp::{Decodable, DecoderError, RlpStream};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Details of a signed transaction
|
/// Details of a signed transaction
|
||||||
|
@ -201,6 +201,152 @@ impl Transaction {
|
||||||
_ => rlp_bytes,
|
_ => rlp_bytes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decodes the Celo-specific metadata starting at the RLP offset passed.
|
||||||
|
/// Increments the offset for each element parsed.
|
||||||
|
#[cfg(feature = "celo")]
|
||||||
|
#[inline]
|
||||||
|
fn decode_celo_metadata(
|
||||||
|
&mut self,
|
||||||
|
rlp: &rlp::Rlp,
|
||||||
|
offset: &mut usize,
|
||||||
|
) -> Result<(), DecoderError> {
|
||||||
|
self.fee_currency = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
self.gateway_fee_recipient = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
self.gateway_fee = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodes fields of the type 2 transaction response starting at the RLP offset passed.
|
||||||
|
/// Increments the offset for each element parsed.
|
||||||
|
#[inline]
|
||||||
|
fn decode_base_eip1559(
|
||||||
|
&mut self,
|
||||||
|
rlp: &rlp::Rlp,
|
||||||
|
offset: &mut usize,
|
||||||
|
) -> Result<(), DecoderError> {
|
||||||
|
self.chain_id = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
self.nonce = rlp.val_at(*offset)?;
|
||||||
|
*offset += 1;
|
||||||
|
self.max_priority_fee_per_gas = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
self.max_fee_per_gas = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
self.gas = rlp.val_at(*offset)?;
|
||||||
|
*offset += 1;
|
||||||
|
self.to = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
self.value = rlp.val_at(*offset)?;
|
||||||
|
*offset += 1;
|
||||||
|
let input = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?;
|
||||||
|
self.input = Bytes::from(input.to_vec());
|
||||||
|
*offset += 1;
|
||||||
|
self.access_list = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodes fields of the type 1 transaction response based on the RLP offset passed.
|
||||||
|
/// Increments the offset for each element parsed.
|
||||||
|
fn decode_base_eip2930(
|
||||||
|
&mut self,
|
||||||
|
rlp: &rlp::Rlp,
|
||||||
|
offset: &mut usize,
|
||||||
|
) -> Result<(), DecoderError> {
|
||||||
|
self.chain_id = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
self.nonce = rlp.val_at(*offset)?;
|
||||||
|
*offset += 1;
|
||||||
|
self.gas_price = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
self.gas = rlp.val_at(*offset)?;
|
||||||
|
*offset += 1;
|
||||||
|
|
||||||
|
#[cfg(feature = "celo")]
|
||||||
|
self.decode_celo_metadata(rlp, offset)?;
|
||||||
|
|
||||||
|
self.gas = rlp.val_at(*offset)?;
|
||||||
|
*offset += 1;
|
||||||
|
self.to = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
self.value = rlp.val_at(*offset)?;
|
||||||
|
*offset += 1;
|
||||||
|
let input = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?;
|
||||||
|
self.input = Bytes::from(input.to_vec());
|
||||||
|
*offset += 1;
|
||||||
|
self.access_list = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodes a legacy transaction starting at the RLP offset passed.
|
||||||
|
/// Increments the offset for each element parsed.
|
||||||
|
#[inline]
|
||||||
|
fn decode_base_legacy(
|
||||||
|
&mut self,
|
||||||
|
rlp: &rlp::Rlp,
|
||||||
|
offset: &mut usize,
|
||||||
|
) -> Result<(), DecoderError> {
|
||||||
|
println!("are we a list {}", rlp.is_list());
|
||||||
|
self.nonce = rlp.val_at(*offset)?;
|
||||||
|
*offset += 1;
|
||||||
|
self.gas_price = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
self.gas = rlp.val_at(*offset)?;
|
||||||
|
*offset += 1;
|
||||||
|
|
||||||
|
#[cfg(feature = "celo")]
|
||||||
|
self.decode_celo_metadata(rlp, offset)?;
|
||||||
|
|
||||||
|
self.to = Some(rlp.val_at(*offset)?);
|
||||||
|
*offset += 1;
|
||||||
|
self.value = rlp.val_at(*offset)?;
|
||||||
|
*offset += 1;
|
||||||
|
let input = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?;
|
||||||
|
self.input = Bytes::from(input.to_vec());
|
||||||
|
*offset += 1;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a TransactionReceipt directly from an rlp encoded byte stream
|
||||||
|
impl Decodable for Transaction {
|
||||||
|
fn decode(rlp: &rlp::Rlp) -> Result<Self, DecoderError> {
|
||||||
|
let mut txn = Self::default();
|
||||||
|
// we can get the type from the first value
|
||||||
|
let mut offset = 0;
|
||||||
|
txn.transaction_type = Some(rlp.data().unwrap().into());
|
||||||
|
let rest = rlp::Rlp::new(
|
||||||
|
rlp.as_raw().get(1..).ok_or(DecoderError::Custom("no transaction payload"))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
match txn.transaction_type {
|
||||||
|
Some(x) if x == U64::from(1) => {
|
||||||
|
// EIP-2930 (0x01)
|
||||||
|
txn.decode_base_eip2930(&rest, &mut offset)?;
|
||||||
|
}
|
||||||
|
Some(x) if x == U64::from(2) => {
|
||||||
|
// EIP-1559 (0x02)
|
||||||
|
txn.decode_base_eip1559(&rest, &mut offset)?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Legacy (0x00)
|
||||||
|
txn.decode_base_legacy(&rest, &mut offset)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sig = decode_signature(&rest, &mut offset)?;
|
||||||
|
txn.r = sig.r;
|
||||||
|
txn.s = sig.s;
|
||||||
|
txn.v = sig.v.into();
|
||||||
|
|
||||||
|
Ok(txn)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// "Receipt" of an executed transaction: details of its execution.
|
/// "Receipt" of an executed transaction: details of its execution.
|
||||||
|
@ -401,4 +547,94 @@ mod tests {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rlp_london_goerli() {
|
||||||
|
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.rlp(),
|
||||||
|
Bytes::from(
|
||||||
|
hex::decode("02f86f05418459682f008459682f098301a0cf9411d7c2ab0d4aa26b7d8502f6a7ef6844908495c28084e5225381c001a01a8d7bef47f6155cbdf13d57107fc577fd52880fa2862b1a50d47641f8839419a03279bbf73fde76de83440d04b9d97f3809fec8617d3557ee40ac3e0edc391514").unwrap()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_rlp_london_goerli() {
|
||||||
|
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()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let rlp_bytes = hex::decode("02f86f05418459682f008459682f098301a0cf9411d7c2ab0d4aa26b7d8502f6a7ef6844908495c28084e5225381c001a01a8d7bef47f6155cbdf13d57107fc577fd52880fa2862b1a50d47641f8839419a03279bbf73fde76de83440d04b9d97f3809fec8617d3557ee40ac3e0edc391514").unwrap();
|
||||||
|
let decoded_transaction = Transaction::decode(&rlp::Rlp::new(&rlp_bytes)).unwrap();
|
||||||
|
|
||||||
|
// we compare hash because the hash depends on the rlp encoding
|
||||||
|
assert_eq!(decoded_transaction.hash(), tx.hash());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,6 +94,9 @@ pub enum SignerMiddlewareError<M: Middleware, S: Signer> {
|
||||||
/// Thrown if a signature is requested from a different address
|
/// Thrown if a signature is requested from a different address
|
||||||
#[error("specified from address is not signer")]
|
#[error("specified from address is not signer")]
|
||||||
WrongSigner,
|
WrongSigner,
|
||||||
|
/// Thrown if the signer's chain_id is different than the chain_id of the transaction
|
||||||
|
#[error("specified chain_id is different than the signer's chain_id")]
|
||||||
|
DifferentChainID,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions for locally signing transactions
|
// Helper functions for locally signing transactions
|
||||||
|
@ -113,11 +116,25 @@ where
|
||||||
&self,
|
&self,
|
||||||
tx: TypedTransaction,
|
tx: TypedTransaction,
|
||||||
) -> Result<Bytes, SignerMiddlewareError<M, S>> {
|
) -> Result<Bytes, SignerMiddlewareError<M, S>> {
|
||||||
|
// compare chain_id and use signer's chain_id if the tranasaction's chain_id is None,
|
||||||
|
// return an error if they are not consistent
|
||||||
|
let chain_id = self.signer.chain_id();
|
||||||
|
match tx.chain_id() {
|
||||||
|
Some(id) if id.as_u64() != chain_id => {
|
||||||
|
return Err(SignerMiddlewareError::DifferentChainID)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let mut tx = tx.clone();
|
||||||
|
tx.set_chain_id(chain_id);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
let signature =
|
let signature =
|
||||||
self.signer.sign_transaction(&tx).await.map_err(SignerMiddlewareError::SignerError)?;
|
self.signer.sign_transaction(&tx).await.map_err(SignerMiddlewareError::SignerError)?;
|
||||||
|
|
||||||
// Return the raw rlp-encoded signed transaction
|
// Return the raw rlp-encoded signed transaction
|
||||||
Ok(tx.rlp_signed(self.signer.chain_id(), &signature))
|
Ok(tx.rlp_signed(&signature))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the client's address
|
/// Returns the client's address
|
||||||
|
@ -190,6 +207,12 @@ where
|
||||||
};
|
};
|
||||||
tx.set_from(from);
|
tx.set_from(from);
|
||||||
|
|
||||||
|
// get the signer's chain_id if the transaction does not set it
|
||||||
|
let chain_id = self.signer.chain_id();
|
||||||
|
if tx.chain_id().is_none() {
|
||||||
|
tx.set_chain_id(chain_id);
|
||||||
|
}
|
||||||
|
|
||||||
let nonce = maybe(tx.nonce().cloned(), self.get_transaction_count(from, block)).await?;
|
let nonce = maybe(tx.nonce().cloned(), self.get_transaction_count(from, block)).await?;
|
||||||
tx.set_nonce(nonce);
|
tx.set_nonce(nonce);
|
||||||
self.inner()
|
self.inner()
|
||||||
|
@ -266,6 +289,7 @@ mod tests {
|
||||||
nonce: Some(0.into()),
|
nonce: Some(0.into()),
|
||||||
gas_price: Some(21_000_000_000u128.into()),
|
gas_price: Some(21_000_000_000u128.into()),
|
||||||
data: None,
|
data: None,
|
||||||
|
chain_id: None,
|
||||||
}
|
}
|
||||||
.into();
|
.into();
|
||||||
let chain_id = 1u64;
|
let chain_id = 1u64;
|
||||||
|
|
|
@ -36,7 +36,10 @@ async fn nonce_manager() {
|
||||||
let mut tx_hashes = Vec::new();
|
let mut tx_hashes = Vec::new();
|
||||||
for _ in 0..10 {
|
for _ in 0..10 {
|
||||||
let tx = provider
|
let tx = provider
|
||||||
.send_transaction(Eip1559TransactionRequest::new().to(address).value(100u64), None)
|
.send_transaction(
|
||||||
|
Eip1559TransactionRequest::new().to(address).value(100u64).chain_id(chain_id),
|
||||||
|
None,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
tx_hashes.push(*tx);
|
tx_hashes.push(*tx);
|
||||||
|
|
|
@ -39,7 +39,7 @@ async fn send_eth() {
|
||||||
let provider = SignerMiddleware::new(provider, wallet);
|
let provider = SignerMiddleware::new(provider, wallet);
|
||||||
|
|
||||||
// craft the transaction
|
// craft the transaction
|
||||||
let tx = TransactionRequest::new().to(wallet2.address()).value(10000);
|
let tx = TransactionRequest::new().to(wallet2.address()).value(10000).chain_id(chain_id);
|
||||||
|
|
||||||
let balance_before = provider.get_balance(provider.address(), None).await.unwrap();
|
let balance_before = provider.get_balance(provider.address(), None).await.unwrap();
|
||||||
|
|
||||||
|
|
|
@ -168,6 +168,7 @@ pub trait Middleware: Sync + Send + Debug {
|
||||||
/// 4. Estimate gas usage _with_ access lists
|
/// 4. Estimate gas usage _with_ access lists
|
||||||
/// 5. Enable access lists IFF they are cheaper
|
/// 5. Enable access lists IFF they are cheaper
|
||||||
/// 6. Poll and set legacy or 1559 gas prices
|
/// 6. Poll and set legacy or 1559 gas prices
|
||||||
|
/// 7. Set the chain_id with the provider's, if not already set
|
||||||
///
|
///
|
||||||
/// It does NOT set the nonce by default.
|
/// It does NOT set the nonce by default.
|
||||||
/// It MAY override the gas amount set by the user, if access lists are
|
/// It MAY override the gas amount set by the user, if access lists are
|
||||||
|
@ -223,7 +224,6 @@ pub trait Middleware: Sync + Send + Debug {
|
||||||
}
|
}
|
||||||
|
|
||||||
let gas_price = original.gas_price().expect("filled");
|
let gas_price = original.gas_price().expect("filled");
|
||||||
let chain_id = self.get_chainid().await?.low_u64();
|
|
||||||
let sign_futs: Vec<_> = (0..escalations)
|
let sign_futs: Vec<_> = (0..escalations)
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
let new_price = policy(gas_price, i);
|
let new_price = policy(gas_price, i);
|
||||||
|
@ -234,7 +234,7 @@ pub trait Middleware: Sync + Send + Debug {
|
||||||
.map(|req| async move {
|
.map(|req| async move {
|
||||||
self.sign_transaction(&req, self.default_sender().unwrap_or_default())
|
self.sign_transaction(&req, self.default_sender().unwrap_or_default())
|
||||||
.await
|
.await
|
||||||
.map(|sig| req.rlp_signed(chain_id, &sig))
|
.map(|sig| req.rlp_signed(&sig))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|
|
@ -235,7 +235,7 @@ impl<'a> super::Signer for AwsSigner<'a> {
|
||||||
|
|
||||||
#[instrument(err)]
|
#[instrument(err)]
|
||||||
async fn sign_transaction(&self, tx: &TypedTransaction) -> Result<EthSig, Self::Error> {
|
async fn sign_transaction(&self, tx: &TypedTransaction) -> Result<EthSig, Self::Error> {
|
||||||
let sighash = tx.sighash(self.chain_id);
|
let sighash = tx.sighash();
|
||||||
self.sign_digest_with_eip155(sighash).await
|
self.sign_digest_with_eip155(sighash).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -118,7 +118,7 @@ impl LedgerEthereum {
|
||||||
/// Signs an Ethereum transaction (requires confirmation on the ledger)
|
/// Signs an Ethereum transaction (requires confirmation on the ledger)
|
||||||
pub async fn sign_tx(&self, tx: &TypedTransaction) -> Result<Signature, LedgerError> {
|
pub async fn sign_tx(&self, tx: &TypedTransaction) -> Result<Signature, LedgerError> {
|
||||||
let mut payload = Self::path_to_bytes(&self.derivation);
|
let mut payload = Self::path_to_bytes(&self.derivation);
|
||||||
payload.extend_from_slice(tx.rlp(self.chain_id).as_ref());
|
payload.extend_from_slice(tx.rlp().as_ref());
|
||||||
self.sign_payload(INS::SIGN, payload).await
|
self.sign_payload(INS::SIGN, payload).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ use ethers_core::{
|
||||||
},
|
},
|
||||||
types::{
|
types::{
|
||||||
transaction::{eip2718::TypedTransaction, eip712::Eip712},
|
transaction::{eip2718::TypedTransaction, eip712::Eip712},
|
||||||
Address, Signature, H256, U256,
|
Address, Signature, H256, U256, U64,
|
||||||
},
|
},
|
||||||
utils::hash_message,
|
utils::hash_message,
|
||||||
};
|
};
|
||||||
|
@ -83,7 +83,23 @@ impl<D: Sync + Send + DigestSigner<Sha256Proxy, RecoverableSignature>> Signer fo
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn sign_transaction(&self, tx: &TypedTransaction) -> Result<Signature, Self::Error> {
|
async fn sign_transaction(&self, tx: &TypedTransaction) -> Result<Signature, Self::Error> {
|
||||||
Ok(self.sign_transaction_sync(tx))
|
let chain_id = tx.chain_id();
|
||||||
|
match chain_id {
|
||||||
|
Some(id) => {
|
||||||
|
if U64::from(self.chain_id) != id {
|
||||||
|
return Err(WalletError::InvalidTransactionError(
|
||||||
|
"transaction chain_id does not match the signer".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Ok(self.sign_transaction_sync(tx))
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// in the case we don't have a chain_id, let's use the signer chain id instead
|
||||||
|
let mut tx_with_chain = tx.clone();
|
||||||
|
tx_with_chain.set_chain_id(self.chain_id);
|
||||||
|
Ok(self.sign_transaction_sync(&tx_with_chain))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn sign_typed_data<T: Eip712 + Send + Sync>(
|
async fn sign_typed_data<T: Eip712 + Send + Sync>(
|
||||||
|
@ -119,7 +135,7 @@ impl<D: Sync + Send + DigestSigner<Sha256Proxy, RecoverableSignature>> Signer fo
|
||||||
impl<D: DigestSigner<Sha256Proxy, RecoverableSignature>> Wallet<D> {
|
impl<D: DigestSigner<Sha256Proxy, RecoverableSignature>> Wallet<D> {
|
||||||
/// Synchronously signs the provided transaction.
|
/// Synchronously signs the provided transaction.
|
||||||
pub fn sign_transaction_sync(&self, tx: &TypedTransaction) -> Signature {
|
pub fn sign_transaction_sync(&self, tx: &TypedTransaction) -> Signature {
|
||||||
let sighash = tx.sighash(self.chain_id);
|
let sighash = tx.sighash();
|
||||||
self.sign_hash(sighash, true)
|
self.sign_hash(sighash, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,8 @@ pub enum WalletError {
|
||||||
/// Error type from Eip712Error message
|
/// Error type from Eip712Error message
|
||||||
#[error("error encoding eip712 struct: {0:?}")]
|
#[error("error encoding eip712 struct: {0:?}")]
|
||||||
Eip712Error(String),
|
Eip712Error(String),
|
||||||
|
#[error("invalid transaction: {0:?}")]
|
||||||
|
InvalidTransactionError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for Wallet<SigningKey> {
|
impl Clone for Wallet<SigningKey> {
|
||||||
|
@ -142,8 +144,8 @@ impl FromStr for Wallet<SigningKey> {
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::Signer;
|
use crate::{Signer, TypedTransaction};
|
||||||
use ethers_core::types::Address;
|
use ethers_core::types::{Address, U64};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
@ -195,7 +197,7 @@ mod tests {
|
||||||
use ethers_core::types::TransactionRequest;
|
use ethers_core::types::TransactionRequest;
|
||||||
// retrieved test vector from:
|
// retrieved test vector from:
|
||||||
// https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction
|
// https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction
|
||||||
let tx = TransactionRequest {
|
let tx: TypedTransaction = TransactionRequest {
|
||||||
from: None,
|
from: None,
|
||||||
to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse::<Address>().unwrap().into()),
|
to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse::<Address>().unwrap().into()),
|
||||||
value: Some(1_000_000_000.into()),
|
value: Some(1_000_000_000.into()),
|
||||||
|
@ -203,16 +205,15 @@ mod tests {
|
||||||
nonce: Some(0.into()),
|
nonce: Some(0.into()),
|
||||||
gas_price: Some(21_000_000_000u128.into()),
|
gas_price: Some(21_000_000_000u128.into()),
|
||||||
data: None,
|
data: None,
|
||||||
|
chain_id: Some(U64::one()),
|
||||||
}
|
}
|
||||||
.into();
|
.into();
|
||||||
let chain_id = 1u64;
|
|
||||||
|
|
||||||
let wallet: Wallet<SigningKey> =
|
let wallet: Wallet<SigningKey> =
|
||||||
"4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318".parse().unwrap();
|
"4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318".parse().unwrap();
|
||||||
let wallet = wallet.with_chain_id(chain_id);
|
let wallet = wallet.with_chain_id(tx.chain_id().unwrap().as_u64());
|
||||||
|
|
||||||
let sig = wallet.sign_transaction(&tx).await.unwrap();
|
let sig = wallet.sign_transaction(&tx).await.unwrap();
|
||||||
let sighash = tx.sighash(chain_id);
|
let sighash = tx.sighash();
|
||||||
assert!(sig.verify(sighash, wallet.address).is_ok());
|
assert!(sig.verify(sighash, wallet.address).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue