ETH JSON RPC extension (#9409)

* Add new ETH JSON RPC methods

* Add new ETH JSON RPC API methods; Add ETH_JSON_RPC_MAX_BATCH_SIZE env

* Process review comments
pull/9537/head
nikitosing 8 months ago committed by GitHub
parent 546b732ac1
commit a14630855c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 23
      apps/block_scout_web/lib/block_scout_web/controllers/api/eth_rpc/eth_controller.ex
  3. 16
      apps/block_scout_web/lib/block_scout_web/views/api/eth_rpc/view.ex
  4. 78
      apps/explorer/lib/explorer/bloom_filter.ex
  5. 4
      apps/explorer/lib/explorer/chain/cache/gas_price_oracle.ex
  6. 6
      apps/explorer/lib/explorer/chain/wei.ex
  7. 843
      apps/explorer/lib/explorer/eth_rpc.ex
  8. 121
      apps/explorer/lib/explorer/eth_rpc_helper.ex
  9. 63
      apps/explorer/test/explorer/bloom_filter_test.exs
  10. 3
      config/runtime.exs

@ -7,6 +7,7 @@
- [#9490](https://github.com/blockscout/blockscout/pull/9490) - Add blob transaction counter and filter in block view
- [#9461](https://github.com/blockscout/blockscout/pull/9461) - Fetch blocks without internal transactions backwards
- [#9460](https://github.com/blockscout/blockscout/pull/9460) - Optimism chain type
- [#9409](https://github.com/blockscout/blockscout/pull/9409) - ETH JSON RPC extension
- [#9390](https://github.com/blockscout/blockscout/pull/9390) - Add stability validators
- [#8702](https://github.com/blockscout/blockscout/pull/8702) - Add OP withdrawal status to transaction page in API
- [#7200](https://github.com/blockscout/blockscout/pull/7200) - Add Optimism BedRock Deposits to the main page in API

@ -3,20 +3,29 @@ defmodule BlockScoutWeb.API.EthRPC.EthController do
alias BlockScoutWeb.AccessHelper
alias BlockScoutWeb.API.EthRPC.View, as: EthRPCView
alias BlockScoutWeb.API.RPC.RPCView
alias Explorer.EthRPC
def eth_request(%{body_params: %{"_json" => requests}} = conn, _) when is_list(requests) do
case AccessHelper.check_rate_limit(conn) do
:ok ->
responses = EthRPC.responses(requests)
eth_json_rpc_max_batch_size = Application.get_env(:block_scout_web, :api_rate_limit)[:eth_json_rpc_max_batch_size]
conn
|> put_status(200)
|> put_view(EthRPCView)
|> render("responses.json", %{responses: responses})
with :ok <- AccessHelper.check_rate_limit(conn),
{:batch_size, true} <- {:batch_size, Enum.count(requests) <= eth_json_rpc_max_batch_size} do
responses = EthRPC.responses(requests)
conn
|> put_status(200)
|> put_view(EthRPCView)
|> render("responses.json", %{responses: responses})
else
:rate_limit_reached ->
AccessHelper.handle_rate_limit_deny(conn)
{:batch_size, _} ->
conn
|> put_status(413)
|> put_view(RPCView)
|> render(:error, %{:error => "Payload Too Large. Max batch size is #{eth_json_rpc_max_batch_size}"})
end
end

@ -59,6 +59,14 @@ defmodule BlockScoutWeb.API.EthRPC.View do
"""
end
def encode(%BlockScoutWeb.API.EthRPC.View{id: id, error: error}, _options) when is_map(error) do
error = Poison.encode!(error)
"""
{"jsonrpc":"2.0","error": #{error},"id": #{id}}
"""
end
def encode(%BlockScoutWeb.API.EthRPC.View{id: id, error: error}, _options) do
"""
{"jsonrpc":"2.0","error": "#{error}","id": #{id}}
@ -75,6 +83,14 @@ defmodule BlockScoutWeb.API.EthRPC.View do
"""
end
def encode(%BlockScoutWeb.API.EthRPC.View{id: id, error: error}, _options) when is_map(error) do
error = Jason.encode!(error)
"""
{"jsonrpc":"2.0","error": #{error},"id": #{id}}
"""
end
def encode(%BlockScoutWeb.API.EthRPC.View{id: id, error: error}, _options) do
"""
{"jsonrpc":"2.0","error": "#{error}","id": #{id}}

@ -0,0 +1,78 @@
defmodule Explorer.BloomFilter do
@moduledoc """
Eth Bloom filter realization. Reference: https://github.com/NethermindEth/nethermind/blob/d61c78af6de2d0a89bd4efd6bfed62cb6b774f59/src/Nethermind/Nethermind.Core/Bloom.cs
"""
import Bitwise
alias Explorer.BloomFilter
alias Explorer.Chain.Log
@bloom_byte_length 256
@bloom_bit_length 8 * @bloom_byte_length
defstruct filter: <<0::2048>>
@doc """
Computes bloom filter from list of logs
"""
@spec logs_bloom([Log.t()]) :: <<_::2048>>
def logs_bloom(logs) do
logs
|> Enum.reduce(%BloomFilter{}, fn log, acc ->
topics =
Enum.reject(
[log.first_topic, log.second_topic, log.third_topic, log.fourth_topic],
&is_nil/1
)
acc_new =
acc
|> add(log.address_hash.bytes)
Enum.reduce(topics, acc_new, fn topic, acc -> add(acc, topic.bytes) end)
end)
|> Map.get(:filter)
end
defp add(%BloomFilter{filter: filter} = bloom, element) do
{i1, i2, i3} = get_extract(element)
new_filter =
filter
|> set_index(i1)
|> set_index(i2)
|> set_index(i3)
%BloomFilter{bloom | filter: new_filter}
end
defp hash_function(data), do: ExKeccak.hash_256(data)
defp set_index(filter, index) do
byte_position = div(index, 8)
shift = rem(index, 8)
byte = :binary.at(filter, byte_position)
value = set_bit(byte, shift)
<<head::binary-size(byte_position), _byte::binary-size(1), tail::binary>> = filter
<<head::binary, value, tail::binary>>
end
defp set_bit(byte, bit) do
mask = 1 <<< (7 - bit)
bor(byte, mask)
end
defp get_extract(bytes) do
hash = hash_function(bytes)
{get_index(hash, 0, 1), get_index(hash, 2, 3), get_index(hash, 4, 5)}
end
defp get_index(bytes, index_1, index_2) do
@bloom_bit_length - 1 - rem((:binary.at(bytes, index_1) <<< 8) + :binary.at(bytes, index_2), 2048)
end
end

@ -333,7 +333,9 @@ defmodule Explorer.Chain.Cache.GasPriceOracle do
time: time && time |> Decimal.to_float(),
fiat_price: fiat_fee(fee, exchange_rate_from_db),
base_fee: base_fee |> format_wei(),
priority_fee: base_fee && priority_fee && priority_fee |> Decimal.new() |> Wei.from(:wei) |> format_wei()
priority_fee: base_fee && priority_fee && priority_fee |> Decimal.new() |> Wei.from(:wei) |> format_wei(),
priority_fee_wei: base_fee && priority_fee && priority_fee |> Decimal.new() |> Decimal.round(),
wei: fee |> Wei.to(:wei) |> Decimal.round()
}
end

@ -118,8 +118,12 @@ defmodule Explorer.Chain.Wei do
@wei_per_ether Decimal.new(1_000_000_000_000_000_000)
@wei_per_gwei Decimal.new(1_000_000_000)
@spec hex_format(Wei.t()) :: String.t()
@spec hex_format(Wei.t() | Decimal.t()) :: String.t()
def hex_format(%Wei{value: decimal}) do
hex_format(decimal)
end
def hex_format(%Decimal{} = decimal) do
hex =
decimal
|> Decimal.to_integer()

@ -2,13 +2,27 @@ defmodule Explorer.EthRPC do
@moduledoc """
Ethereum JSON RPC methods logic implementation.
"""
import Explorer.EthRpcHelper
alias Ecto.Type, as: EctoType
alias Explorer.{Chain, Repo}
alias Explorer.Chain.{Block, Data, Hash, Hash.Address, Wei}
alias Explorer.Chain.Cache.BlockNumber
alias Explorer.{BloomFilter, Chain, Helper, Repo}
alias Explorer.Chain.{
Block,
Data,
DenormalizationHelper,
Hash,
Hash.Address,
Transaction,
Transaction.Status,
Wei
}
alias Explorer.Chain.Cache.{BlockNumber, GasPriceOracle}
alias Explorer.Etherscan.{Blocks, Logs, RPC}
@nil_gas_price_message "Gas price is not estimated yet"
@methods %{
"eth_blockNumber" => %{
action: :eth_block_number,
@ -84,6 +98,553 @@ defmodule Explorer.EthRPC do
}]
}
"""
},
"eth_gasPrice" => %{
action: :eth_gas_price,
notes: """
Returns the average gas price per gas in wei.
""",
example: """
{"jsonrpc": "2.0", "id": 4, "method": "eth_gasPrice", "params": []}
""",
params: [],
result: """
{"jsonrpc": "2.0", "id": 4, "result": "0xbf69c09bb"}
"""
},
"eth_getTransactionByHash" => %{
action: :eth_get_transaction_by_hash,
notes: """
""",
example: """
{"jsonrpc": "2.0", "id": 4, "method": "eth_getTransactionByHash", "params": ["0x98318a5a22e363928d4565382c1022a8aed169b6a657f639c2f5c6e2c5114e4c"]}
""",
params: [
%{
name: "Data",
description: "32 Bytes - transaction hash to get",
type: "string",
default: nil,
required: true
}
],
result: """
{
"jsonrpc": "2.0",
"result": {
"blockHash": "0x33c4ddb4478395b9d73aad2eb8640004a4a312da29ebccbaa33933a43edda019",
"blockNumber": "0x87855e",
"chainId": "0x5",
"from": "0xe38ecdf3cfbaf5cf347e6a3d6490eb34e3a0119d",
"gas": "0x186a0",
"gasPrice": "0x195d",
"hash": "0xfe524295c6c01ab25645035a228387bf0e64c8af429f3dd9d6ef2e3b05337839",
"input": "0xe9e05c42000000000000000000000000e38ecdf3cfbaf5cf347e6a3d6490eb34e3a0119d0000000000000000000000000000000000000000000000000001c6bf5263400000000000000000000000000000000000000000000000000000000000000186a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"maxFeePerGas": null,
"maxPriorityFeePerGas": null,
"nonce": "0x1",
"r": "0xf2a3f18fd456ef9a9d6201cf622b5ad14db9cfc6786ba574e036037f80a15d61",
"s": "0x4cbb018dc0a966cd15a6bf5f3d432c72127639314d6aeb7a6bbb36000d86dc08",
"to": "0xe93c8cd0d409341205a592f8c4ac1a5fe5585cfa",
"transactionIndex": "0x7f",
"type": "0x0",
"v": "0x2d",
"value": "0x1c6bf52634000"
},
"id": 4
}
"""
},
"eth_getTransactionReceipt" => %{
action: :eth_get_transaction_receipt,
notes: """
""",
example: """
{"jsonrpc": "2.0","id": 0,"method": "eth_getTransactionReceipt","params": ["0xFE524295C6C01AB25645035A228387BF0E64C8AF429F3DD9D6EF2E3B05337839"]}
""",
params: [
%{
name: "Data",
description: "32 Bytes - transaction hash to get",
type: "string",
default: nil,
required: true
}
],
result: """
{
"jsonrpc": "2.0",
"result": {
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000040000000000000000000000000002000000000000000000000000000000000000000000000000030000000000000000000800000000000000000000000000000000000000000000000002000000008000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000002000000000000000080000000000000000000000",
"blockHash": "0x33c4ddb4478395b9d73aad2eb8640004a4a312da29ebccbaa33933a43edda019",
"blockNumber": "0x87855e",
"contractAddress": null,
"cumulativeGasUsed": "0x15a9b84",
"effectiveGasPrice": "0x195d",
"from": "0xe38ecdf3cfbaf5cf347e6a3d6490eb34e3a0119d",
"gasUsed": "0x9821",
"logs": [
{
"address": "0xe93c8cd0d409341205a592f8c4ac1a5fe5585cfa",
"blockHash": "0x33c4ddb4478395b9d73aad2eb8640004a4a312da29ebccbaa33933a43edda019",
"blockNumber": "0x87855e",
"data": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000490000000000000000000000000000000000000000000000000001c6bf526340000000000000000000000000000000000000000000000000000001c6bf5263400000000000000186a0000000000000000000000000000000000000000000000000",
"logIndex": "0xdf",
"removed": false,
"topics": [
"0xb3813568d9991fc951961fcb4c784893574240a28925604d09fc577c55bb7c32",
"0x000000000000000000000000e38ecdf3cfbaf5cf347e6a3d6490eb34e3a0119d",
"0x000000000000000000000000e38ecdf3cfbaf5cf347e6a3d6490eb34e3a0119d",
"0x0000000000000000000000000000000000000000000000000000000000000000"
],
"transactionHash": "0xfe524295c6c01ab25645035a228387bf0e64c8af429f3dd9d6ef2e3b05337839",
"transactionIndex": "0x7f"
}
],
"status": "0x1",
"to": "0xe93c8cd0d409341205a592f8c4ac1a5fe5585cfa",
"transactionHash": "0xfe524295c6c01ab25645035a228387bf0e64c8af429f3dd9d6ef2e3b05337839",
"transactionIndex": "0x7f",
"type": "0x0"
},
"id": 0
}
"""
},
"eth_chainId" => %{
action: :eth_chain_id,
notes: """
""",
example: """
{"jsonrpc": "2.0","id": 0,"method": "eth_chainId","params": []}
""",
params: [],
result: """
{
"jsonrpc": "2.0",
"id": 0,
"result": "0x5"
}
"""
},
"eth_maxPriorityFeePerGas" => %{
action: :eth_max_priority_fee_per_gas,
notes: """
""",
example: """
{"jsonrpc": "2.0","id": 0,"method": "eth_maxPriorityFeePerGas","params": []}
""",
params: [],
result: """
{
"jsonrpc": "2.0",
"id": 0,
"result": "0x3b9aca00"
}
"""
}
}
@proxy_methods %{
"eth_getTransactionCount" => %{
arity: 2,
params_validators: [&address_hash_validator/1, &block_validator/1],
example: """
{"id": 0, "jsonrpc": "2.0", "method": "eth_getTransactionCount", "params": ["0x0000000000000000000000000000000000000007", "latest"]}
""",
result: """
{"id": 0, "jsonrpc": "2.0", "result": "0x2"}
"""
},
"eth_getCode" => %{
arity: 2,
params_validators: [&address_hash_validator/1, &block_validator/1],
example: """
{"jsonrpc":"2.0","id": 0,"method":"eth_getCode","params":["0x1BF313AADe1e1f76295943f40B558Eb13Db7aA99", "latest"]}
""",
result: """
{
"jsonrpc": "2.0",
"result": "0x60806040523661001357610011610017565b005b6100115b610027610022610067565b61009f565b565b606061004e838360405180606001604052806027815260200161026b602791396100c3565b9392505050565b6001600160a01b03163b151590565b90565b600061009a7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546001600160a01b031690565b905090565b3660008037600080366000845af43d6000803e8080156100be573d6000f35b3d6000fd5b6060600080856001600160a01b0316856040516100e0919061021b565b600060405180830381855af49150503d806000811461011b576040519150601f19603f3d011682016040523d82523d6000602084013e610120565b606091505b50915091506101318683838761013b565b9695505050505050565b606083156101af5782516000036101a8576001600160a01b0385163b6101a85760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e747261637400000060448201526064015b60405180910390fd5b50816101b9565b6101b983836101c1565b949350505050565b8151156101d15781518083602001fd5b8060405162461bcd60e51b815260040161019f9190610237565b60005b838110156102065781810151838201526020016101ee565b83811115610215576000848401525b50505050565b6000825161022d8184602087016101eb565b9190910192915050565b60208152600082518060208401526102568160408501602087016101eb565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a2646970667358221220ef6e0977d993c1b69ec75a2f9fd6a53122d4ad4f9d71477641195afb6a6a45dd64736f6c634300080f0033",
"id": 0
}
"""
},
"eth_getStorageAt" => %{
arity: 3,
params_validators: [&address_hash_validator/1, &integer_validator/1, &block_validator/1],
example: """
{"jsonrpc":"2.0","id":4,"method":"eth_getStorageAt","params":["0x1643E812aE58766192Cf7D2Cf9567dF2C37e9B7F", "0x", "latest"]}
""",
result: """
{
"jsonrpc": "2.0",
"result": "0x0000000000000000000000000000000000000000000000000000000000000000",
"id": 4
}
"""
},
"eth_estimateGas" => %{
arity: 2,
params_validators: [&eth_call_validator/1, &block_validator/1],
example: """
{"jsonrpc":"2.0","id": 0,"method":"eth_estimateGas","params":[{"to": "0x1643E812aE58766192Cf7D2Cf9567dF2C37e9B7F", "input": "0xd4aae0c4", "from": "0x1643E812aE58766192Cf7D2Cf9567dF2C37e9B7F"}, "latest"]}
""",
result: """
{
"jsonrpc": "2.0",
"result": "0x5bb6",
"id": 0
}
"""
},
"eth_getBlockByNumber" => %{
arity: 2,
params_validators: [&block_validator/1, &bool_validator/1],
example: """
{"jsonrpc":"2.0","id": 0,"method":"eth_getBlockByNumber","params":["latest", false]}
""",
result: """
{
"jsonrpc": "2.0",
"result": {
"baseFeePerGas": "0x7",
"blobGasUsed": "0x0",
"difficulty": "0x0",
"excessBlobGas": "0x4bc0000",
"extraData": "0xd883010d0a846765746888676f312e32312e35856c696e7578",
"gasLimit": "0x1c9c380",
"gasUsed": "0x29b80d",
"hash": "0xbc2e3a9caf7364d306fe4af34d2e9f0a3d478ed1a8e135bf7cd0845646c858f5",
"logsBloom": "0x022100021800180480000040e0008004001044020100000204080000a20001100100100002000802c00020194040204000020010000200400000020004212000804100a4242020041800108d0228082402000040090000c80001040080000080000600000224a0b00000d88000004803000000220008014000204010100040008000804408000004200000250010400004001481a80001080080404104114040032000307022969010004000840040000322400002010108490180088040205030055002481208004903100400070000104000002008001008080010002020001818002020a04000501101080000000000201004000001400040880000000000",
"miner": "0x94750381be1aba0504c666ee1db118f68f0780d4",
"mixHash": "0xd6b01921b81abdec5eccc9f5e17246be9dfec6d3bdbf59503bdeee2db3f97a57",
"nonce": "0x0000000000000000",
"number": "0xa0ff94",
"parentBeaconBlockRoot": "0xbd4670ba8503146561cb96962185fc251e2040eed07fccc749a26b8edbfd2d1c",
"parentHash": "0x7d4de3172a22e4549b28492ab9ffe6b5bf050b82d2c9b744133657aa7ae4385d",
"receiptsRoot": "0x13ae8ce96a643074f94bc1358b1ac1a3e3660856df943b9c6b60d499386e580d",
"sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"size": "0x2d07",
"stateRoot": "0x68510947af6edb94d0d1852d881589001318872b5bad832006c569e1a4f26871",
"timestamp": "0x65d07350",
"totalDifficulty": "0xa4a470",
"transactions": [
"0xa3bb1b7bb5ee2d04114d47bbca1d8597c390e7c8ccfe04b5bfe96f6dfe897ec7",
"0xb6f680d4ba7e258e5e306744e61be1abb9b6cd005eb9423badc0b3603eb4ad5c",
"0x3c97b4ee54827e95ebf915dafdba9059ba5f4013c0371d443fe934a644725c60",
"0xd91e6db89992030da48d92825220053b1ea39f6d8d619c0f3fbc9a9e059c903e",
"0xbf763dc0a81dd2ef44f19673f001de560bce4db1499b7c0461c208afd863a62c",
"0xadd61d6e79560df74dc72891b2b19c83586d7857e313c0fdea9edbe1bfb11866",
"0xc66df09eaefac0348f48ce9e3f79e27a537bc8f274c525dd884f285d5e05bf31",
"0x217a26e8e407638e68364c2edebdae35f2a55eae080caa9ab31be430247a06e7",
"0xcae598dde02f35993cc4dad6f431596d8326a69b8f6563156edc3e970d6736d6",
"0xcfa79201e7574bce217f3f790f99bee8e0af45cffcd75ad17a9742630664df3f",
"0xe1e9b3b32e1098b3e08786407043410a6142481c1076818341cb05d7ebb3aaa3",
"0x7ce2eb696fd7c60e443bfeeeb39c2011d968b7bcfa40c20613549963f11e30a6",
"0xb4b5db2b4397e89b068ca01fc1b6bf8494a7fcd60e39e7059baef2968e874ba4",
"0x877e4ce429f4b64a095e0648b5ee69c31591116a697d03fddc5ff069302c944d",
"0xa5b8f358a3210221551250369c8dc2584c79fb424af1dd134bdab3a125eb1ea8",
"0x1c1d3df874c3ff9b84195bfe0bd5dbd50677443ff9a429bd01de4a18ccaf9293",
"0x79be4e1a433f250a35b7898916a0611f957fb7ca522836354eebfa421b2c8c99",
"0xd1c7fc2537a6627d0056e70a23bf90f988eabc518a31cd3d7520ec4ca0f9f9f0",
"0xb8c0b577257a0a184bf53454b68ce612a7567bcce48a64ee10e8b3d899c6ee16",
"0x26e81d1ba0109e5f50da13cb03d70a4fd5ffc97a0dad8e0c33fa7a8856db1480",
"0x4667088b1ab61818ebd08810a354dbe2f1ce9a4cb3f735aa692efcc8f15c7e5f"
],
"transactionsRoot": "0x9d4d5a21e9ae6294a2a197c6d051a184c109882a7f74b7e63aaf3e64e4a77a33",
"uncles": [],
"withdrawals": [
{
"address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb",
"amount": "0x13d378",
"index": "0x1cdf824",
"validatorIndex": "0xa6d24"
},
{
"address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb",
"amount": "0x124012",
"index": "0x1cdf825",
"validatorIndex": "0xa6d25"
},
{
"address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb",
"amount": "0x175e6f",
"index": "0x1cdf826",
"validatorIndex": "0xa6d26"
},
{
"address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb",
"amount": "0x16b5fe",
"index": "0x1cdf827",
"validatorIndex": "0xa6d27"
},
{
"address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb",
"amount": "0x1660d2",
"index": "0x1cdf828",
"validatorIndex": "0xa6d28"
},
{
"address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb",
"amount": "0x145405",
"index": "0x1cdf829",
"validatorIndex": "0xa6d29"
},
{
"address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb",
"amount": "0x16246d",
"index": "0x1cdf82a",
"validatorIndex": "0xa6d2a"
},
{
"address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb",
"amount": "0x14a5a1",
"index": "0x1cdf82b",
"validatorIndex": "0xa6d2b"
},
{
"address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb",
"amount": "0x142199",
"index": "0x1cdf82c",
"validatorIndex": "0xa6d2c"
},
{
"address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb",
"amount": "0x182250",
"index": "0x1cdf82d",
"validatorIndex": "0xa6d2d"
},
{
"address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb",
"amount": "0x18b97e",
"index": "0x1cdf82e",
"validatorIndex": "0xa6d2e"
},
{
"address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb",
"amount": "0x151536",
"index": "0x1cdf82f",
"validatorIndex": "0xa6d2f"
},
{
"address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb",
"amount": "0x14bc4a",
"index": "0x1cdf830",
"validatorIndex": "0xa6d30"
},
{
"address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb",
"amount": "0x162f06",
"index": "0x1cdf831",
"validatorIndex": "0xa6d31"
},
{
"address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb",
"amount": "0x13563b",
"index": "0x1cdf832",
"validatorIndex": "0xa6d32"
},
{
"address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb",
"amount": "0x148d8b",
"index": "0x1cdf833",
"validatorIndex": "0xa6d33"
}
],
"withdrawalsRoot": "0xc6a4b2cace2cc78c3a304731165b848e455fc7a3bf876837048cb4974a62c25f"
},
"id": 0
}
"""
},
"eth_getBlockByHash" => %{
arity: 2,
params_validators: [&hash_validator/1, &bool_validator/1],
example: """
{"jsonrpc":"2.0","id": 0,"method":"eth_getBlockByHash","params":["0x2980314632a35ff83ef1f26a2a972259dca49353ed9368a04f21bcd7a5512231", false]}
""",
result: """
{
"jsonrpc": "2.0",
"id": 0,
"result": {
"baseFeePerGas": "0x7",
"blobGasUsed": "0xc0000",
"difficulty": "0x0",
"excessBlobGas": "0x4b40000",
"extraData": "0x496c6c756d696e61746520446d6f63726174697a6520447374726962757465",
"gasLimit": "0x1c9c380",
"gasUsed": "0x2ff140",
"hash": "0x2980314632a35ff83ef1f26a2a972259dca49353ed9368a04f21bcd7a5512231",
"logsBloom": "0x40200000202018800808200040082040800001000040000000200984800600000200000000000810000020014000200028000000200000010530034004202010000440800d00a0000100000800000820020100009040808c80004000000040000017000003040610800002002800081a405800080060140080004a100000000308220100000400020002000100004000040412020010020000018040000000010700804008040088108001020004110008026280800021824180002c00008200a01440120000223009022014801001120080000020080000090100020000281004102000802802a1820024000c00020008000290151802004000080000000804",
"miner": "0xb64a30399f7f6b0c154c2e7af0a3ec7b0a5b131a",
"mixHash": "0xe5cf393a9e4b40800fd4e4a1d2be0de08e7aabc83de5fd16ff719680d7a04253",
"nonce": "0x0000000000000000",
"number": "0xa21bc8",
"parentBeaconBlockRoot": "0xca280fd409ee503ae331931d64ee7fc29da9ed566cba6dfc4212a2f2f8004c41",
"parentHash": "0xc2fc3c51d15a2fe6f219079694865ffd9f8fe56e714d9bc49e9451e1c430acf9",
"receiptsRoot": "0x7941071eea76cec0eb4541854bd7820ef36c4d44ae51413063b45d9ea127313d",
"sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"size": "0x34cf",
"stateRoot": "0xcff5056271c6e6f6bf04d2e82392fa3ebcf2bc4aca9fe8801edcfcc261ddb557",
"timestamp": "0x65e324a4",
"totalDifficulty": "0xa4a470",
"transactions": [
"0x204c69c327e3202adba5cfb1e15b99e63fe104905e19a2359d827788f24b0579",
"0x2d07b3bc722c139ffe2ed6a32fc56e944569cc47511fef6e0351dc1da9a23562",
"0x87772fafc7eee41d723a2dcdf2ceaae3726d40a9588ae1f7802f03dce6902fbc",
"0x2e18d4a8bd9b4d0d70651097e44b25f29b4c013ff548ea6e5f3eb975b2bdfb78",
"0xf54ef7af503a7031dc01696339cfcfee3066979a63e3ed626c15bb8282273cea",
"0xb71201da6ad30304942a308e3a7666198394f1916accc9db72d03c1b508c8065",
"0xdc854518c44ae0c3fb80b8b9fdde5da72445552356f79bbfc45d7503a32a23f8",
"0x8b866e254a609a1a4163484cf330bdfc6c6a1878cd35dcc9fbfe2256f324a626",
"0xbc7448bf0c34c0a358ead13e8d3687cbccbbb7fe4048d005cb6648c897bc9254",
"0xa01733dc6d416a59d69fe17dc9d6960dbeb013b0dab2cd59b72cf84b371d19c1",
"0x1c194a1bca34deb14e93e9007de6f971856c43a65208393254f0b2e6f99deab3",
"0x9e9c8a6094300ed29a22892e87ab7fe33d19630ccdd85a0cce72ce6095d0c7da",
"0xa1d6c2fe6a937e437cda199cc0f6891727c0f3ca810a262fb3179fd961cb95c4",
"0xc4b55bada8c0c044f1e8bbd7fb57cd3a46844848e273720fc7bbc757d8e68665",
"0x8e971964ef06896d541d5cefef7cebc79d60d6746aae2fa39e954608e2c49824",
"0x6d120cf3998a767e567ef1b6615e5a14c380103b287c92d1da229cabc49ebb77",
"0x95d1b8d32a80809d79f4a0246e960fec11b59c07f1a33207485dda0b356b3c2c",
"0xa19b4b6372a9c8145e03d62e91536468169350790162508c0f07c66849fde86d",
"0x55348af743327b1377082b9fccddfcdefe7300b65e7ed32575c09d881ece711d",
"0xaaf03a35d70aa96582889565d1211e32fc395c9e63ce82d25cc23518e38aa4bc",
"0x85054ea8eddd5b1e8d010f8aac77693484c5863d3355756a64bd0225124c8fca"
],
"transactionsRoot": "0x22309b0cc7df445160ca2c6ca344e63296231fad2e9322989477851d38c0eea0",
"uncles": [],
"withdrawals": [
{
"index": "0x1dfb534",
"validatorIndex": "0xaef81",
"address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d",
"amount": "0x1e2a5b"
},
{
"index": "0x1dfb535",
"validatorIndex": "0xaef82",
"address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d",
"amount": "0x1f9526"
},
{
"index": "0x1dfb536",
"validatorIndex": "0xaef83",
"address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d",
"amount": "0x1fa60c"
},
{
"index": "0x1dfb537",
"validatorIndex": "0xaef84",
"address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d",
"amount": "0x1e806a"
},
{
"index": "0x1dfb538",
"validatorIndex": "0xaef85",
"address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d",
"amount": "0x1eb4e4"
},
{
"index": "0x1dfb539",
"validatorIndex": "0xaef86",
"address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d",
"amount": "0x2054a0"
},
{
"index": "0x1dfb53a",
"validatorIndex": "0xaef87",
"address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d",
"amount": "0x1d984a"
},
{
"index": "0x1dfb53b",
"validatorIndex": "0xaef88",
"address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d",
"amount": "0x1fa4d4"
},
{
"index": "0x1dfb53c",
"validatorIndex": "0xaef89",
"address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d",
"amount": "0x203a98"
},
{
"index": "0x1dfb53d",
"validatorIndex": "0xaef8a",
"address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d",
"amount": "0x1fec28"
},
{
"index": "0x1dfb53e",
"validatorIndex": "0xaef8b",
"address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d",
"amount": "0x2025a5"
},
{
"index": "0x1dfb53f",
"validatorIndex": "0xaef8c",
"address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d",
"amount": "0x1fdb08"
},
{
"index": "0x1dfb540",
"validatorIndex": "0xaef8d",
"address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d",
"amount": "0x200a11"
},
{
"index": "0x1dfb541",
"validatorIndex": "0xaef8e",
"address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d",
"amount": "0x1f03d5"
},
{
"index": "0x1dfb542",
"validatorIndex": "0xaef8f",
"address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d",
"amount": "0x200804"
},
{
"index": "0x1dfb543",
"validatorIndex": "0xaef90",
"address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d",
"amount": "0x1dd0bb"
}
],
"withdrawalsRoot": "0xcba66455c17861d36575f98adedc90b1fc56bbef7982992cab6914528dbd0100"
}
}
"""
},
"eth_sendRawTransaction" => %{
arity: 1,
params_validators: [&hex_data_validator/1],
example: """
{"jsonrpc":"2.0","id": 0,"method":"eth_sendRawTransaction","params":["0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"]}
""",
result: """
{
"jsonrpc": "2.0",
"id": 0,
"result": "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1527331"
}
"""
},
"eth_call" => %{
arity: 2,
params_validators: [&eth_call_validator/1, &block_validator/1],
example: """
{"jsonrpc":"2.0","id": 0,"method":"eth_call","params":[{"to": "0x1643E812aE58766192Cf7D2Cf9567dF2C37e9B7F", "input": "0xd4aae0c4", "from": "0x1643E812aE58766192Cf7D2Cf9567dF2C37e9B7F"}, "latest"]}
""",
result: """
{
"jsonrpc": "2.0",
"result": "0x0000000000000000000000001dd91b354ebd706ab3ac7c727455c7baa164945a",
"id": 0
}
"""
}
}
@ -94,18 +655,125 @@ defmodule Explorer.EthRPC do
3 => "fourth"
}
@incorrect_number_of_params "Incorrect number of params."
@spec responses([map()]) :: [map()]
def responses(requests) do
Enum.map(requests, fn request ->
with {:id, {:ok, id}} <- {:id, Map.fetch(request, "id")},
requests =
requests
|> Enum.with_index()
proxy_requests =
requests
|> Enum.reduce(%{}, fn {request, index}, acc ->
case proxy_method?(request) do
true ->
Map.put(acc, index, request)
{:error, _reason} = error ->
Map.put(acc, index, error)
false ->
acc
end
end)
|> json_rpc()
Enum.map(requests, fn {request, index} ->
with {:proxy, nil} <- {:proxy, proxy_requests[index]},
{:id, {:ok, id}} <- {:id, Map.fetch(request, "id")},
{:request, {:ok, result}} <- {:request, do_eth_request(request)} do
format_success(result, id)
else
{:id, :error} -> format_error("id is a required field", 0)
{:request, {:error, message}} -> format_error(message, Map.get(request, "id"))
{:proxy, {:error, message}} -> format_error(message, Map.get(request, "id"))
{:proxy, %{result: result}} -> format_success(result, Map.get(request, "id"))
{:proxy, %{error: error}} -> format_error(error, Map.get(request, "id"))
end
end)
end
defp proxy_method?(%{"jsonrpc" => "2.0", "method" => method, "params" => params, "id" => id})
when is_list(params) and (is_number(id) or is_binary(id) or is_nil(id)) do
with method_definition when not is_nil(method_definition) <- @proxy_methods[method],
{:arity, true} <- {:arity, method_definition[:arity] == length(params)},
:ok <- validate_params(method_definition[:params_validators], params) do
true
else
{:error, _reason} = error ->
error
{:arity, false} ->
{:error, @incorrect_number_of_params}
_ ->
false
end
end
defp proxy_method?(_), do: false
defp validate_params(validators, params) do
validators
|> Enum.zip(params)
|> Enum.reduce_while(:ok, fn
{validator_func, param}, :ok ->
{:cont, validator_func.(param)}
_, error ->
{:halt, error}
end)
end
defp json_rpc(map) when is_map(map) do
to_request =
Enum.flat_map(Map.values(map), fn
{:error, _} ->
[]
map when is_map(map) ->
[request_to_elixir(map)]
end)
with [_ | _] = to_request <- to_request,
{:ok, responses} <-
EthereumJSONRPC.json_rpc(to_request, Application.get_env(:explorer, :json_rpc_named_arguments)) do
{map, []} =
Enum.map_reduce(map, responses, fn
{_index, {:error, _}} = elem, responses ->
{elem, responses}
{index, _request}, [response | other_responses] ->
{{index, response}, other_responses}
end)
Enum.into(map, %{})
else
[] ->
map
{:error, _reason} = error ->
map
|> Enum.map(fn
{_index, {:error, _}} = elem ->
elem
{index, _request} ->
{index, error}
end)
|> Enum.into(%{})
end
end
defp request_to_elixir(%{"jsonrpc" => json_rpc, "method" => method, "params" => params, "id" => id}) do
%{jsonrpc: json_rpc, method: method, params: params, id: id}
end
@doc """
Handles `eth_blockNumber` method
"""
@spec eth_block_number() :: {:ok, String.t()}
def eth_block_number do
max_block_number = BlockNumber.get_max()
@ -116,6 +784,10 @@ defmodule Explorer.EthRPC do
{:ok, max_block_number_hex}
end
@doc """
Handles `eth_getBalance` method
"""
@spec eth_get_balance(String.t(), String.t() | nil) :: {:ok, String.t()} | {:error, String.t()}
def eth_get_balance(address_param, block_param \\ nil) do
with {:address, {:ok, address}} <- {:address, Chain.string_to_address_hash(address_param)},
{:block, {:ok, block}} <- {:block, block_param(block_param)},
@ -133,6 +805,126 @@ defmodule Explorer.EthRPC do
end
end
@doc """
Handles `eth_gasPrice` method
"""
@spec eth_gas_price() :: {:ok, String.t()} | {:error, String.t()}
def eth_gas_price do
case GasPriceOracle.get_gas_prices() do
{:ok, gas_prices} ->
{:ok, Wei.hex_format(gas_prices[:average][:wei])}
_ ->
{:error, @nil_gas_price_message}
end
end
@doc """
Handles `eth_maxPriorityFeePerGas` method
"""
@spec eth_max_priority_fee_per_gas() :: {:ok, String.t()} | {:error, String.t()}
def eth_max_priority_fee_per_gas do
case GasPriceOracle.get_gas_prices() do
{:ok, gas_prices} ->
{:ok, Wei.hex_format(gas_prices[:average][:priority_fee_wei])}
_ ->
{:error, @nil_gas_price_message}
end
end
@doc """
Handles `eth_chainId` method
"""
@spec eth_chain_id() :: {:ok, String.t() | nil}
def eth_chain_id do
{:ok, chain_id()}
end
@doc """
Handles `eth_getTransactionByHash` method
"""
@spec eth_get_transaction_by_hash(String.t()) :: {:ok, map() | nil} | {:error, String.t()}
def eth_get_transaction_by_hash(transaction_hash_string) do
validate_and_render_transaction(transaction_hash_string, &render_transaction/1, api?: true)
end
defp render_transaction(transaction) do
{:ok,
%{
"blockHash" => transaction.block_hash,
"blockNumber" => encode_quantity(transaction.block_number),
"from" => transaction.from_address_hash,
"gas" => encode_quantity(transaction.gas),
"gasPrice" => transaction.gas_price |> Wei.to(:wei) |> encode_quantity(),
"maxPriorityFeePerGas" => transaction.max_priority_fee_per_gas |> Wei.to(:wei) |> encode_quantity(),
"maxFeePerGas" => transaction.max_fee_per_gas |> Wei.to(:wei) |> encode_quantity(),
"hash" => transaction.hash,
"input" => transaction.input,
"nonce" => encode_quantity(transaction.nonce),
"to" => transaction.to_address_hash,
"transactionIndex" => encode_quantity(transaction.index),
"value" => transaction.value |> Wei.to(:wei) |> encode_quantity(),
"type" => encode_quantity(transaction.type),
"chainId" => chain_id(),
"v" => encode_quantity(transaction.v),
"r" => encode_quantity(transaction.r),
"s" => encode_quantity(transaction.s)
}}
end
@doc """
Handles `eth_getTransactionReceipt` method
"""
@spec eth_get_transaction_receipt(String.t()) :: {:ok, map() | nil} | {:error, String.t()}
def eth_get_transaction_receipt(transaction_hash_string) do
necessity_by_association = %{block: :optional, logs: :optional}
validate_and_render_transaction(transaction_hash_string, &render_transaction_receipt/1,
api?: true,
necessity_by_association: necessity_by_association
)
end
defp render_transaction_receipt(transaction) do
{:ok, status} = Status.dump(transaction.status)
{:ok,
%{
"blockHash" => transaction.block_hash,
"blockNumber" => encode_quantity(transaction.block_number),
"contractAddress" => transaction.created_contract_address_hash,
"cumulativeGasUsed" => encode_quantity(transaction.cumulative_gas_used),
"effectiveGasPrice" =>
(transaction.gas_price || transaction |> Transaction.effective_gas_price())
|> Wei.to(:wei)
|> encode_quantity(),
"from" => transaction.from_address_hash,
"gasUsed" => encode_quantity(transaction.gas_used),
"logs" => Enum.map(transaction.logs, &render_log(&1, transaction)),
'logsBloom' => "0x" <> (transaction.logs |> BloomFilter.logs_bloom() |> Base.encode16(case: :lower)),
"status" => encode_quantity(status),
"to" => transaction.to_address_hash,
"transactionHash" => transaction.hash,
"transactionIndex" => encode_quantity(transaction.index),
"type" => encode_quantity(transaction.type)
}}
end
defp validate_and_render_transaction(transaction_hash_string, render_func, params) do
with {:transaction_hash, {:ok, transaction_hash}} <-
{:transaction_hash, Chain.string_to_transaction_hash(transaction_hash_string)},
{:transaction, {:ok, transaction}} <- {:transaction, Chain.hash_to_transaction(transaction_hash, params)} do
render_func.(transaction)
else
{:transaction_hash, :error} ->
{:error, "Transaction hash is invalid"}
{:transaction, _} ->
{:ok, nil}
end
end
def eth_get_logs(filter_options) do
with {:ok, address_or_topic_params} <- address_or_topic_params(filter_options),
{:ok, from_block_param, to_block_param} <- logs_blocks_filter(filter_options),
@ -164,11 +956,7 @@ defmodule Explorer.EthRPC do
end
defp render_log(log) do
topics =
Enum.reject(
[log.first_topic, log.second_topic, log.third_topic, log.fourth_topic],
&is_nil/1
)
topics = prepare_topics(log)
%{
"address" => to_string(log.address_hash),
@ -189,6 +977,37 @@ defmodule Explorer.EthRPC do
}
end
defp render_log(log, transaction) do
topics = prepare_topics(log)
%{
"address" => log.address_hash,
"blockHash" => log.block_hash,
"blockNumber" => encode_quantity(log.block_number),
"data" => log.data,
"logIndex" => encode_quantity(log.index),
"removed" => transaction_consensus(transaction) == false,
"topics" => topics,
"transactionHash" => log.transaction_hash,
"transactionIndex" => encode_quantity(transaction.index)
}
end
defp transaction_consensus(transaction) do
if DenormalizationHelper.transactions_denormalization_finished?() do
transaction.block_consensus
else
transaction.block.consensus
end
end
defp prepare_topics(log) do
Enum.reject(
[log.first_topic, log.second_topic, log.third_topic, log.fourth_topic],
&is_nil/1
)
end
defp cast_block("0x" <> hexadecimal_digits = input) do
case Integer.parse(hexadecimal_digits, 16) do
{integer, ""} -> {:ok, integer}
@ -429,6 +1248,8 @@ defmodule Explorer.EthRPC do
defp block_param(nil), do: {:ok, :latest}
defp block_param(_), do: :error
def encode_quantity(%Decimal{} = decimal), do: encode_quantity(Decimal.to_integer(decimal))
def encode_quantity(binary) when is_binary(binary) do
hex_binary = Base.encode16(binary, case: :lower)
@ -450,4 +1271,6 @@ defmodule Explorer.EthRPC do
end
def methods, do: @methods
defp chain_id, do: :block_scout_web |> Application.get_env(:chain_id) |> Helper.parse_integer() |> encode_quantity()
end

@ -0,0 +1,121 @@
defmodule Explorer.EthRpcHelper do
@moduledoc """
Helper module for Explorer.EthRPC. Mostly contains functions to validate input args
"""
alias Explorer.Chain.{Data, Hash.Address}
alias Explorer.Chain.Hash.Full, as: Hash
@invalid_address "Invalid address"
@invalid_block_number "Invalid block number"
@invalid_integer "Invalid integer"
@missed_to_address "Missed `to` address"
@invalid_bool "Invalid bool"
@invalid_hash "Invalid hash"
@invalid_hex_data "Invalid hex data"
@doc """
Validates if address is valid
"""
@spec address_hash_validator(binary(), String.t()) :: :ok | {:error, String.t()}
def address_hash_validator(address_hash, message \\ @invalid_address) do
case Address.cast(address_hash) do
{:ok, _} -> :ok
:error -> {:error, message}
end
end
@doc """
Validates if hash is valid
"""
@spec hash_validator(binary()) :: :ok | {:error, String.t()}
def hash_validator(hash) do
case Hash.cast(hash) do
{:ok, _} -> :ok
:error -> {:error, @invalid_hash}
end
end
@doc """
Validates if hex data is valid
"""
@spec hex_data_validator(binary()) :: :ok | {:error, String.t()}
def hex_data_validator(hex_data) do
case Data.cast(hex_data) do
{:ok, _} -> :ok
_ -> {:error, @invalid_hex_data}
end
end
@doc """
Validates if block is valid
"""
@spec block_validator(binary()) :: :ok | {:error, String.t()}
def block_validator(block_tag) when block_tag in ["latest", "earliest", "pending"], do: :ok
def block_validator(block_number) do
parse_integer(block_number) || {:error, @invalid_block_number}
end
def integer_validator(hex) do
parse_integer(hex) || {:error, @invalid_integer}
end
@doc """
Validates eth_call map
"""
@spec eth_call_validator(map()) :: :ok | {:error, String.t()}
def eth_call_validator(%{"to" => to_address} = eth_call) do
with :ok <- address_hash_validator(to_address, "Invalid `to` address"),
:ok <- validate_optional_address(eth_call["from"], "from"),
:ok <- validate_optional_integer(eth_call["gas"], "gas"),
:ok <- validate_optional_integer(eth_call["gasPrice"], "gasPrice"),
:ok <- validate_optional_integer(eth_call["value"], "value"),
:ok <- validate_optional_hex_data(eth_call["input"], "input") do
:ok
else
error ->
error
end
end
def eth_call_validator(_), do: {:error, @missed_to_address}
@doc """
Validates if bool is valid
"""
@spec bool_validator(boolean()) :: :ok | {:error, String.t()}
def bool_validator(bool) when is_boolean(bool), do: :ok
def bool_validator(_), do: {:error, @invalid_bool}
defp validate_optional_address(nil, _), do: :ok
defp validate_optional_address(address_hash, field_name) do
address_hash_validator(address_hash, "Invalid `#{field_name}` address")
end
defp validate_optional_integer(nil, _), do: :ok
defp validate_optional_integer(integer, field_name) do
parse_integer(integer) || {:error, "Invalid `#{field_name}` quantity"}
end
defp parse_integer("0x"), do: :ok
defp parse_integer("0x" <> hex_integer) do
case Integer.parse(hex_integer, 16) do
{_integer, ""} -> :ok
_ -> nil
end
end
defp parse_integer(_), do: nil
defp validate_optional_hex_data(nil, _), do: :ok
defp validate_optional_hex_data(data, field_name) do
case Data.cast(data) do
{:ok, _} -> :ok
_ -> {:error, "Invalid `#{field_name}` data"}
end
end
end

@ -0,0 +1,63 @@
defmodule Explorer.BloomFilterTest do
use Explorer.DataCase
alias Explorer.BloomFilter
describe "Test bloom filter for random Goerli transactions" do
# {"jsonrpc":"2.0","id": 0,"method":"eth_getTransactionReceipt","params":["0xFE524295C6C01AB25645035A228387BF0E64C8AF429F3DD9D6EF2E3B05337839"]}
test "#1 (0xFE524295C6C01AB25645035A228387BF0E64C8AF429F3DD9D6EF2E3B05337839)" do
log_1 =
insert(:log,
first_topic: "0xb3813568d9991fc951961fcb4c784893574240a28925604d09fc577c55bb7c32",
second_topic: "0x000000000000000000000000e38ecdf3cfbaf5cf347e6a3d6490eb34e3a0119d",
third_topic: "0x000000000000000000000000e38ecdf3cfbaf5cf347e6a3d6490eb34e3a0119d",
fourth_topic: "0x0000000000000000000000000000000000000000000000000000000000000000",
address_hash: "0xe93c8cd0d409341205a592f8c4ac1a5fe5585cfa",
address: nil
)
assert BloomFilter.logs_bloom([log_1]) |> Base.encode16(case: :lower) ==
"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000040000000000000000000000000002000000000000000000000000000000000000000000000000030000000000000000000800000000000000000000000000000000000000000000000002000000008000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000002000000000000000080000000000000000000000"
end
test "#2 (0x9f44d7080354147fc42ee0eb62c8f77d0477e7686d18debcb81f90b0d54ea1d1)" do
log_1 =
insert(:log,
first_topic: "0x9866f8ddfe70bb512b2f2b28b49d4017c43f7ba775f1a20c61c13eea8cdac111",
second_topic: nil,
third_topic: nil,
fourth_topic: nil,
address_hash: "0xd5c325d183c592c94998000c5e0eed9e6655c020",
address: nil
)
log_2 =
insert(:log,
first_topic: "0xd342ddf7a308dec111745b00315c14b7efb2bdae570a6856e088ed0c65a3576c",
second_topic: nil,
third_topic: nil,
fourth_topic: nil,
address_hash: "0xd5c325d183c592c94998000c5e0eed9e6655c020",
address: nil
)
assert BloomFilter.logs_bloom([log_1, log_2]) |> Base.encode16(case: :lower) ==
"00000000010002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000800000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000020000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000"
end
test "#3 (0x2548b6514211bafdfeff37dc184c4700c8ca7056ac2a119bef5a98f8a79662cc)" do
log_1 =
insert(:log,
first_topic: "0x1a37b94876a9c4d5697c33a6fc124022beba6ce60e84469f41d49536d2a55924",
second_topic: "0x000000000000000000000000000000000000000000000000000000000001ba63",
third_topic: "0x00000000000000000000000000000000000000000000000000f8b0a10e470000",
fourth_topic: "0x0000000000000000000000000000000000000000000000000000000000000002",
address_hash: "0x2a5ccc8813d89119263b49f567c541e925c75f13",
address: nil
)
assert BloomFilter.logs_bloom([log_1]) |> Base.encode16(case: :lower) ==
"04000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000420000000000000000000000800000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000020000000000000000000000000000000100000000000100000000000002010000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000010000000000000"
end
end
end

@ -103,7 +103,8 @@ config :block_scout_web, :api_rate_limit,
whitelisted_ips: System.get_env("API_RATE_LIMIT_WHITELISTED_IPS"),
is_blockscout_behind_proxy: ConfigHelper.parse_bool_env_var("API_RATE_LIMIT_IS_BLOCKSCOUT_BEHIND_PROXY"),
api_v2_ui_limit: ConfigHelper.parse_integer_env_var("API_RATE_LIMIT_UI_V2_WITH_TOKEN", 5),
api_v2_token_ttl_seconds: ConfigHelper.parse_integer_env_var("API_RATE_LIMIT_UI_V2_TOKEN_TTL_IN_SECONDS", 18000)
api_v2_token_ttl_seconds: ConfigHelper.parse_integer_env_var("API_RATE_LIMIT_UI_V2_TOKEN_TTL_IN_SECONDS", 18000),
eth_json_rpc_max_batch_size: ConfigHelper.parse_integer_env_var("ETH_JSON_RPC_MAX_BATCH_SIZE", 5)
# Configures History
price_chart_config =

Loading…
Cancel
Save