Skip to content

Commit 314e911

Browse files
authored
Add EIP-2930 support (#179)
* Dynamically generate t_payload typespec * Add EIP-2930 support * Fix typedoc * Improve tests * Update CHANGELOG
1 parent 820e3f2 commit 314e911

File tree

4 files changed

+195
-25
lines changed

4 files changed

+195
-25
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
- Support [EIP-1191](https://eips.ethereum.org/EIPS/eip-1191): Add chain id to mixed-case checksum address encoding
88
- Add [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844) transaction support
9+
- Add [EIP-2930](https://eips.ethereum.org/EIPS/eip-2930) transaction support
910

1011
## v0.6.1 (2025-01-02)
1112

lib/ethers/transaction.ex

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,33 @@ defmodule Ethers.Transaction do
99
"""
1010

1111
alias Ethers.Transaction.Eip1559
12+
alias Ethers.Transaction.Eip2930
1213
alias Ethers.Transaction.Eip4844
1314
alias Ethers.Transaction.Legacy
1415
alias Ethers.Transaction.Protocol, as: TxProtocol
1516
alias Ethers.Transaction.Signed
1617
alias Ethers.Utils
1718

19+
@default_transaction_types [Eip1559, Eip2930, Eip4844, Legacy]
20+
21+
@transaction_types Application.compile_env(
22+
:ethers,
23+
:transaction_types,
24+
@default_transaction_types
25+
)
26+
27+
@default_transaction_type Eip1559
28+
29+
@rpc_fields %{
30+
access_list: :accessList,
31+
blob_versioned_hashes: :blobVersionedHashes,
32+
chain_id: :chainId,
33+
gas_price: :gasPrice,
34+
max_fee_per_blob_gas: :maxFeePerBlobGas,
35+
max_fee_per_gas: :maxFeePerGas,
36+
max_priority_fee_per_gas: :maxPriorityFeePerGas
37+
}
38+
1839
@typedoc """
1940
EVM Transaction type
2041
"""
@@ -23,7 +44,12 @@ defmodule Ethers.Transaction do
2344
@typedoc """
2445
EVM Transaction payload type
2546
"""
26-
@type t_payload :: Eip4844.t() | Eip1559.t() | Legacy.t()
47+
@type t_payload ::
48+
unquote(
49+
@transaction_types
50+
|> Enum.map(&{{:., [], [{:__aliases__, [alias: false], [&1]}, :t]}, [], []})
51+
|> Enum.reduce(&{:|, [], [&1, &2]})
52+
)
2753

2854
@doc "Creates a new transaction struct with the given parameters."
2955
@callback new(map()) :: {:ok, t()} | {:error, reason :: atom()}
@@ -41,24 +67,6 @@ defmodule Ethers.Transaction do
4167
@callback from_rlp_list([binary() | [binary()]]) ::
4268
{:ok, t(), rest :: [binary() | [binary()]]} | {:error, reason :: term()}
4369

44-
@default_transaction_type Eip1559
45-
46-
@transaction_type_modules Application.compile_env(:ethers, :transaction_types, [
47-
Eip4844,
48-
Eip1559,
49-
Legacy
50-
])
51-
52-
@rpc_fields %{
53-
access_list: :accessList,
54-
blob_versioned_hashes: :blobVersionedHashes,
55-
chain_id: :chainId,
56-
gas_price: :gasPrice,
57-
max_fee_per_blob_gas: :maxFeePerBlobGas,
58-
max_fee_per_gas: :maxFeePerGas,
59-
max_priority_fee_per_gas: :maxPriorityFeePerGas
60-
}
61-
6270
@doc """
6371
Creates a new transaction struct with the given parameters.
6472
@@ -72,7 +80,7 @@ defmodule Ethers.Transaction do
7280
@spec new(map()) :: {:ok, t()} | {:error, reason :: term()}
7381
def new(params) do
7482
case Map.fetch(params, :type) do
75-
{:ok, type} when type in @transaction_type_modules ->
83+
{:ok, type} when type in @transaction_types ->
7684
input =
7785
params
7886
|> Map.get(:input, Map.get(params, :data))
@@ -190,7 +198,7 @@ defmodule Ethers.Transaction do
190198
end
191199
end
192200

193-
Enum.each(@transaction_type_modules, fn module ->
201+
Enum.each(@transaction_types, fn module ->
194202
type_envelope = module.type_envelope()
195203

196204
defp decode_transaction_data(<<unquote(type_envelope)::binary, rest::binary>>) do
@@ -375,7 +383,7 @@ defmodule Ethers.Transaction do
375383

376384
defp decode_type("0x" <> _ = type), do: decode_type(Utils.hex_decode!(type))
377385

378-
Enum.each(@transaction_type_modules, fn module ->
386+
Enum.each(@transaction_types, fn module ->
379387
type_envelope = module.type_envelope()
380388
defp decode_type(unquote(type_envelope)), do: {:ok, unquote(module)}
381389
end)

lib/ethers/transaction/eip2930.ex

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
defmodule Ethers.Transaction.Eip2930 do
2+
@moduledoc """
3+
Transaction struct and protocol implementation for Ethereum Improvement Proposal (EIP) 2930
4+
transactions. EIP-2930 introduced a new transaction type that includes an access list,
5+
allowing transactions to pre-specify and pre-pay for account and storage access to mitigate
6+
gas cost changes from EIP-2929 and prevent contract breakage. The access list format also
7+
enables future use cases like block-wide witnesses and static state access patterns.
8+
9+
See: https://eips.ethereum.org/EIPS/eip-2930
10+
"""
11+
12+
alias Ethers.Types
13+
alias Ethers.Utils
14+
15+
@behaviour Ethers.Transaction
16+
17+
@type_id 1
18+
19+
@enforce_keys [:chain_id, :nonce, :gas_price, :gas]
20+
defstruct [
21+
:chain_id,
22+
:nonce,
23+
:gas_price,
24+
:gas,
25+
:to,
26+
:value,
27+
:input,
28+
access_list: []
29+
]
30+
31+
@typedoc """
32+
A transaction type following EIP-2930 (Type-1) and incorporating the following fields:
33+
- `chain_id` - chain ID of network where the transaction is to be executed
34+
- `nonce` - sequence number for the transaction from this sender
35+
- `gas_price`: Price willing to pay for each unit of gas (in wei)
36+
- `gas` - maximum amount of gas allowed for transaction execution
37+
- `to` - destination address for transaction, nil for contract creation
38+
- `value` - amount of ether (in wei) to transfer
39+
- `input` - data payload of the transaction
40+
- `access_list` - list of addresses and storage keys to warm up (introduced in EIP-2930)
41+
"""
42+
@type t :: %__MODULE__{
43+
chain_id: non_neg_integer(),
44+
nonce: non_neg_integer(),
45+
gas_price: non_neg_integer(),
46+
gas: non_neg_integer(),
47+
to: Types.t_address() | nil,
48+
value: non_neg_integer(),
49+
input: binary(),
50+
access_list: [{binary(), [binary()]}]
51+
}
52+
53+
@impl Ethers.Transaction
54+
def new(params) do
55+
to = params[:to]
56+
57+
{:ok,
58+
%__MODULE__{
59+
chain_id: params.chain_id,
60+
nonce: params.nonce,
61+
gas_price: params.gas_price,
62+
gas: params.gas,
63+
to: to && Utils.to_checksum_address(to),
64+
value: params[:value] || 0,
65+
input: params[:input] || params[:data] || "",
66+
access_list: params[:access_list] || []
67+
}}
68+
end
69+
70+
@impl Ethers.Transaction
71+
def auto_fetchable_fields do
72+
[:chain_id, :nonce, :gas_price, :gas]
73+
end
74+
75+
@impl Ethers.Transaction
76+
def type_envelope, do: <<type_id()>>
77+
78+
@impl Ethers.Transaction
79+
def type_id, do: @type_id
80+
81+
@impl Ethers.Transaction
82+
def from_rlp_list([
83+
chain_id,
84+
nonce,
85+
gas_price,
86+
gas,
87+
to,
88+
value,
89+
input,
90+
access_list | rest
91+
]) do
92+
{:ok,
93+
%__MODULE__{
94+
chain_id: :binary.decode_unsigned(chain_id),
95+
nonce: :binary.decode_unsigned(nonce),
96+
gas_price: :binary.decode_unsigned(gas_price),
97+
gas: :binary.decode_unsigned(gas),
98+
to: (to != "" && Utils.encode_address!(to)) || nil,
99+
value: :binary.decode_unsigned(value),
100+
input: input,
101+
access_list: access_list
102+
}, rest}
103+
end
104+
105+
def from_rlp_list(_rlp_list), do: {:error, :transaction_decode_failed}
106+
107+
defimpl Ethers.Transaction.Protocol do
108+
def type_id(_transaction), do: @for.type_id()
109+
110+
def type_envelope(_transaction), do: @for.type_envelope()
111+
112+
def to_rlp_list(tx, _mode) do
113+
# Eip2930 does not discriminate in RLP encoding between payload and hash
114+
[
115+
tx.chain_id,
116+
tx.nonce,
117+
tx.gas_price,
118+
tx.gas,
119+
(tx.to && Utils.decode_address!(tx.to)) || "",
120+
tx.value,
121+
tx.input,
122+
tx.access_list || []
123+
]
124+
end
125+
end
126+
end

test/ethers/transaction_test.exs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ defmodule Ethers.TransactionTest do
6262
end
6363

6464
describe "decode/1" do
65-
test "decodes raw EIP-4844 transaction correctly" do
65+
test "decodes raw EIP-4844 transaction and re-encodes it correctly" do
6666
raw_tx =
6767
"0x03f9043c01830b3444847d2b75008519a4418ab283036fd5941c479675ad559dc151f6ec7ed3fbf8cee79582b680b8a43e5aa08200000000000000000000000000000000000000000000000000000000000bfc5200000000000000000000000000000000000000000000000000000000001bd614000000000000000000000000e64a54e2533fd126c2e452c5fab544d80e2e4eb500000000000000000000000000000000000000000000000000000000101868220000000000000000000000000000000000000000000000000000000010186a47f902c0f8dd941c479675ad559dc151f6ec7ed3fbf8cee79582b6f8c6a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000000000000000000000000000000000000000000aa0b53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103a0360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbca0a10aa54071443520884ed767b0684edf43acec528b7da83ab38ce60126562660f90141948315177ab297ba92a06054ce80a67ed4dbd7ed3af90129a00000000000000000000000000000000000000000000000000000000000000006a00000000000000000000000000000000000000000000000000000000000000007a00000000000000000000000000000000000000000000000000000000000000009a0000000000000000000000000000000000000000000000000000000000000000aa0b53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103a0360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbca0a66cc928b5edb82af9bd49922954155ab7b0942694bea4ce44661d9a8742c2d9a0a66cc928b5edb82af9bd49922954155ab7b0942694bea4ce44661d9a8742c2daa0f652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f3797e352f89b94e64a54e2533fd126c2e452c5fab544d80e2e4eb5f884a00000000000000000000000000000000000000000000000000000000000000004a00000000000000000000000000000000000000000000000000000000000000005a0e85fd79f89ff278fc57d40aecb7947873df9f0beac531c8f71a98f630e1eab62a07686888b19bb7b75e46bb1aa328b65150743f4899443d722f0adf8e252ccda410af863a001e74519daf1b03d40e76d557588db2e9b21396f7aeb6086bd794cc4357083efa00169766b1aff3508331a39e7081e591a3ff3bacf957788571269797db7ff3ccca0017045639ffe91febe66cc4427fcf6331980dd9a0dab4af3e81c5514b918ed6180a036a73bf3fe4b9a375c2564b2b1a4a795c82b3923225af0a2ab5d7a561b0c4b92a0366ac3b831ece20f95d1eac369b1c8d4c2c5ac730655d89c005fe310d1db2086"
6868

@@ -145,9 +145,11 @@ defmodule Ethers.TransactionTest do
145145
"0x0169766b1aff3508331a39e7081e591a3ff3bacf957788571269797db7ff3ccc",
146146
"0x017045639ffe91febe66cc4427fcf6331980dd9a0dab4af3e81c5514b918ed61"
147147
]
148+
149+
assert Ethers.Utils.hex_encode(Transaction.encode(decoded_tx)) == raw_tx
148150
end
149151

150-
test "decodes raw EIP-1559 transaction correctly" do
152+
test "decodes raw EIP-1559 transaction and re-encodes it correctly" do
151153
raw_tx =
152154
"0x02f8af0177837a12008502c4bfbc3282f88c948881562783028f5c1bcb985d2283d5e170d8888880b844a9059cbb0000000000000000000000002ef7f5c7c727d8845e685f462a5b4f8ac4972a6700000000000000000000000000000000000000000000051ab2ea6fbbb7420000c001a007280557e86f690290f9ea9e26cc17e0cf09a17f6c2d041e95b33be4b81888d0a06c7a24e8fba5cceb455b19950849b9733f0deb92d7e8c2a919f4a82df9c6036a"
153155

@@ -172,9 +174,40 @@ defmodule Ethers.TransactionTest do
172174
assert decoded_tx.payload.max_priority_fee_per_gas == 8_000_000
173175
assert decoded_tx.payload.to == "0x8881562783028F5c1BCB985d2283D5E170D88888"
174176
assert decoded_tx.payload.value == 0
177+
178+
assert Ethers.Utils.hex_encode(Transaction.encode(decoded_tx)) == raw_tx
175179
end
176180

177-
test "decodes raw legacy transaction correctly" do
181+
test "decodes raw EIP-2930 transaction and re-encodes it correctly" do
182+
raw_tx =
183+
"0x01f903640182dd688503a656ac80830623c4944a137fd5e7a256ef08a7de531a17d0be0cc7b6b680b901a46dbf2fa0000000000000000000000000e592427a0aece92de3edee1f18e0157c05861564000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000104414bf3890000000000000000000000007d1afa7b718fb893db30a3abc0cfc608aacfebb0000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000bb80000000000000000000000004a137fd5e7a256ef08a7de531a17d0be0cc7b6b60000000000000000000000000000000000000000000000000000000060bda78e0000000000000000000000000000000000000000000000cc223b921be6800000000000000000000000000000000000000000000000000000000000017dd4e6ca000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f90153f87a9407a6e955ba4345bae83ac2a6faa771fddd8a2011f863a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000008f87a947d1afa7b718fb893db30a3abc0cfc608aacfebb0f863a014d5312942240e565c56aec11806ce58e3c0e38c96269d759c5d35a2a2e4a449a02701fd0b2638f33db225d91c6adbdad46590a86a09a2b2c386405c2f742af842a037b0b82ee5d8a88672df3895a46af48bbcd30d6efcc908136e29456fa30604bbf85994a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48f842a037570cf18c6d95744a154fa2b19b7e958c78ef68b8c60a80dc527fc15e2ceb8fa06e89d31e3fd8d2bf0b411c458e98c7463bf723878c3ce8a845bcf9dc3b2e391780a01d40605de92c503219631e625ca0d023df8dfef9058896804fb1952d386b64e1a00e0ec0714b7956fe29820cb62998936b78ca4b8a3b05291db90e475244d5c63f"
184+
185+
expected_from = "0x005FdE5294199d5C3Eb5Eb7a6E51954123b74b1c"
186+
expected_hash = "0xdb32a678b6c5855eb3c5ff47513e136a85a391469755d045d8846e37fc99d774"
187+
188+
assert {:ok, decoded_tx} = Transaction.decode(raw_tx)
189+
assert %Transaction.Signed{payload: %Transaction.Eip2930{}} = decoded_tx
190+
191+
# Verify transaction hash matches
192+
assert Transaction.transaction_hash(decoded_tx) == expected_hash
193+
194+
# Verify recovered from address
195+
recovered_from = Transaction.Signed.from_address(decoded_tx)
196+
assert String.downcase(recovered_from) == String.downcase(expected_from)
197+
198+
# Verify other transaction fields
199+
assert decoded_tx.payload.chain_id == 1
200+
assert decoded_tx.payload.gas == 402_372
201+
assert decoded_tx.payload.gas_price == 15_675_600_000
202+
assert decoded_tx.payload.nonce == 56_680
203+
assert decoded_tx.payload.to == "0x4A137FD5e7a256eF08A7De531A17D0BE0cc7B6b6"
204+
assert decoded_tx.payload.value == 0
205+
assert Enum.count(decoded_tx.payload.access_list) == 3
206+
207+
assert Ethers.Utils.hex_encode(Transaction.encode(decoded_tx)) == raw_tx
208+
end
209+
210+
test "decodes raw legacy transaction and re-encodes it correctly" do
178211
raw_tx =
179212
"0xf86c81c6850c92a69c0082520894e48c9a989438606a79a7560cfba3d34bafbac38e87596f744abf34368025a0ee0b54a64cf8130e36cd1d19395d6d434c285c832a7908873a24610ec32896dfa070b5e779cdcaf5c661c1df44e80895f6ab68463d3ede2cf4955855bc3c6edebb"
180213

@@ -198,6 +231,8 @@ defmodule Ethers.TransactionTest do
198231
assert decoded_tx.payload.gas_price == 54_000_000_000
199232
assert decoded_tx.payload.to == "0xe48C9A989438606a79a7560cfba3d34BAfBAC38E"
200233
assert decoded_tx.payload.value == 25_173_818_188_182_582
234+
235+
assert Ethers.Utils.hex_encode(Transaction.encode(decoded_tx)) == raw_tx
201236
end
202237
end
203238
end

0 commit comments

Comments
 (0)