From f89a27adf4af92f49fd54760d7517d4df7a0720e Mon Sep 17 00:00:00 2001 From: Konstantin Zolotarev Date: Tue, 16 Oct 2018 17:39:50 +0300 Subject: [PATCH 1/4] Fixes for ganache integration --- .../lib/ethereum_jsonrpc/transaction.ex | 45 ++- .../web_socket/web_socket_client_ganache.ex | 345 ++++++++++++++++++ apps/explorer/config/dev/ganache.exs | 20 + apps/indexer/config/dev/ganache.exs | 20 + 4 files changed, 427 insertions(+), 3 deletions(-) create mode 100644 apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/web_socket/web_socket_client_ganache.ex create mode 100644 apps/explorer/config/dev/ganache.exs create mode 100644 apps/indexer/config/dev/ganache.exs diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex index 7d04df023d..ac7fdcee61 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex @@ -13,7 +13,12 @@ defmodule EthereumJSONRPC.Transaction do alias EthereumJSONRPC @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() + | nil } @typedoc """ @@ -43,7 +48,11 @@ 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() + | nil } @type params :: %{ @@ -137,6 +146,35 @@ defmodule EthereumJSONRPC.Transaction do } end + # Ganache bug. it return `to: "0x0"` except of `to: null` + def elixir_to_params( + %{ + "to" => "0x0" + } = tr + ) do + elixir_to_params(%{tr | "to" => nil}) + end + + # Ganache bug. It don't send `r,s,v` transaction fields. + # Fix is in sources but not released yet + def elixir_to_params( + %{ + "blockHash" => block_hash, + "blockNumber" => block_number, + "from" => from_address_hash, + "gas" => gas, + "gasPrice" => gas_price, + "hash" => hash, + "input" => input, + "nonce" => nonce, + "to" => to_address_hash, + "transactionIndex" => index, + "value" => value + } = tr + ) do + elixir_to_params(%{tr | "r" => 0, "s" => 0, "v" => 0}) + end + @doc """ Extracts `t:EthereumJSONRPC.hash/0` from transaction `params` @@ -226,7 +264,8 @@ defmodule EthereumJSONRPC.Transaction do when key in ~w(blockHash condition creates from hash input jsonrpc publicKey raw to), do: {key, value} - defp entry_to_elixir({key, quantity}) when key in ~w(gas gasPrice nonce r s standardV v value) and quantity != nil do + defp entry_to_elixir({key, quantity}) + when key in ~w(gas gasPrice nonce r s standardV v value) and quantity != nil do {key, quantity_to_integer(quantity)} end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/web_socket/web_socket_client_ganache.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/web_socket/web_socket_client_ganache.ex new file mode 100644 index 0000000000..c549a2e5ee --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/web_socket/web_socket_client_ganache.ex @@ -0,0 +1,345 @@ +defmodule EthereumJSONRPC.WebSocket.WebSocketClientGanache do + @moduledoc """ + `EthereumJSONRPC.WebSocket` that uses `websocket_client` + """ + + require Logger + + import EthereumJSONRPC, only: [request: 1] + + alias EthereumJSONRPC.{Subscription, Transport, WebSocket} + alias EthereumJSONRPC.WebSocket.Registration + + @behaviour :websocket_client + @behaviour WebSocket + + @enforce_keys ~w(url)a + defstruct request_id_to_registration: %{}, + subscription_id_to_subscription: %{}, + url: nil + + # Supervisor interface + + @impl WebSocket + def child_spec(arg) do + Supervisor.child_spec(%{id: __MODULE__, start: {__MODULE__, :start_link, [arg]}}, []) + end + + @impl WebSocket + # only allow secure WSS + def start_link(["wss://" <> _ = url, gen_fsm_options]) when is_list(gen_fsm_options) do + fsm_name = + case Keyword.fetch(gen_fsm_options, :name) do + {:ok, name} when is_atom(name) -> {:local, name} + :error -> :undefined + end + + %URI{host: host} = URI.parse(url) + host_charlist = String.to_charlist(host) + + # `:depth`, `:verify`, and `:verify_fun`, are based on `:hackney_connect.ssl_opts_1/2` as we use `:hackney` through + # `:httpoison` and this keeps the SSL rules consistent between HTTP and WebSocket + :websocket_client.start_link( + fsm_name, + url, + __MODULE__, + url, + ssl_verify: :verify_peer, + socket_opts: [ + cacerts: :certifi.cacerts(), + depth: 99, + # SNI extension discloses host name in the clear, but allows for compatibility with Virtual Hosting for TLS + server_name_indication: host_charlist, + verify_fun: {&:ssl_verify_hostname.verify_fun/3, [check_hostname: host_charlist]} + ] + ) + end + + def start_link(["ws://" <> _ = url, gen_fsm_options]) when is_list(gen_fsm_options) do + fsm_name = + case Keyword.fetch(gen_fsm_options, :name) do + {:ok, name} when is_atom(name) -> {:local, name} + :error -> :undefined + end + + :websocket_client.start_link( + fsm_name, + url, + __MODULE__, + url, + [] + ) + end + + # Client interface + + @impl WebSocket + @spec json_rpc(WebSocket.web_socket(), Transport.request()) :: {:ok, Transport.result()} | {:error, reason :: term()} + def json_rpc(web_socket, request) do + GenServer.call(web_socket, {:json_rpc, request}) + end + + @impl WebSocket + @spec subscribe(WebSocket.web_socket(), Subscription.event(), Subscription.params()) :: + {:ok, Subscription.t()} | {:error, reason :: term()} + def subscribe(web_socket, event, params) when is_binary(event) and is_list(params) do + GenServer.call(web_socket, {:subscribe, event, params}) + end + + @impl WebSocket + @spec unsubscribe(WebSocket.web_socket(), Subscription.t()) :: :ok | {:error, :not_found} + def unsubscribe(web_socket, %Subscription{} = subscription) do + GenServer.call(web_socket, {:unsubscribe, subscription}) + end + + @impl :websocket_client + def init(url) do + {:reconnect, %__MODULE__{url: url}} + end + + @impl :websocket_client + def onconnect(_, %__MODULE__{} = state) do + {:ok, state} + end + + @impl :websocket_client + def ondisconnect(reason, %__MODULE__{} = state) do + {:close, reason, state} + end + + @impl :websocket_client + def websocket_handle({:text, text}, _request, %__MODULE__{} = state) do + case Jason.decode(text) do + {:ok, json} -> + handle_response(json, state) + + {:error, _} = error -> + broadcast(error, state) + {:ok, state} + end + end + + @impl :websocket_client + def websocket_info({:"$gen_call", from, request}, _, %__MODULE__{} = state) do + handle_call(request, from, state) + end + + @impl :websocket_client + def websocket_terminate(close, _request, %__MODULE__{} = state) do + broadcast(close, state) + end + + defp broadcast(message, %__MODULE__{subscription_id_to_subscription: id_to_subscription}) do + id_to_subscription + |> Map.values() + |> Subscription.broadcast(message) + end + + defp handle_call(message, from, %__MODULE__{} = state) do + {updated_state, unique_request} = register(message, from, state) + + {:reply, {:text, Jason.encode!(unique_request)}, updated_state} + end + + defp handle_response( + %{"method" => "eth_subscription", "params" => %{"result" => result, "subscription" => subscription_id}}, + %__MODULE__{subscription_id_to_subscription: subscription_id_to_subscription} = state + ) do + case subscription_id_to_subscription do + %{^subscription_id => subscription} -> + Subscription.publish(subscription, {:ok, result}) + + _ -> + Logger.error(fn -> + [ + "Unexpected `eth_subscription` subscription ID (", + inspect(subscription_id), + ") result (", + inspect(result), + "). Subscription ID not in known subscription IDs (", + subscription_id_to_subscription + |> Map.values() + |> Enum.map(&inspect/1), + ")." + ] + end) + end + + {:ok, state} + end + + defp handle_response( + %{"id" => id} = response, + %__MODULE__{request_id_to_registration: request_id_to_registration} = state + ) do + {registration, new_request_id_to_registration} = Map.pop(request_id_to_registration, id) + respond_to_registration(registration, new_request_id_to_registration, response, state) + end + + defp handle_response(response, %__MODULE__{} = state) do + Logger.error(fn -> + [ + "Unexpected JSON response from web socket\n", + "\n", + " Response:\n", + " ", + inspect(response) + ] + end) + + {:ok, state} + end + + defp register( + {:json_rpc, original_request}, + from, + %__MODULE__{request_id_to_registration: request_id_to_registration} = state + ) do + unique_id = unique_request_id(state) + + {%__MODULE__{ + state + | request_id_to_registration: + Map.put(request_id_to_registration, unique_id, %Registration{ + from: from, + type: :json_rpc + }) + }, %{original_request | id: unique_id}} + end + + defp register( + {:subscribe, event, params}, + from, + %__MODULE__{request_id_to_registration: request_id_to_registration} = state + ) + when is_binary(event) and is_list(params) do + unique_id = unique_request_id(state) + + { + %__MODULE__{ + state + | request_id_to_registration: + Map.put(request_id_to_registration, unique_id, %Registration{from: from, type: :subscribe}) + }, + request(%{id: unique_id, method: "eth_subscribe", params: [event | params]}) + } + end + + defp register( + {:unsubscribe, %Subscription{id: subscription_id}}, + from, + %__MODULE__{request_id_to_registration: request_id_to_registration} = state + ) do + unique_id = unique_request_id(state) + + { + %__MODULE__{ + state + | request_id_to_registration: + Map.put(request_id_to_registration, unique_id, %Registration{ + from: from, + type: :unsubscribe, + subscription_id: subscription_id + }) + }, + request(%{id: unique_id, method: "eth_unsubscribe", params: [subscription_id]}) + } + end + + defp respond_to_registration( + %Registration{type: :json_rpc, from: from}, + new_request_id_to_registration, + response, + %__MODULE__{} = state + ) do + reply = + case response do + %{"result" => result} -> {:ok, result} + %{"error" => error} -> {:error, error} + end + + GenServer.reply(from, reply) + + {:ok, %__MODULE__{state | request_id_to_registration: new_request_id_to_registration}} + end + + defp respond_to_registration( + %Registration{type: :subscribe, from: {subscriber_pid, _} = from}, + new_request_id_to_registration, + %{"result" => subscription_id}, + %__MODULE__{url: url} = state + ) do + subscription = %Subscription{ + id: subscription_id, + subscriber_pid: subscriber_pid, + transport: EthereumJSONRPC.WebSocket, + transport_options: [web_socket: __MODULE__, web_socket_options: %{web_socket: self()}, url: url] + } + + GenServer.reply(from, {:ok, subscription}) + + new_state = + state + |> put_in([Access.key!(:request_id_to_registration)], new_request_id_to_registration) + |> put_in([Access.key!(:subscription_id_to_subscription), subscription_id], subscription) + + {:ok, new_state} + end + + defp respond_to_registration( + %Registration{type: :subscribe, from: from}, + new_request_id_to_registration, + %{"error" => error}, + %__MODULE__{} = state + ) do + GenServer.reply(from, {:error, error}) + + {:ok, %__MODULE__{state | request_id_to_registration: new_request_id_to_registration}} + end + + defp respond_to_registration( + %Registration{type: :unsubscribe, from: from, subscription_id: subscription_id}, + new_request_id_to_registration, + response, + %__MODULE__{} = state + ) do + reply = + case response do + %{"result" => true} -> :ok + %{"result" => false} -> {:error, :not_found} + %{"error" => %{"message" => "subscription not found"}} -> {:error, :not_found} + %{"error" => error} -> {:error, error} + end + + GenServer.reply(from, reply) + + new_state = + state + |> put_in([Access.key!(:request_id_to_registration)], new_request_id_to_registration) + |> update_in([Access.key!(:subscription_id_to_subscription)], &Map.delete(&1, subscription_id)) + + {:ok, new_state} + end + + defp respond_to_registration(nil, _, response, %__MODULE__{} = state) do + Logger.error(fn -> ["Got response for unregistered request ID: ", inspect(response)] end) + + {:ok, state} + end + + defp unique_request_id(%__MODULE__{request_id_to_registration: request_id_to_registration} = state) do + # Ganache couldn't work with int64. + # https://github.com/trufflesuite/ganache-core/issues/190 + <> = :crypto.strong_rand_bytes(4) + + case request_id_to_registration do + # collision + %{^unique_request_id => _} -> + unique_request_id(state) + + _ -> + unique_request_id + end + end + end + \ No newline at end of file diff --git a/apps/explorer/config/dev/ganache.exs b/apps/explorer/config/dev/ganache.exs new file mode 100644 index 0000000000..260f4ea663 --- /dev/null +++ b/apps/explorer/config/dev/ganache.exs @@ -0,0 +1,20 @@ +use Mix.Config + +config :explorer, + json_rpc_named_arguments: [ + transport: EthereumJSONRPC.HTTP, + transport_options: [ + http: EthereumJSONRPC.HTTP.HTTPoison, + url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL") || "http://localhost:7545", + http_options: [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :ethereum_jsonrpc]] + ], + variant: EthereumJSONRPC.Geth + ], + subscribe_named_arguments: [ + transport: EthereumJSONRPC.WebSocket, + transport_options: [ + web_socket: EthereumJSONRPC.WebSocket.WebSocketClientGanache, + url: System.get_env("ETHEREUM_JSONRPC_WEB_SOCKET_URL") || "ws://localhost:7545" + ], + variant: EthereumJSONRPC.Geth + ] diff --git a/apps/indexer/config/dev/ganache.exs b/apps/indexer/config/dev/ganache.exs new file mode 100644 index 0000000000..b8c06b7e0a --- /dev/null +++ b/apps/indexer/config/dev/ganache.exs @@ -0,0 +1,20 @@ +use Mix.Config + +config :indexer, + block_interval: 5_000, + json_rpc_named_arguments: [ + transport: EthereumJSONRPC.HTTP, + transport_options: [ + http: EthereumJSONRPC.HTTP.HTTPoison, + url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL") || "http://localhost:7545", + http_options: [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :ethereum_jsonrpc]] + ], + variant: EthereumJSONRPC.Geth + ], + subscribe_named_arguments: [ + transport: EthereumJSONRPC.WebSocket, + transport_options: [ + web_socket: EthereumJSONRPC.WebSocket.WebSocketClientGanache, + url: System.get_env("ETHEREUM_JSONRPC_WEB_SOCKET_URL") || "ws://localhost:7545" + ] + ] From 0604f7a2349485899e14751b9331409235177574 Mon Sep 17 00:00:00 2001 From: Konstantin Zolotarev Date: Tue, 16 Oct 2018 20:19:10 +0300 Subject: [PATCH 2/4] Fixes for pr comments --- apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex | 3 +- .../lib/ethereum_jsonrpc/transaction.ex | 55 ++- .../web_socket/web_socket_client_ganache.ex | 345 ------------------ apps/explorer/config/dev/ganache.exs | 2 +- apps/indexer/config/dev/ganache.exs | 2 +- 5 files changed, 28 insertions(+), 379 deletions(-) delete mode 100644 apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/web_socket/web_socket_client_ganache.ex diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex index 75e1972f17..814496b469 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex @@ -372,8 +372,9 @@ defmodule EthereumJSONRPC do # We can only depend on implementations supporting 64-bit integers: # * Parity only supports u64 (https://github.com/paritytech/jsonrpc-core/blob/f2c61edb817e344d92ab3baf872fa77d1602430a/src/id.rs#L13) + # * Ganache only supports u32 (https://github.com/trufflesuite/ganache-core/issues/190) def unique_request_id do - <> = :crypto.strong_rand_bytes(8) + <> = :crypto.strong_rand_bytes(4) unique_request_id end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex index ac7fdcee61..917fbf1332 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex @@ -13,13 +13,8 @@ defmodule EthereumJSONRPC.Transaction do alias EthereumJSONRPC @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() | nil + } @typedoc """ * `"blockHash"` - `t:EthereumJSONRPC.hash/0` of the block this transaction is in. `nil` when transaction is @@ -47,13 +42,9 @@ defmodule EthereumJSONRPC.Transaction do * `"value"` - `t:EthereumJSONRPC.quantity/0` of wei transferred """ @type t :: %{ - String.t() => - EthereumJSONRPC.address() - | EthereumJSONRPC.hash() - | EthereumJSONRPC.quantity() - | String.t() - | nil - } + String.t() => + EthereumJSONRPC.address() | EthereumJSONRPC.hash() | EthereumJSONRPC.quantity() | String.t() | nil + } @type params :: %{ block_hash: EthereumJSONRPC.hash(), @@ -150,29 +141,32 @@ defmodule EthereumJSONRPC.Transaction do def elixir_to_params( %{ "to" => "0x0" - } = tr + } = transaction ) do - elixir_to_params(%{tr | "to" => nil}) + %{ transaction | "to" => nil } + |> elixir_to_params() end # Ganache bug. It don't send `r,s,v` transaction fields. # Fix is in sources but not released yet def elixir_to_params( %{ - "blockHash" => block_hash, - "blockNumber" => block_number, - "from" => from_address_hash, - "gas" => gas, - "gasPrice" => gas_price, - "hash" => hash, - "input" => input, - "nonce" => nonce, - "to" => to_address_hash, - "transactionIndex" => index, - "value" => value - } = tr + "blockHash" => _, + "blockNumber" => _, + "from" => _, + "gas" => _, + "gasPrice" => _, + "hash" => _, + "input" => _, + "nonce" => _, + "to" => _, + "transactionIndex" => _, + "value" => _ + } = transaction ) do - elixir_to_params(%{tr | "r" => 0, "s" => 0, "v" => 0}) + transaction + |> Map.merge(%{"r" => 0, "s" => 0, "v" => 0}) + |> elixir_to_params() end @doc """ @@ -264,8 +258,7 @@ defmodule EthereumJSONRPC.Transaction do when key in ~w(blockHash condition creates from hash input jsonrpc publicKey raw to), do: {key, value} - defp entry_to_elixir({key, quantity}) - when key in ~w(gas gasPrice nonce r s standardV v value) and quantity != nil do + defp entry_to_elixir({key, quantity}) when key in ~w(gas gasPrice nonce r s standardV v value) and quantity != nil do {key, quantity_to_integer(quantity)} end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/web_socket/web_socket_client_ganache.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/web_socket/web_socket_client_ganache.ex deleted file mode 100644 index c549a2e5ee..0000000000 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/web_socket/web_socket_client_ganache.ex +++ /dev/null @@ -1,345 +0,0 @@ -defmodule EthereumJSONRPC.WebSocket.WebSocketClientGanache do - @moduledoc """ - `EthereumJSONRPC.WebSocket` that uses `websocket_client` - """ - - require Logger - - import EthereumJSONRPC, only: [request: 1] - - alias EthereumJSONRPC.{Subscription, Transport, WebSocket} - alias EthereumJSONRPC.WebSocket.Registration - - @behaviour :websocket_client - @behaviour WebSocket - - @enforce_keys ~w(url)a - defstruct request_id_to_registration: %{}, - subscription_id_to_subscription: %{}, - url: nil - - # Supervisor interface - - @impl WebSocket - def child_spec(arg) do - Supervisor.child_spec(%{id: __MODULE__, start: {__MODULE__, :start_link, [arg]}}, []) - end - - @impl WebSocket - # only allow secure WSS - def start_link(["wss://" <> _ = url, gen_fsm_options]) when is_list(gen_fsm_options) do - fsm_name = - case Keyword.fetch(gen_fsm_options, :name) do - {:ok, name} when is_atom(name) -> {:local, name} - :error -> :undefined - end - - %URI{host: host} = URI.parse(url) - host_charlist = String.to_charlist(host) - - # `:depth`, `:verify`, and `:verify_fun`, are based on `:hackney_connect.ssl_opts_1/2` as we use `:hackney` through - # `:httpoison` and this keeps the SSL rules consistent between HTTP and WebSocket - :websocket_client.start_link( - fsm_name, - url, - __MODULE__, - url, - ssl_verify: :verify_peer, - socket_opts: [ - cacerts: :certifi.cacerts(), - depth: 99, - # SNI extension discloses host name in the clear, but allows for compatibility with Virtual Hosting for TLS - server_name_indication: host_charlist, - verify_fun: {&:ssl_verify_hostname.verify_fun/3, [check_hostname: host_charlist]} - ] - ) - end - - def start_link(["ws://" <> _ = url, gen_fsm_options]) when is_list(gen_fsm_options) do - fsm_name = - case Keyword.fetch(gen_fsm_options, :name) do - {:ok, name} when is_atom(name) -> {:local, name} - :error -> :undefined - end - - :websocket_client.start_link( - fsm_name, - url, - __MODULE__, - url, - [] - ) - end - - # Client interface - - @impl WebSocket - @spec json_rpc(WebSocket.web_socket(), Transport.request()) :: {:ok, Transport.result()} | {:error, reason :: term()} - def json_rpc(web_socket, request) do - GenServer.call(web_socket, {:json_rpc, request}) - end - - @impl WebSocket - @spec subscribe(WebSocket.web_socket(), Subscription.event(), Subscription.params()) :: - {:ok, Subscription.t()} | {:error, reason :: term()} - def subscribe(web_socket, event, params) when is_binary(event) and is_list(params) do - GenServer.call(web_socket, {:subscribe, event, params}) - end - - @impl WebSocket - @spec unsubscribe(WebSocket.web_socket(), Subscription.t()) :: :ok | {:error, :not_found} - def unsubscribe(web_socket, %Subscription{} = subscription) do - GenServer.call(web_socket, {:unsubscribe, subscription}) - end - - @impl :websocket_client - def init(url) do - {:reconnect, %__MODULE__{url: url}} - end - - @impl :websocket_client - def onconnect(_, %__MODULE__{} = state) do - {:ok, state} - end - - @impl :websocket_client - def ondisconnect(reason, %__MODULE__{} = state) do - {:close, reason, state} - end - - @impl :websocket_client - def websocket_handle({:text, text}, _request, %__MODULE__{} = state) do - case Jason.decode(text) do - {:ok, json} -> - handle_response(json, state) - - {:error, _} = error -> - broadcast(error, state) - {:ok, state} - end - end - - @impl :websocket_client - def websocket_info({:"$gen_call", from, request}, _, %__MODULE__{} = state) do - handle_call(request, from, state) - end - - @impl :websocket_client - def websocket_terminate(close, _request, %__MODULE__{} = state) do - broadcast(close, state) - end - - defp broadcast(message, %__MODULE__{subscription_id_to_subscription: id_to_subscription}) do - id_to_subscription - |> Map.values() - |> Subscription.broadcast(message) - end - - defp handle_call(message, from, %__MODULE__{} = state) do - {updated_state, unique_request} = register(message, from, state) - - {:reply, {:text, Jason.encode!(unique_request)}, updated_state} - end - - defp handle_response( - %{"method" => "eth_subscription", "params" => %{"result" => result, "subscription" => subscription_id}}, - %__MODULE__{subscription_id_to_subscription: subscription_id_to_subscription} = state - ) do - case subscription_id_to_subscription do - %{^subscription_id => subscription} -> - Subscription.publish(subscription, {:ok, result}) - - _ -> - Logger.error(fn -> - [ - "Unexpected `eth_subscription` subscription ID (", - inspect(subscription_id), - ") result (", - inspect(result), - "). Subscription ID not in known subscription IDs (", - subscription_id_to_subscription - |> Map.values() - |> Enum.map(&inspect/1), - ")." - ] - end) - end - - {:ok, state} - end - - defp handle_response( - %{"id" => id} = response, - %__MODULE__{request_id_to_registration: request_id_to_registration} = state - ) do - {registration, new_request_id_to_registration} = Map.pop(request_id_to_registration, id) - respond_to_registration(registration, new_request_id_to_registration, response, state) - end - - defp handle_response(response, %__MODULE__{} = state) do - Logger.error(fn -> - [ - "Unexpected JSON response from web socket\n", - "\n", - " Response:\n", - " ", - inspect(response) - ] - end) - - {:ok, state} - end - - defp register( - {:json_rpc, original_request}, - from, - %__MODULE__{request_id_to_registration: request_id_to_registration} = state - ) do - unique_id = unique_request_id(state) - - {%__MODULE__{ - state - | request_id_to_registration: - Map.put(request_id_to_registration, unique_id, %Registration{ - from: from, - type: :json_rpc - }) - }, %{original_request | id: unique_id}} - end - - defp register( - {:subscribe, event, params}, - from, - %__MODULE__{request_id_to_registration: request_id_to_registration} = state - ) - when is_binary(event) and is_list(params) do - unique_id = unique_request_id(state) - - { - %__MODULE__{ - state - | request_id_to_registration: - Map.put(request_id_to_registration, unique_id, %Registration{from: from, type: :subscribe}) - }, - request(%{id: unique_id, method: "eth_subscribe", params: [event | params]}) - } - end - - defp register( - {:unsubscribe, %Subscription{id: subscription_id}}, - from, - %__MODULE__{request_id_to_registration: request_id_to_registration} = state - ) do - unique_id = unique_request_id(state) - - { - %__MODULE__{ - state - | request_id_to_registration: - Map.put(request_id_to_registration, unique_id, %Registration{ - from: from, - type: :unsubscribe, - subscription_id: subscription_id - }) - }, - request(%{id: unique_id, method: "eth_unsubscribe", params: [subscription_id]}) - } - end - - defp respond_to_registration( - %Registration{type: :json_rpc, from: from}, - new_request_id_to_registration, - response, - %__MODULE__{} = state - ) do - reply = - case response do - %{"result" => result} -> {:ok, result} - %{"error" => error} -> {:error, error} - end - - GenServer.reply(from, reply) - - {:ok, %__MODULE__{state | request_id_to_registration: new_request_id_to_registration}} - end - - defp respond_to_registration( - %Registration{type: :subscribe, from: {subscriber_pid, _} = from}, - new_request_id_to_registration, - %{"result" => subscription_id}, - %__MODULE__{url: url} = state - ) do - subscription = %Subscription{ - id: subscription_id, - subscriber_pid: subscriber_pid, - transport: EthereumJSONRPC.WebSocket, - transport_options: [web_socket: __MODULE__, web_socket_options: %{web_socket: self()}, url: url] - } - - GenServer.reply(from, {:ok, subscription}) - - new_state = - state - |> put_in([Access.key!(:request_id_to_registration)], new_request_id_to_registration) - |> put_in([Access.key!(:subscription_id_to_subscription), subscription_id], subscription) - - {:ok, new_state} - end - - defp respond_to_registration( - %Registration{type: :subscribe, from: from}, - new_request_id_to_registration, - %{"error" => error}, - %__MODULE__{} = state - ) do - GenServer.reply(from, {:error, error}) - - {:ok, %__MODULE__{state | request_id_to_registration: new_request_id_to_registration}} - end - - defp respond_to_registration( - %Registration{type: :unsubscribe, from: from, subscription_id: subscription_id}, - new_request_id_to_registration, - response, - %__MODULE__{} = state - ) do - reply = - case response do - %{"result" => true} -> :ok - %{"result" => false} -> {:error, :not_found} - %{"error" => %{"message" => "subscription not found"}} -> {:error, :not_found} - %{"error" => error} -> {:error, error} - end - - GenServer.reply(from, reply) - - new_state = - state - |> put_in([Access.key!(:request_id_to_registration)], new_request_id_to_registration) - |> update_in([Access.key!(:subscription_id_to_subscription)], &Map.delete(&1, subscription_id)) - - {:ok, new_state} - end - - defp respond_to_registration(nil, _, response, %__MODULE__{} = state) do - Logger.error(fn -> ["Got response for unregistered request ID: ", inspect(response)] end) - - {:ok, state} - end - - defp unique_request_id(%__MODULE__{request_id_to_registration: request_id_to_registration} = state) do - # Ganache couldn't work with int64. - # https://github.com/trufflesuite/ganache-core/issues/190 - <> = :crypto.strong_rand_bytes(4) - - case request_id_to_registration do - # collision - %{^unique_request_id => _} -> - unique_request_id(state) - - _ -> - unique_request_id - end - end - end - \ No newline at end of file diff --git a/apps/explorer/config/dev/ganache.exs b/apps/explorer/config/dev/ganache.exs index 260f4ea663..2e66b814ef 100644 --- a/apps/explorer/config/dev/ganache.exs +++ b/apps/explorer/config/dev/ganache.exs @@ -13,7 +13,7 @@ config :explorer, subscribe_named_arguments: [ transport: EthereumJSONRPC.WebSocket, transport_options: [ - web_socket: EthereumJSONRPC.WebSocket.WebSocketClientGanache, + web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, url: System.get_env("ETHEREUM_JSONRPC_WEB_SOCKET_URL") || "ws://localhost:7545" ], variant: EthereumJSONRPC.Geth diff --git a/apps/indexer/config/dev/ganache.exs b/apps/indexer/config/dev/ganache.exs index b8c06b7e0a..eb133dd9d2 100644 --- a/apps/indexer/config/dev/ganache.exs +++ b/apps/indexer/config/dev/ganache.exs @@ -14,7 +14,7 @@ config :indexer, subscribe_named_arguments: [ transport: EthereumJSONRPC.WebSocket, transport_options: [ - web_socket: EthereumJSONRPC.WebSocket.WebSocketClientGanache, + web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, url: System.get_env("ETHEREUM_JSONRPC_WEB_SOCKET_URL") || "ws://localhost:7545" ] ] From 3eee0a294454a2e9654fd728797f2bf3563b6543 Mon Sep 17 00:00:00 2001 From: Konstantin Zolotarev Date: Tue, 16 Oct 2018 20:20:45 +0300 Subject: [PATCH 3/4] Fix spacing --- .../lib/ethereum_jsonrpc/transaction.ex | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex index 917fbf1332..dc3a9b6578 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex @@ -13,8 +13,8 @@ defmodule EthereumJSONRPC.Transaction do alias EthereumJSONRPC @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() | nil + } @typedoc """ * `"blockHash"` - `t:EthereumJSONRPC.hash/0` of the block this transaction is in. `nil` when transaction is @@ -42,9 +42,9 @@ defmodule EthereumJSONRPC.Transaction do * `"value"` - `t:EthereumJSONRPC.quantity/0` of wei transferred """ @type t :: %{ - String.t() => - EthereumJSONRPC.address() | EthereumJSONRPC.hash() | EthereumJSONRPC.quantity() | String.t() | nil - } + String.t() => + EthereumJSONRPC.address() | EthereumJSONRPC.hash() | EthereumJSONRPC.quantity() | String.t() | nil + } @type params :: %{ block_hash: EthereumJSONRPC.hash(), From ff8053d65618f4cbc7be10ab4662a9a256ff6467 Mon Sep 17 00:00:00 2001 From: Konstantin Zolotarev Date: Tue, 16 Oct 2018 20:38:44 +0300 Subject: [PATCH 4/4] fix formatting --- apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex index dc3a9b6578..4cd2f0191d 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex @@ -143,7 +143,7 @@ defmodule EthereumJSONRPC.Transaction do "to" => "0x0" } = transaction ) do - %{ transaction | "to" => nil } + %{transaction | "to" => nil} |> elixir_to_params() end @@ -164,9 +164,9 @@ defmodule EthereumJSONRPC.Transaction do "value" => _ } = transaction ) do - transaction - |> Map.merge(%{"r" => 0, "s" => 0, "v" => 0}) - |> elixir_to_params() + transaction + |> Map.merge(%{"r" => 0, "s" => 0, "v" => 0}) + |> elixir_to_params() end @doc """