defmodule EthereumJSONRPC do @moduledoc """ Ethereum JSONRPC client. ## Configuration Configuration for Nethermind URLs can be provided with the following mix config: config :ethereum_jsonrpc, url: "http://localhost:8545", trace_url: "http://localhost:8545", http: [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :ethereum_jsonrpc]] Note: the tracing node URL is provided separately from `:url`, via `:trace_url`. The trace URL and is used for `fetch_internal_transactions`, which is only a supported method on tracing nodes. The `:http` option is passed directly to the HTTP library (`HTTPoison`), which forwards the options down to `:hackney`. ## Throttling Requests for fetching blockchain can put a lot of CPU pressure on JSON RPC nodes. EthereumJSONRPC will check for request timeouts as well as bad-gateway responses and add delay between requests until the JSON RPC nodes reach stability. For finer tuning and configuration of throttling, read the documentation for `EthereumJSONRPC.RequestCoordinator`. """ require Logger alias EthereumJSONRPC.{ Block, Blocks, Contract, FetchedBalances, FetchedBeneficiaries, FetchedCodes, Receipts, RequestCoordinator, Subscription, Transport, Utility.CommonHelper, Utility.EndpointAvailabilityObserver, Utility.RangesHelper, Variant } @default_throttle_timeout :timer.minutes(2) @typedoc """ Truncated 20-byte [KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash encoded as a hexadecimal number in a `String.t`. """ @type address :: String.t() @typedoc """ A block number as an Elixir `t:non_neg_integer/0` instead of `t:data/0`. """ @type block_number :: non_neg_integer() @typedoc """ Reference to an uncle block by nephew block's `hash` and `index` in it. """ @type nephew_index :: %{required(:nephew_hash) => String.t(), required(:index) => non_neg_integer()} @typedoc """ Binary data encoded as a single hexadecimal number in a `String.t` """ @type data :: String.t() @typedoc """ Contract code encoded as a single hexadecimal number in a `String.t` """ @type code :: String.t() @typedoc """ A full 32-byte [KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash encoded as a hexadecimal number in a `String.t` ## Example "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1527331" """ @type hash :: String.t() @typedoc """ Named arguments to `json_rpc/2`. * `:transport` - the `t:EthereumJSONRPC.Transport.t/0` callback module * `:transport_options` - options passed to `c:EthereumJSONRPC.Transport.json_rpc/2` * `:variant` - the `t:EthereumJSONRPC.Variant.t/0` callback module * `:throttle_timeout` - the maximum amount of time in milliseconds to throttle before automatically returning a timeout. Defaults to #{@default_throttle_timeout} milliseconds. """ @type json_rpc_named_arguments :: [ {:transport, Transport.t()} | {:transport_options, Transport.options()} | {:variant, Variant.t()} | {:throttle_timeout, non_neg_integer()} ] @typedoc """ Named arguments to `subscribe/2`. * `:transport` - the `t:EthereumJSONRPC.Transport.t/0` callback module * `:transport_options` - options passed to `c:EthereumJSONRPC.Transport.json_rpc/2` * `:variant` - the `t:EthereumJSONRPC.Variant.t/0` callback module """ @type subscribe_named_arguments :: [ {:transport, Transport.t()} | {:transport_options, Transport.options()} | {:variant, Variant.t()} ] @typedoc """ 8 byte [KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash of the proof-of-work. """ @type nonce :: String.t() @typedoc """ A number encoded as a hexadecimal number in a `String.t` ## Example "0x1b4" """ @type quantity :: String.t() @typedoc """ A logic block tag that can be used in place of a block number. | Tag | Description | |--------------|--------------------------------| | `"earliest"` | The first block in the chain | | `"latest"` | The latest collated block. | | `"pending"` | The next block to be collated. | """ @type tag :: String.t() @typedoc """ Unix timestamp encoded as a hexadecimal number in a `String.t` """ @type timestamp :: String.t() @typedoc """ JSONRPC request id can be a `String.t` or Integer """ @type request_id :: String.t() | non_neg_integer() @doc """ Execute smart contract functions. Receives a list of smart contract functions to execute. Each function is represented by a map. The contract_address key is the address of the smart contract being queried, the data key indicates which function should be executed, as well as what are their arguments, and the id key is the id that is going to be sent with the JSON-RPC call. ## Examples Execute the "sum" function that receives two arguments (20 and 22) and returns their sum (42): iex> EthereumJSONRPC.execute_contract_functions([%{ ...> contract_address: "0x7e50612682b8ee2a8bb94774d50d6c2955726526", ...> data: "0xcad0899b00000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000016", ...> id: "sum" ...> }]) {:ok, [ %{ "id" => "sum", "jsonrpc" => "2.0", "result" => "0x000000000000000000000000000000000000000000000000000000000000002a" } ]} """ @spec execute_contract_functions([Contract.call()], [map()], json_rpc_named_arguments) :: [Contract.call_result()] def execute_contract_functions(functions, abi, json_rpc_named_arguments, leave_error_as_map \\ false) do if Enum.empty?(functions) do [] else Contract.execute_contract_functions(functions, abi, json_rpc_named_arguments, leave_error_as_map) end end @doc """ Fetches balance for each address `hash` at the `block_number` """ @spec fetch_balances( [%{required(:block_quantity) => quantity, required(:hash_data) => data()}], json_rpc_named_arguments ) :: {:ok, FetchedBalances.t()} | {:error, reason :: term} def fetch_balances(params_list, json_rpc_named_arguments, latest_block_number \\ 0, chunk_size \\ nil) when is_list(params_list) and is_list(json_rpc_named_arguments) do latest_block_number_params = case latest_block_number do 0 -> fetch_block_number_by_tag("latest", json_rpc_named_arguments) number -> {:ok, number} end params_in_range = params_list |> Enum.filter(fn %{block_quantity: block_quantity} -> block_quantity |> quantity_to_integer() |> RangesHelper.traceable_block_number?() end) trace_url_used? = !is_nil(json_rpc_named_arguments[:transport_options][:method_to_url][:eth_getBalance]) archive_disabled? = Application.get_env(:ethereum_jsonrpc, :disable_archive_balances?) {latest_balances_params, archive_balance_params} = with true <- not trace_url_used? or archive_disabled?, {:ok, max_block_number} <- latest_block_number_params do window = Application.get_env(:ethereum_jsonrpc, :archive_balances_window) Enum.split_with(params_in_range, fn %{block_quantity: "latest"} -> true %{block_quantity: block_quantity} -> quantity_to_integer(block_quantity) > max_block_number - window _ -> false end) else _ -> {params_in_range, []} end latest_id_to_params = id_to_params(latest_balances_params) archive_id_to_params = id_to_params(archive_balance_params) with {:ok, latest_responses} <- do_balances_request(latest_id_to_params, chunk_size, json_rpc_named_arguments), {:ok, archive_responses} <- maybe_request_archive_balances( archive_id_to_params, trace_url_used?, archive_disabled?, chunk_size, json_rpc_named_arguments ) do latest_fetched_balances = FetchedBalances.from_responses(latest_responses, latest_id_to_params) archive_fetched_balances = FetchedBalances.from_responses(archive_responses, archive_id_to_params) {:ok, FetchedBalances.merge(latest_fetched_balances, archive_fetched_balances)} end end @doc """ Fetches contract code for multiple addresses at specified block numbers. This function takes a list of parameters, each containing an address and a block number, and retrieves the contract code for each address at the specified block. ## Parameters - `params_list`: A list of maps, each containing: - `:block_quantity`: The block number (as a quantity string) at which to fetch the code. - `:address`: The address of the contract to fetch the code for. - `json_rpc_named_arguments`: A keyword list of JSON-RPC configuration options. ## Returns - `{:ok, fetched_codes}`, where `fetched_codes` is a `FetchedCodes.t()` struct containing: - `params_list`: A list of successfully fetched code parameters, each containing: - `address`: The contract address. - `block_number`: The block number at which the code was fetched. - `code`: The fetched contract code in hexadecimal format. - `errors`: A list of errors encountered during the fetch operation. - `{:error, reason}`: An error occurred during the fetch operation. """ @spec fetch_codes( [%{required(:block_quantity) => quantity, required(:address) => address()}], json_rpc_named_arguments ) :: {:ok, FetchedCodes.t()} | {:error, reason :: term} def fetch_codes(params_list, json_rpc_named_arguments) when is_list(params_list) and is_list(json_rpc_named_arguments) do id_to_params = id_to_params(params_list) with {:ok, responses} <- id_to_params |> FetchedCodes.requests() |> json_rpc(json_rpc_named_arguments) do {:ok, FetchedCodes.from_responses(responses, id_to_params)} end end @doc """ Fetches block reward contract beneficiaries from variant API. """ @spec fetch_beneficiaries([block_number], json_rpc_named_arguments) :: {:ok, FetchedBeneficiaries.t()} | {:error, reason :: term} | :ignore def fetch_beneficiaries(block_numbers, json_rpc_named_arguments) when is_list(block_numbers) do filtered_block_numbers = RangesHelper.filter_traceable_block_numbers(block_numbers) Keyword.fetch!(json_rpc_named_arguments, :variant).fetch_beneficiaries( filtered_block_numbers, json_rpc_named_arguments ) end @doc """ Fetches blocks by block hashes. Transaction data is included for each block. """ @spec fetch_blocks_by_hash([hash()], json_rpc_named_arguments) :: {:ok, Blocks.t()} | {:error, reason :: term} def fetch_blocks_by_hash(block_hashes, json_rpc_named_arguments) do block_hashes |> Enum.map(fn block_hash -> %{hash: block_hash} end) |> fetch_blocks_by_params(&Block.ByHash.request/1, json_rpc_named_arguments) end @doc """ Fetches blocks by block number range. """ @spec fetch_blocks_by_range(Range.t(), json_rpc_named_arguments) :: {:ok, Blocks.t()} | {:error, reason :: term} def fetch_blocks_by_range(_first.._last//_ = range, json_rpc_named_arguments) do range |> Enum.map(fn number -> %{number: number} end) |> fetch_blocks_by_params(&Block.ByNumber.request/1, json_rpc_named_arguments) end @doc """ Fetches blocks by block number list. """ @spec fetch_blocks_by_numbers([block_number()], json_rpc_named_arguments, boolean()) :: {:ok, Blocks.t()} | {:error, reason :: term} def fetch_blocks_by_numbers(block_numbers, json_rpc_named_arguments, with_transactions? \\ true) do block_numbers |> Enum.map(fn number -> %{number: number} end) |> fetch_blocks_by_params(&Block.ByNumber.request(&1, with_transactions?), json_rpc_named_arguments) end @doc """ Fetches block by "t:tag/0". """ @spec fetch_block_by_tag(tag(), json_rpc_named_arguments) :: {:ok, Blocks.t()} | {:error, reason :: :invalid_tag | :not_found | term()} def fetch_block_by_tag(tag, json_rpc_named_arguments) when tag in ~w(earliest latest pending safe) do [%{tag: tag}] |> fetch_blocks_by_params(&Block.ByTag.request/1, json_rpc_named_arguments) end @doc """ Fetches uncle blocks by nephew hashes and indices. """ @spec fetch_uncle_blocks([nephew_index()], json_rpc_named_arguments) :: {:ok, Blocks.t()} | {:error, reason :: term} def fetch_uncle_blocks(blocks, json_rpc_named_arguments) do blocks |> fetch_blocks_by_params(&Block.ByNephew.request/1, json_rpc_named_arguments) end @spec fetch_net_version(json_rpc_named_arguments) :: {:ok, non_neg_integer()} | {:error, reason :: term} def fetch_net_version(json_rpc_named_arguments) do result = %{id: 0, method: "net_version", params: []} |> request() |> json_rpc(json_rpc_named_arguments) case result do {:ok, bin_number} -> {:ok, String.to_integer(bin_number)} other -> other end end @doc """ Fetches block number by `t:tag/0`. ## Returns * `{:ok, number}` - the block number for the given `tag`. * `{:error, :invalid_tag}` - When `tag` is not a valid `t:tag/0`. * `{:error, reason}` - other JSONRPC error. """ @spec fetch_block_number_by_tag_op_version(tag(), json_rpc_named_arguments) :: {:ok, non_neg_integer()} | {:error, reason :: :invalid_tag | :not_found | term()} def fetch_block_number_by_tag_op_version(tag, json_rpc_named_arguments) when tag in ~w(earliest latest pending safe) do %{id: 0, tag: tag} |> Block.ByTag.request() |> json_rpc(json_rpc_named_arguments) |> Block.ByTag.number_from_result() end @spec fetch_block_number_by_tag(tag(), json_rpc_named_arguments) :: {:ok, non_neg_integer()} | {:error, reason :: :invalid_tag | :not_found | term()} def fetch_block_number_by_tag(tag, json_rpc_named_arguments) when tag in ~w(earliest latest pending safe) do tag |> fetch_block_by_tag(json_rpc_named_arguments) |> Block.ByTag.number_from_result() end @doc """ Fetches internal transactions from variant API. """ def fetch_internal_transactions(params_list, json_rpc_named_arguments) when is_list(params_list) do Keyword.fetch!(json_rpc_named_arguments, :variant).fetch_internal_transactions( params_list, json_rpc_named_arguments ) end @doc """ Fetches internal transactions for entire blocks from variant API. """ def fetch_block_internal_transactions(block_numbers, json_rpc_named_arguments) when is_list(block_numbers) do filtered_block_numbers = RangesHelper.filter_traceable_block_numbers(block_numbers) Keyword.fetch!(json_rpc_named_arguments, :variant).fetch_block_internal_transactions( filtered_block_numbers, json_rpc_named_arguments ) end @doc """ Retrieves traces from variant API. """ def fetch_first_trace(params_list, json_rpc_named_arguments) when is_list(params_list) do Keyword.fetch!(json_rpc_named_arguments, :variant).fetch_first_trace( params_list, json_rpc_named_arguments ) end @doc """ Fetches pending transactions from variant API. """ def fetch_pending_transactions(json_rpc_named_arguments) do Keyword.fetch!(json_rpc_named_arguments, :variant).fetch_pending_transactions(json_rpc_named_arguments) end @doc """ Retrieves raw traces from Ethereum JSON RPC variant API. """ def fetch_transaction_raw_traces(transaction_params, json_rpc_named_arguments) do Keyword.fetch!(json_rpc_named_arguments, :variant).fetch_transaction_raw_traces( transaction_params, json_rpc_named_arguments ) end @spec fetch_transaction_receipts( [ %{required(:gas) => non_neg_integer(), required(:hash) => hash, optional(atom) => any} ], json_rpc_named_arguments ) :: {:ok, %{logs: list(), receipts: list()}} | {:error, reason :: term} def fetch_transaction_receipts(transactions_params, json_rpc_named_arguments) when is_list(transactions_params) do Receipts.fetch(transactions_params, json_rpc_named_arguments) end @doc """ Assigns a unique integer ID to each set of parameters in the given list. This function is used to prepare parameters for batch request-response correlation in JSON-RPC calls. ## Parameters - `params_list`: A list of parameter sets, where each set can be of any type. ## Returns A map where the keys are integer IDs (starting from 0) and the values are the corresponding parameter sets from the input list. ## Example iex> id_to_params([%{block: 1}, %{block: 2}]) %{0 => %{block: 1}, 1 => %{block: 2}} """ @spec id_to_params([params]) :: %{id => params} when id: non_neg_integer(), params: any() def id_to_params(params_list) do params_list |> Stream.with_index() |> Enum.into(%{}, fn {params, id} -> {id, params} end) end @doc """ Sanitizes responses by assigning unmatched IDs to responses with missing IDs. This function processes a list of responses and a map of expected IDs to parameters. It handles cases where responses have missing (nil) IDs by assigning them unmatched IDs from the id_to_params map. ## Parameters - `responses`: A list of response maps from a batch JSON-RPC call. - `id_to_params`: A map of request IDs to their corresponding parameters. ## Returns A list of sanitized response maps where each response has a valid ID. ## Example iex> responses = [%{id: 1, result: "ok"}, %{id: nil, result: "error"}] iex> id_to_params = %{1 => %{}, 2 => %{}, 3 => %{}} iex> EthereumJSONRPC.sanitize_responses(responses, id_to_params) [%{id: 1, result: "ok"}, %{id: 2, result: "error"}] """ @spec sanitize_responses(Transport.batch_response(), %{id => params}) :: Transport.batch_response() when id: EthereumJSONRPC.request_id(), params: any() def sanitize_responses(responses, id_to_params) do responses |> Enum.reduce( {[], Map.keys(id_to_params) -- Enum.map(responses, & &1.id)}, fn %{id: nil} = res, {result_res, [id | rest]} -> Logger.error( "Empty id in response: #{inspect(res)}, stacktrace: #{inspect(Process.info(self(), :current_stacktrace))}" ) {[%{res | id: id} | result_res], rest} res, {result_res, non_matched} -> {[res | result_res], non_matched} end ) |> elem(0) |> Enum.reverse() end @doc """ 1. POSTs JSON `payload` to `url` 2. Decodes the response 3. Handles the response ## Returns * Handled response * `{:error, reason}` if POST fails """ @spec json_rpc(Transport.request(), json_rpc_named_arguments) :: {:ok, Transport.result()} | {:error, reason :: term()} @spec json_rpc(Transport.batch_request(), json_rpc_named_arguments) :: {:ok, Transport.batch_response()} | {:error, reason :: term()} def json_rpc(request, named_arguments) when (is_map(request) or is_list(request)) and is_list(named_arguments) do transport = Keyword.fetch!(named_arguments, :transport) transport_options = Keyword.fetch!(named_arguments, :transport_options) throttle_timeout = Keyword.get(named_arguments, :throttle_timeout, @default_throttle_timeout) url = maybe_replace_url(transport_options[:url], transport_options[:fallback_url], transport) corrected_transport_options = Keyword.replace(transport_options, :url, url) case RequestCoordinator.perform(request, transport, corrected_transport_options, throttle_timeout) do {:ok, result} -> {:ok, result} {:error, reason} -> maybe_inc_error_count(corrected_transport_options[:url], named_arguments, transport) {:error, reason} end end defp do_balances_request(id_to_params, _chunk_size, _args) when id_to_params == %{}, do: {:ok, []} defp do_balances_request(id_to_params, chunk_size, json_rpc_named_arguments) do id_to_params |> FetchedBalances.requests() |> chunk_requests(chunk_size) |> json_rpc(json_rpc_named_arguments) end defp archive_json_rpc_named_arguments(json_rpc_named_arguments) do CommonHelper.put_in_keyword_nested( json_rpc_named_arguments, [:transport_options, :method_to_url, :eth_getBalance], System.get_env("ETHEREUM_JSONRPC_TRACE_URL") ) end defp maybe_request_archive_balances(id_to_params, trace_url_used?, disabled?, chunk_size, json_rpc_named_arguments) do if not trace_url_used? and not disabled? do do_balances_request(id_to_params, chunk_size, archive_json_rpc_named_arguments(json_rpc_named_arguments)) else {:ok, []} end end defp maybe_replace_url(url, _replace_url, EthereumJSONRPC.HTTP), do: url defp maybe_replace_url(url, replace_url, _), do: EndpointAvailabilityObserver.maybe_replace_url(url, replace_url, :ws) defp maybe_inc_error_count(_url, _arguments, EthereumJSONRPC.HTTP), do: :ok defp maybe_inc_error_count(url, arguments, _), do: EndpointAvailabilityObserver.inc_error_count(url, arguments, :ws) @doc """ Converts `t:quantity/0` to `t:non_neg_integer/0`. """ @spec quantity_to_integer(quantity) :: non_neg_integer() | nil def quantity_to_integer("0x" <> hexadecimal_digits) do String.to_integer(hexadecimal_digits, 16) end def quantity_to_integer(integer) when is_integer(integer), do: integer def quantity_to_integer(string) when is_binary(string) do case Integer.parse(string) do {integer, ""} -> integer _ -> nil end end def quantity_to_integer(_), do: nil @doc """ Sanitizes ID in JSON RPC request following JSON RPC [spec](https://www.jsonrpc.org/specification#request_object:~:text=An%20identifier%20established%20by%20the%20Client%20that%20MUST%20contain%20a%20String%2C%20Number%2C%20or%20NULL%20value%20if%20included.%20If%20it%20is%20not%20included%20it%20is%20assumed%20to%20be%20a%20notification.%20The%20value%20SHOULD%20normally%20not%20be%20Null%20%5B1%5D%20and%20Numbers%20SHOULD%20NOT%20contain%20fractional%20parts%20%5B2%5D). """ @spec sanitize_id(quantity) :: non_neg_integer() | String.t() | nil def sanitize_id(integer) when is_integer(integer), do: integer def sanitize_id(string) when is_binary(string) do # match ID string and ID string without non-ASCII characters if string == for(<>, c < 128, into: "", do: <>) do string else nil end end def sanitize_id(_), do: nil @doc """ Converts `t:non_neg_integer/0` to `t:quantity/0` """ @spec integer_to_quantity(non_neg_integer | binary) :: quantity def integer_to_quantity(integer) when is_integer(integer) and integer >= 0 do "0x" <> Integer.to_string(integer, 16) end def integer_to_quantity(integer) when is_binary(integer) do integer end @doc """ A request payload for a JSONRPC. """ @spec request(%{id: request_id, method: String.t(), params: list()}) :: Transport.request() def request(%{method: method, params: params} = map) when is_binary(method) and is_list(params) do Map.put(map, :jsonrpc, "2.0") end @doc """ Subscribes to `t:EthereumJSONRPC.Subscription.event/0` with `t:EthereumJSONRPC.Subscription.params/0`. Events are delivered in a tuple tagged with the `t:EthereumJSONRPC.Subscription.t/0` and containing the same output as the single-request form of `json_rpc/2`. | Message | Description | |-----------------------------------------------------------------------------------|----------------------------------------| | `{EthereumJSONRPC.Subscription.t(), {:ok, EthereumJSONRPC.Transport.result.t()}}` | New result in subscription | | `{EthereumJSONRPC.Subscription.t(), {:error, reason :: term()}}` | There was an error in the subscription | Subscription can be canceled by calling `unsubscribe/1` with the returned `t:EthereumJSONRPC.Subscription.t/0`. """ @spec subscribe(event :: Subscription.event(), params :: Subscription.params(), subscribe_named_arguments) :: {:ok, Subscription.t()} | {:error, reason :: term()} def subscribe(event, params \\ [], named_arguments) when is_list(params) do transport = Keyword.fetch!(named_arguments, :transport) transport_options = Keyword.fetch!(named_arguments, :transport_options) transport.subscribe(event, params, transport_options) end @doc """ Unsubscribes to `t:EthereumJSONRPC.Subscription.t/0` created with `subscribe/2`. ## Returns * `:ok` - subscription was canceled * `{:error, :not_found}` - subscription could not be canceled. It did not exist because either the server already canceled it, it never existed, or `unsubscribe/1 ` was called on it before. * `{:error, reason :: term}` - other error cancelling subscription. """ @spec unsubscribe(Subscription.t()) :: :ok | {:error, reason :: term()} def unsubscribe(%Subscription{transport: transport} = subscription) do transport.unsubscribe(subscription) end # We can only depend on implementations supporting 64-bit integers: # * Ganache only supports u32 (https://github.com/trufflesuite/ganache-core/issues/190) def unique_request_id do <> = :crypto.strong_rand_bytes(4) unique_request_id end @doc """ Converts `t:timestamp/0` to `t:DateTime.t/0` """ def timestamp_to_datetime(timestamp) do case quantity_to_integer(timestamp) do nil -> nil quantity -> Timex.from_unix(quantity) end end defp fetch_blocks_by_params(params, request, json_rpc_named_arguments) when is_list(params) and is_function(request, 1) do id_to_params = id_to_params(params) with {:ok, responses} <- id_to_params |> Blocks.requests(request) |> json_rpc(json_rpc_named_arguments) do {:ok, Blocks.from_responses(responses, id_to_params)} end end defp chunk_requests(requests, nil), do: requests defp chunk_requests(requests, chunk_size), do: Enum.chunk_every(requests, chunk_size) def put_if_present(result, transaction, keys) do Enum.reduce(keys, result, fn key, acc -> key_list = key |> Tuple.to_list() from_key = Enum.at(key_list, 0) to_key = Enum.at(key_list, 1) opts = if Enum.count(key_list) > 2, do: Enum.at(key_list, 2), else: %{} value = transaction[from_key] validate_key(acc, to_key, value, opts) end) end defp validate_key(acc, _to_key, nil, _opts), do: acc defp validate_key(acc, to_key, value, %{:validation => validation}) do case validation do :address_hash -> if address_correct?(value), do: Map.put(acc, to_key, value), else: acc _ -> Map.put(acc, to_key, value) end end defp validate_key(acc, to_key, value, _validation) do Map.put(acc, to_key, value) end # todo: The similar function exists in Indexer application: # Here is the room for future refactoring to keep a single function. @spec address_correct?(binary()) :: boolean() defp address_correct?(address) when is_binary(address) do String.match?(address, ~r/^0x[[:xdigit:]]{40}$/i) end defp address_correct?(_address) do false end end