Skip to content

Commit

Permalink
Add Adapter for RPC client (#152)
Browse files Browse the repository at this point in the history
* add an adapter interface

* add rpc_client module

* add ethereumex http client adapter implementation

* apply changes to ethers facade

* backwards compatibility

* backwards compatibility on the rpc_client configuration

* Add missing rpc calls and default opt

* Add test and improve naming

* Add moduledoc false to private modules

---------

Co-authored-by: Alisina Bahadori <[email protected]>
  • Loading branch information
joaop21 and alisinabh authored Nov 21, 2024
1 parent c3315ab commit 5e62d9d
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 15 deletions.
18 changes: 6 additions & 12 deletions lib/ethers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -459,8 +459,8 @@ defmodule Ethers do
- `:address`: Indicates event emitter contract address. (nil means all contracts)
- `:rpc_client`: The RPC Client to use. It should implement ethereum jsonRPC API. default: Ethereumex.HttpClient
- `:rpc_opts`: Extra options to pass to rpc_client. (Like timeout, Server URL, etc.)
- `:fromBlock`: Minimum block number of logs to filter.
- `:toBlock`: Maximum block number of logs to filter.
- `:fromBlock` | `:from_block`: Minimum block number of logs to filter.
- `:toBlock` | `:to_block`: Maximum block number of logs to filter.
"""
@spec get_logs(map(), Keyword.t()) :: {:ok, [Event.t()]} | {:error, atom()}
def get_logs(event_filter, overrides \\ []) do
Expand Down Expand Up @@ -564,19 +564,11 @@ defmodule Ethers do

@doc false
@spec rpc_client() :: atom()
def rpc_client, do: Application.get_env(:ethers, :rpc_client, Ethereumex.HttpClient)
defdelegate rpc_client(), to: Ethers.RpcClient

@doc false
@spec get_rpc_client(Keyword.t()) :: {atom(), Keyword.t()}
def get_rpc_client(opts) do
module =
case Keyword.fetch(opts, :rpc_client) do
{:ok, module} when is_atom(module) -> module
:error -> Ethers.rpc_client()
end

{module, Keyword.get(opts, :rpc_opts, [])}
end
defdelegate get_rpc_client(opts), to: Ethers.RpcClient

defp pre_process(tx_data, overrides, :call = _action, _opts) do
{block, overrides} = Keyword.pop(overrides, :block, "latest")
Expand Down Expand Up @@ -658,7 +650,9 @@ defmodule Ethers do
event_filter
|> EventFilter.to_map(overrides)
|> ensure_hex_value(:fromBlock)
|> ensure_hex_value(:from_block)
|> ensure_hex_value(:toBlock)
|> ensure_hex_value(:to_block)

{:ok, log_params}
end
Expand Down
25 changes: 25 additions & 0 deletions lib/ethers/rpc_client.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
defmodule Ethers.RpcClient do
@moduledoc false

@doc false
@spec rpc_client() :: atom()
def rpc_client do
case Application.get_env(:ethers, :rpc_client, Ethereumex.HttpClient) do
Ethereumex.HttpClient -> Ethers.RpcClient.EthereumexHttpClient
module when is_atom(module) -> module
_ -> raise ArgumentError, "Invalid ethers configuration. :rpc_client must be a module"
end
end

@doc false
@spec get_rpc_client(Keyword.t()) :: {atom(), Keyword.t()}
def get_rpc_client(opts) do
module =
case Keyword.fetch(opts, :rpc_client) do
{:ok, module} when is_atom(module) -> module
:error -> Ethers.rpc_client()
end

{module, Keyword.get(opts, :rpc_opts, [])}
end
end
35 changes: 35 additions & 0 deletions lib/ethers/rpc_client/adapter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
defmodule Ethers.RpcClient.Adapter do
@moduledoc false

@type error :: {:error, map() | binary() | atom()}

@callback batch_request([{atom(), list(boolean() | binary())}], keyword()) ::
{:ok, [any()]} | error

@callback eth_block_number(keyword()) :: {:ok, binary()} | error()

@callback eth_call(map(), binary(), keyword()) :: {:ok, binary()} | error()

@callback eth_estimate_gas(map(), keyword()) :: {:ok, binary()} | error()

@callback eth_gas_price(keyword()) :: {:ok, binary()} | error()

@callback eth_get_balance(binary(), binary(), keyword()) :: {:ok, binary()} | error()

@callback eth_get_block_by_number(binary() | non_neg_integer(), boolean(), keyword()) ::
{:ok, map()} | error()

@callback eth_get_transaction_by_hash(binary(), keyword()) :: {:ok, map()} | error()

@callback eth_get_transaction_count(binary(), binary(), keyword()) :: {:ok, binary()} | error()

@callback eth_get_transaction_receipt(binary(), keyword()) :: {:ok, map()} | error()

@callback eth_max_priority_fee_per_gas(keyword()) :: {:ok, binary()} | error()

@callback eth_get_logs(map(), keyword()) :: {:ok, [binary()] | [map()]} | error()

@callback eth_send_transaction(map(), keyword()) :: {:ok, binary()} | error()

@callback eth_send_raw_transaction(binary(), keyword()) :: {:ok, binary()} | error()
end
38 changes: 38 additions & 0 deletions lib/ethers/rpc_client/ethereumex_http_client.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
defmodule Ethers.RpcClient.EthereumexHttpClient do
@moduledoc false

alias Ethers.RpcClient.Adapter

@behaviour Ethers.RpcClient.Adapter

@exclude_delegation [:eth_get_logs]

for {func, arity} <- Adapter.behaviour_info(:callbacks), func not in @exclude_delegation do
args = Macro.generate_arguments(arity - 1, __MODULE__)

@impl true
def unquote(func)(unquote_splicing(args), opts \\ []) do
apply(Ethereumex.HttpClient, unquote(func), [unquote_splicing(args), opts])
end
end

@impl true
def eth_get_logs(params, opts \\ []) do
params
|> replace_key(:from_block, :fromBlock)
|> replace_key(:to_block, :toBlock)
|> Ethereumex.HttpClient.eth_get_logs(opts)
end

defp replace_key(map, ethers_key, ethereumex_key) do
case Map.fetch(map, ethers_key) do
{:ok, value} ->
map
|> Map.put(ethereumex_key, value)
|> Map.delete(ethers_key)

:error ->
map
end
end
end
27 changes: 24 additions & 3 deletions test/ethers/counter_contract_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ defmodule Ethers.CounterContractTest do
assert String.valid?(block_hash)
end

test "can filter logs with fromBlock and toBlock options", %{address: address} do
test "can filter logs with from_block and to_block options", %{address: address} do
{:ok, tx_hash} = CounterContract.set(101) |> Ethers.send(from: @from, to: address)

wait_for_transaction!(tx_hash)
Expand All @@ -285,8 +285,29 @@ defmodule Ethers.CounterContractTest do

assert [] ==
Ethers.get_logs!(filter,
fromBlock: current_block_number - 1,
toBlock: current_block_number - 1
from_block: current_block_number - 1,
to_block: current_block_number - 1
)

assert [
%Ethers.Event{
address: ^address,
topics: ["SetCalled(uint256,uint256)", 100],
data: [101],
data_raw: "0x0000000000000000000000000000000000000000000000000000000000000065",
log_index: 0,
removed: false,
topics_raw: [
"0x9db4e91e99652c2cf1713076f100fca6a4f5b81f166bce406ff2b3012694f49f",
"0x0000000000000000000000000000000000000000000000000000000000000064"
],
transaction_hash: ^tx_hash,
transaction_index: 0
}
] =
Ethers.get_logs!(filter,
from_block: current_block_number - 1,
to_block: current_block_number
)
end
end
Expand Down

0 comments on commit 5e62d9d

Please sign in to comment.