Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
@spec transaction(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()}
def transaction(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do
necessity_by_association_with_actions =
Map.put(@transaction_necessity_by_association, :transaction_actions, :optional)
@transaction_necessity_by_association
|> Map.put(:transaction_actions, :optional)
|> Map.put(:signed_authorizations, :optional)

necessity_by_association =
case Application.get_env(:explorer, :chain_type) do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
}
end

def render("authorization_list.json", %{signed_authorizations: signed_authorizations}) do
signed_authorizations
|> Enum.sort_by(& &1.index, :asc)
|> Enum.map(&prepare_signed_authorization/1)
end

@doc """
Decodes list of logs
"""
Expand Down Expand Up @@ -336,6 +342,18 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
}
end

def prepare_signed_authorization(signed_authorization) do
%{
"address" => signed_authorization.address,
"chain_id" => signed_authorization.chain_id,
"nonce" => signed_authorization.nonce,
"r" => signed_authorization.r,
"s" => signed_authorization.s,
"v" => signed_authorization.v,
"authority" => signed_authorization.authority
}
end

defp get_tx_hash(%Transaction{} = tx), do: to_string(tx.hash)
defp get_tx_hash(hash), do: to_string(hash)

Expand Down Expand Up @@ -450,7 +468,8 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
"method" => Transaction.method_name(transaction, decoded_input),
"tx_types" => tx_types(transaction),
"tx_tag" => GetTransactionTags.get_transaction_tags(transaction.hash, current_user(single_tx? && conn)),
"has_error_in_internal_txs" => transaction.has_error_in_internal_txs
"has_error_in_internal_txs" => transaction.has_error_in_internal_txs,
"authorization_list" => authorization_list(transaction.signed_authorizations)
}

result
Expand Down Expand Up @@ -482,6 +501,13 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
render("transaction_actions.json", %{actions: actions})
end

def authorization_list(nil), do: []
def authorization_list(%NotLoaded{}), do: []

def authorization_list(signed_authorizations) do
render("authorization_list.json", %{signed_authorizations: signed_authorizations})
end

defp burnt_fees(transaction, max_fee_per_gas, base_fee_per_gas) do
if !is_nil(max_fee_per_gas) and !is_nil(transaction.gas_used) and !is_nil(base_fee_per_gas) do
if Decimal.compare(max_fee_per_gas.value, 0) == :eq do
Expand Down Expand Up @@ -608,7 +634,20 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
| :token_creation
| :token_transfer
| :blob_transaction
def tx_types(tx, types \\ [], stage \\ :blob_transaction)
| :set_code_transaction
def tx_types(tx, types \\ [], stage \\ :set_code_transaction)

def tx_types(%Transaction{type: type} = tx, types, :set_code_transaction) do
# EIP-7702 set code transaction type
types =
if type == 4 do
[:set_code_transaction | types]
else
types
end

tx_types(tx, types, :blob_transaction)
end

def tx_types(%Transaction{type: type} = tx, types, :blob_transaction) do
# EIP-2718 blob transaction type
Expand Down
44 changes: 44 additions & 0 deletions apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/signed_authorization.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
defmodule EthereumJSONRPC.SignedAuthorization do
@moduledoc """
The format of authorization tuples returned for
set code transactions [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702).
"""

import EthereumJSONRPC, only: [quantity_to_integer: 1]

@typedoc """
* `"chainId"` - specifies the chain for which the authorization was created `t:EthereumJSONRPC.quantity/0`.
* `"address"` - `t:EthereumJSONRPC.address/0` of the delegate contract.
* `"nonce"` - signature nonce `t:EthereumJSONRPC.quantity/0`.
* `"v"` - v component of the signature `t:EthereumJSONRPC.quantity/0`.
* `"r"` - r component of the signature `t:EthereumJSONRPC.quantity/0`.
* `"s"` - s component of the signature `t:EthereumJSONRPC.quantity/0`.
"""
@type t :: %{
String.t() => EthereumJSONRPC.address() | EthereumJSONRPC.quantity()
}

@type params :: %{
chain_id: non_neg_integer(),
address: EthereumJSONRPC.address(),
nonce: non_neg_integer(),
r: non_neg_integer(),
s: non_neg_integer(),
v: non_neg_integer()
}

@doc """
Converts `t:t/0` to `t:params/0`
"""
@spec to_params(t()) :: params()
def to_params(raw) do
%{
chain_id: quantity_to_integer(raw["chainId"]),
address: raw["address"],
nonce: quantity_to_integer(raw["nonce"]),
r: quantity_to_integer(raw["r"]),
s: quantity_to_integer(raw["s"]),
v: quantity_to_integer(raw["v"])
}
end
end
31 changes: 26 additions & 5 deletions apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ defmodule EthereumJSONRPC.Transaction do
]

alias EthereumJSONRPC
alias EthereumJSONRPC.SignedAuthorization

case Application.compile_env(:explorer, :chain_type) do
:ethereum ->
Expand Down Expand Up @@ -74,8 +75,16 @@ defmodule EthereumJSONRPC.Transaction do
@chain_type_fields quote(do: [])
end

# todo: Check if it's possible to simplify by avoiding t -> elixir -> params conversions
# and directly convert t -> params.
@type elixir :: %{
String.t() => EthereumJSONRPC.address() | EthereumJSONRPC.hash() | String.t() | non_neg_integer() | nil
String.t() =>
EthereumJSONRPC.address()
| EthereumJSONRPC.hash()
| String.t()
| non_neg_integer()
| [SignedAuthorization.params()]
| nil
}

@typedoc """
Expand Down Expand Up @@ -105,6 +114,7 @@ defmodule EthereumJSONRPC.Transaction do
* `"maxPriorityFeePerGas"` - `t:EthereumJSONRPC.quantity/0` of wei to denote max priority fee per unit of gas used. Introduced in [EIP-1559](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md)
* `"maxFeePerGas"` - `t:EthereumJSONRPC.quantity/0` of wei to denote max fee per unit of gas used. Introduced in [EIP-1559](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md)
* `"type"` - `t:EthereumJSONRPC.quantity/0` denotes transaction type. Introduced in [EIP-1559](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md)
* `"authorizationList"` - `t:list/0` of `t:EthereumJSONRPC.SignedAuthorization.t/0` authorization tuples. Introduced in [EIP-7702](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md)
#{case Application.compile_env(:explorer, :chain_type) do
:ethereum -> """
* `"maxFeePerBlobGas"` - `t:EthereumJSONRPC.quantity/0` of wei to denote max fee per unit of blob gas used. Introduced in [EIP-4844](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-4844.md)
Expand All @@ -128,7 +138,12 @@ defmodule EthereumJSONRPC.Transaction do
"""
@type t :: %{
String.t() =>
EthereumJSONRPC.address() | EthereumJSONRPC.hash() | EthereumJSONRPC.quantity() | String.t() | nil
EthereumJSONRPC.address()
| EthereumJSONRPC.hash()
| EthereumJSONRPC.quantity()
| String.t()
| [SignedAuthorization.t()]
| nil
}

@type params :: %{
Expand All @@ -150,7 +165,8 @@ defmodule EthereumJSONRPC.Transaction do
transaction_index: non_neg_integer(),
max_priority_fee_per_gas: non_neg_integer(),
max_fee_per_gas: non_neg_integer(),
type: non_neg_integer()
type: non_neg_integer(),
authorization_list: [SignedAuthorization.params()]
}

@doc """
Expand Down Expand Up @@ -322,7 +338,8 @@ defmodule EthereumJSONRPC.Transaction do
{"block_timestamp", :block_timestamp},
{"r", :r},
{"s", :s},
{"v", :v}
{"v", :v},
{"authorizationList", :authorization_list}
])
end

Expand Down Expand Up @@ -368,7 +385,8 @@ defmodule EthereumJSONRPC.Transaction do
{"block_timestamp", :block_timestamp},
{"r", :r},
{"s", :s},
{"v", :v}
{"v", :v},
{"authorizationList", :authorization_list}
])
end

Expand Down Expand Up @@ -700,6 +718,9 @@ defmodule EthereumJSONRPC.Transaction do
end
end

defp entry_to_elixir({"authorizationList" = key, value}),
do: {key, value |> Enum.map(&SignedAuthorization.to_params/1)}

# Celo-specific fields
if Application.compile_env(:explorer, :chain_type) == :celo do
defp entry_to_elixir({key, value})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
defmodule Explorer.Chain.Import.Runner.SignedAuthorizations do
@moduledoc """
Bulk imports `t:Explorer.Chain.SignedAuthorization.t/0`.
"""

require Ecto.Query

import Ecto.Query, only: [from: 2]

alias Ecto.{Changeset, Multi, Repo}
alias Explorer.Chain.{Import, SignedAuthorization}
alias Explorer.Prometheus.Instrumenter

@behaviour Import.Runner

# milliseconds
@timeout 60_000

@type imported :: [SignedAuthorization.t()]

@impl Import.Runner
def ecto_schema_module, do: SignedAuthorization

@impl Import.Runner
def option_key, do: :signed_authorizations

@impl Import.Runner
def imported_table_row do
%{
value_type: "[#{ecto_schema_module()}.t()]",
value_description: "List of `t:#{ecto_schema_module()}.t/0`s"
}
end

@impl Import.Runner
def run(multi, changes_list, %{timestamps: timestamps} = options) do
insert_options =
options
|> Map.get(option_key(), %{})
|> Map.take(~w(on_conflict timeout)a)
|> Map.put_new(:timeout, @timeout)
|> Map.put(:timestamps, timestamps)

Multi.run(multi, :signed_authorizations, fn repo, _ ->
Instrumenter.block_import_stage_runner(
fn -> insert(repo, changes_list, insert_options) end,
:block_referencing,
:signed_authorizations,
:signed_authorizations
)
end)
end

@impl Import.Runner
def timeout, do: @timeout

@spec insert(Repo.t(), [map()], %{
optional(:on_conflict) => Import.Runner.on_conflict(),
required(:timeout) => timeout,
required(:timestamps) => Import.timestamps()
}) ::
{:ok, [SignedAuthorization.t()]}
| {:error, [Changeset.t()]}
defp insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0)
conflict_target = [:transaction_hash, :index]

{:ok, _} =
Import.insert_changes_list(
repo,
changes_list,
for: SignedAuthorization,
on_conflict: on_conflict,
conflict_target: conflict_target,
returning: true,
timeout: timeout,
timestamps: timestamps
)
end

defp default_on_conflict do
from(
authorization in SignedAuthorization,
update: [
set: [
chain_id: fragment("EXCLUDED.chain_id"),
address: fragment("EXCLUDED.address"),
nonce: fragment("EXCLUDED.nonce"),
r: fragment("EXCLUDED.r"),
s: fragment("EXCLUDED.s"),
v: fragment("EXCLUDED.v"),
authority: fragment("EXCLUDED.authority"),
inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", authorization.inserted_at),
updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", authorization.updated_at)
]
],
where:
fragment(
"(EXCLUDED.chain_id, EXCLUDED.address, EXCLUDED.nonce, EXCLUDED.r, EXCLUDED.s, EXCLUDED.v, EXCLUDED.authority) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?)",
authorization.chain_id,
authorization.address,
authorization.nonce,
authorization.r,
authorization.s,
authorization.v,
authorization.authority
)
)
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ defmodule Explorer.Chain.Import.Stage.BlockReferencing do
Runner.Tokens,
Runner.TokenInstances,
Runner.TransactionActions,
Runner.Withdrawals
Runner.Withdrawals,
Runner.SignedAuthorizations
]

@extra_runners_by_chain_type %{
Expand Down
Loading