Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Transaction handling #164

Merged
merged 7 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,27 @@

## Unreleased

### Breaking Changes

- Removed `Ethers.Transaction` struct and replaced with separate EIP-1559 and Legacy transaction structs for improved type safety
- Deprecated `Ethers.Transaction.from_map/1` - use `Ethers.Transaction.from_rpc_map/1` instead for RPC response parsing
- Deprecated `Ethers.Utils.maybe_add_gas_limit/2` - gas limits should now be set explicitly
- Changed input format requirements: All inputs to `Ethers` functions must use native types (e.g., integers) instead of hex strings encoded values.
- Removed auto-gas estimation from send_transaction calls
- `tx_type` option in transaction overrides has been replaced with `type`, now requiring explicit struct modules (e.g. `Ethers.Transaction.Eip1559`, `Ethers.Transaction.Legacy`).

### New features
- Implement `Ethers.CcipRead` to support EIP-3668
- NameService now support off-chain/cross-chain lookups using CCIP-Read

- Added **EIP-3668 CCIP-Read** support via `Ethers.CcipRead` module for off-chain data resolution
- Extended NameService to handle off-chain and cross-chain name resolution using CCIP-Read protocol
- Introduced `Ethers.Transaction.Protocol` behaviour for improved transaction handling.
- Added dedicated *EIP-1559* and *Legacy* transaction struct types with validation
- New address utilities: `Ethers.Utils.decode_address/1` and `Ethers.Utils.encode_address/1`

### Enhancements

- Improve `Ethers.deploy/2` error handling
- NameService improvements and support for ENSIP-10
- Improved error handling and reporting in `Ethers.deploy/2`
- Enhanced NameService with ENSIP-10 wildcard resolution support

## v0.5.5 (2024-12-03)

Expand Down
54 changes: 33 additions & 21 deletions lib/ethers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,16 @@ defmodule Ethers do
alias Ethers.Types
alias Ethers.Utils

@option_keys [:rpc_client, :rpc_opts, :signer, :signer_opts, :tx_type]
@option_keys [:rpc_client, :rpc_opts, :signer, :signer_opts]
@hex_decode_post_process [
:chain_id,
:current_block_number,
:current_gas_price,
:estimate_gas,
:get_balance,
:get_transaction_count,
:max_priority_fee_per_gas
:max_priority_fee_per_gas,
:gas_price
]
@rpc_actions_map %{
call: :eth_call,
Expand Down Expand Up @@ -347,7 +348,7 @@ defmodule Ethers do
- `:rpc_opts`: Extra options to pass to rpc_client. (Like timeout, Server URL, etc.)
- `:signer`: The signer module to use for signing transaction. Default is nil and will rely on the RPC server for signing.
- `:signer_opts`: Options for signer module. See your signer docs for more details.
- `:tx_type`: Transaction type. Either `:eip1559` (default) or `:legacy`.
- `:type`: Transaction type. Either `Ethers.Transaction.Eip1559` (default) or `Ethers.Transaction.Legacy`.
- `:to`: Address of the contract or a receiver of this transaction. (required if TxData does not have default_address)
- `:value`: Ether value to send with the transaction to the receiver (`from => to`).

Expand Down Expand Up @@ -375,7 +376,7 @@ defmodule Ethers do
"""
@spec send!(map() | TxData.t(), Keyword.t()) :: String.t() | no_return()
def send!(tx_data, overrides \\ []) do
case Ethers.send(tx_data, overrides) do
case __MODULE__.send(tx_data, overrides) do
{:ok, tx_hash} -> tx_hash
{:error, reason} -> raise ExecutionError, reason
end
Expand Down Expand Up @@ -619,9 +620,7 @@ defmodule Ethers do
to: nil
})

with {:ok, tx_params} <- Utils.maybe_add_gas_limit(tx_params, opts) do
maybe_use_signer(tx_params, opts)
end
maybe_use_signer(tx_params, opts)
end

defp pre_process("0x" <> _ = signed_tx, _overrides, :send, _opts) do
Expand All @@ -631,25 +630,24 @@ defmodule Ethers do
defp pre_process(tx_data, overrides, :send = action, opts) do
tx_params = TxData.to_map(tx_data, overrides)

with :ok <- check_params(tx_params, action),
{:ok, tx_params} <- Utils.maybe_add_gas_limit(tx_params, opts) do
with :ok <- check_params(tx_params, action) do
maybe_use_signer(tx_params, opts)
end
end

defp pre_process(tx_data, overrides, :sign_transaction = action, opts) do
defp pre_process(tx_data, overrides, :sign_transaction = action, _opts) do
tx_params = TxData.to_map(tx_data, overrides)

with :ok <- check_params(tx_params, action) do
Utils.maybe_add_gas_limit(tx_params, opts)
{:ok, tx_params}
end
end

defp pre_process(tx_data, overrides, :estimate_gas = action, _opts) do
tx_params = TxData.to_map(tx_data, overrides)

with :ok <- check_params(tx_params, action) do
{:ok, tx_params}
{:ok, Transaction.to_rpc_map(tx_params)}
end
end

Expand Down Expand Up @@ -716,7 +714,7 @@ defmodule Ethers do
do: {:error, :transaction_not_found}

defp post_process({:ok, tx_data}, _tx_hash, :get_transaction) do
Transaction.from_map(tx_data)
Transaction.from_rpc_map(tx_data)
end

defp post_process({:ok, nil}, _tx_hash, :get_transaction_receipt),
Expand Down Expand Up @@ -800,6 +798,8 @@ defmodule Ethers do
use_signer(tx_params, signer, opts)

{:error, :no_signer} ->
tx_params = Transaction.to_rpc_map(tx_params)

{:ok, tx_params, :eth_send_transaction}
end
end
Expand All @@ -820,14 +820,26 @@ defmodule Ethers do
end

defp use_signer(tx_params, signer, opts) do
signer_opts = Keyword.get(opts, :signer_opts) || default_signer_opts()
tx_type = Keyword.get(opts, :tx_type, :eip1559)

with {:ok, tx} <-
Transaction.new(tx_params, tx_type) |> Transaction.fill_with_defaults(opts),
{:ok, signed_tx} <-
signer.sign_transaction(tx, signer_opts) do
{:ok, signed_tx, :eth_send_raw_transaction}
with {:ok, tx_params} <- Transaction.add_auto_fetchable_fields(tx_params, opts),
{:ok, tx} <- Transaction.new(tx_params),
{:ok, signed_tx_hex} <- signer.sign_transaction(tx, build_signer_opts(tx_params, opts)) do
{:ok, signed_tx_hex, :eth_send_raw_transaction}
end
end

defp build_signer_opts(tx_params, opts) do
signer_opts = Keyword.get(opts, :signer_opts, default_signer_opts())
tx_from = Map.get(tx_params, :from)
signer_from = Keyword.get(signer_opts, :from) || tx_from

if tx_from do
if tx_from != signer_from do
raise ArgumentError, ":signer_opts has a different from address than transaction"
end

Keyword.put(signer_opts, :from, tx_from)
else
signer_opts
end
end

Expand Down
12 changes: 10 additions & 2 deletions lib/ethers/signer/json_rpc.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,16 @@ defmodule Ethers.Signer.JsonRPC do
alias Ethers.Transaction

@impl true
def sign_transaction(%Transaction{} = tx, opts) do
tx_map = Transaction.to_map(tx)
def sign_transaction(tx, opts) do
tx_map = Transaction.to_rpc_map(tx)

tx_map =
if from = Keyword.get(opts, :from) do
Map.put_new(tx_map, :from, from)
else
tx_map
end

{rpc_module, opts} = Keyword.pop(opts, :rpc_module, Ethereumex.HttpClient)

rpc_module.request("eth_signTransaction", [tx_map], opts)
Expand Down
42 changes: 20 additions & 22 deletions lib/ethers/signer/local.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ defmodule Ethers.Signer.Local do
import Ethers, only: [secp256k1_module: 0, keccak_module: 0]

alias Ethers.Transaction
alias Ethers.Transaction.SignedTransaction
alias Ethers.Utils

unless Code.ensure_loaded?(secp256k1_module()) do
if not Code.ensure_loaded?(secp256k1_module()) do
@impl true
def sign_transaction(_tx, _opts), do: {:error, :secp256k1_module_not_loaded}

Expand All @@ -29,26 +30,23 @@ defmodule Ethers.Signer.Local do
end

@impl true
def sign_transaction(%Transaction{} = tx, opts) do
def sign_transaction(transaction, opts) do
with {:ok, private_key} <- private_key(opts),
:ok <- validate_private_key(private_key, tx.from),
{:ok, {r, s, recovery_id}} <-
Transaction.encode(tx)
|> keccak_module().hash_256()
|> secp256k1_module().sign(private_key) do
y_parity_or_v = Transaction.calculate_y_parity_or_v(tx, recovery_id)

signed =
%Ethers.Transaction{
tx
| signature_r: Utils.hex_encode(r),
signature_s: Utils.hex_encode(s),
signature_y_parity_or_v: Utils.integer_to_hex(y_parity_or_v)
:ok <- validate_private_key(private_key, Keyword.get(opts, :from)),
encoded = Transaction.encode(transaction, :hash),
sign_hash = keccak_module().hash_256(encoded),
{:ok, {r, s, recovery_id}} <- secp256k1_module().sign(sign_hash, private_key) do
signed_transaction =
%SignedTransaction{
transaction: transaction,
signature_r: r,
signature_s: s,
signature_y_parity_or_v: Transaction.calculate_y_parity_or_v(transaction, recovery_id)
}
|> Transaction.encode()
|> Utils.hex_encode()

{:ok, signed}
encoded_signed_transaction = Transaction.encode(signed_transaction)

{:ok, Utils.hex_encode(encoded_signed_transaction)}
end
end

Expand All @@ -66,14 +64,14 @@ defmodule Ethers.Signer.Local do
end
end

defp validate_private_key(_private_key, nil), do: {:error, :no_from_address}
defp validate_private_key(_private_key, nil), do: :ok

defp validate_private_key(private_key, address) do
with {:ok, private_key_address} <- do_get_address(private_key) do
private_key_address = String.downcase(private_key_address)
address = String.downcase(address)
private_key_address_bin = Utils.decode_address!(private_key_address)
address_bin = Utils.decode_address!(address)

if address == private_key_address do
if address_bin == private_key_address_bin do
:ok
else
{:error, :wrong_key}
Expand Down
Loading
Loading