diff --git a/README.md b/README.md index 66068ef..ea82b3e 100644 --- a/README.md +++ b/README.md @@ -20,77 +20,7 @@ end ## Examples -From a new module, call `use Xogmios.ChainSync` and implement the `start_link/1` and `handle_block/2` functions as such: - -```elixir -defmodule MyApp.ChainSyncClient do - @moduledoc """ - This module syncs with the tip of the chain and reads blocks indefinitely - """ - - use Xogmios.ChainSync - - def start_link(opts), - do: start_connection(opts) - - @impl true - def handle_block(block, state) do - IO.puts("handle_block #{block["height"]}") - {:ok, :next_block, state} - end -end -``` - -Add the new module to your app's supervision tree in `application.ex`: - -```elixir -def start(_type, _args) do - children =[ - {MyApp.ChainSyncClient, url: "ws://url-for-ogmios"}, - ] - - opts = [strategy: :one_for_one, name: MyApp.Supervisor] - Supervisor.start_link(children, opts) -end -``` - -The example below syncs with the tip and prints the next 3 blocks: - -```elixir -defmodule MyApp.ChainSyncClient do - @moduledoc """ - This module syncs with the tip of the chain and reads the following 3 blocks - """ - - use Xogmios.ChainSync - - require Logger - - def start_link(opts) do - # Initial state currently has to be defined here - # and passed as argument to start_connection - initial_state = [counter: 3] - - opts - |> Keyword.merge(initial_state) - |> start_connection() - end - - @impl true - def handle_block(block, %{counter: counter} = state) when counter > 1 do - Logger.info("handle_block #{block["height"]}") - {:ok, :next_block, %{state | counter: counter - 1}} - end - - @impl true - def handle_block(block, state) do - Logger.info("final handle_block #{block["height"]}") - {:ok, :close, state} - end -end -``` - -See more examples in the [examples](./examples/) folder. +See [ChainSyncClient](./examples/chain_sync_client.ex) and [StateQueryClient](./examples/state_query_client.ex) ## Test diff --git a/examples/chain_sync_client.ex b/examples/chain_sync_client.ex index 38fc210..41fdcd1 100644 --- a/examples/chain_sync_client.ex +++ b/examples/chain_sync_client.ex @@ -1,6 +1,7 @@ defmodule ChainSyncClient do @moduledoc """ - This module syncs with the tip of the chain and reads blocks indefinitely. + This module syncs with the tip of the chain, reads the next 3 blocks + and then closes the connection with the server. Add this to your application's supervision tree like so: @@ -12,14 +13,23 @@ defmodule ChainSyncClient do end """ - use Xogmios.ChainSync + use Xogmios, :chain_sync - def start_link(opts), - do: start_connection(opts) + def start_link(opts) do + initial_state = [counter: 3] + opts = Keyword.merge(opts, initial_state) + Xogmios.start_chain_sync_link(__MODULE__, opts) + end + + @impl true + def handle_block(block, %{counter: counter} = state) when counter > 1 do + IO.puts("handle_block #{block["height"]}") + {:ok, :next_block, %{state | counter: counter - 1}} + end @impl true def handle_block(block, state) do - IO.puts("handle_block from client #{block["height"]}") - {:ok, :next_block, state} + IO.puts("final handle_block #{block["height"]}") + {:ok, :close, state} end end diff --git a/examples/state_query_client.ex b/examples/state_query_client.ex index 30b9ca8..95f1f25 100644 --- a/examples/state_query_client.ex +++ b/examples/state_query_client.ex @@ -13,14 +13,16 @@ defmodule StateQueryClient do Then invoke functions: * StateQueryClient.get_current_epoch() - * StateQueryClient.get_era_start() + * StateQueryClient.get_era_start() # not yet supported Not all queries are supported yet. """ - use Xogmios.StateQuery - def start_link(opts), - do: start_connection(opts) + use Xogmios, :state_query + + def start_link(opts) do + Xogmios.start_state_link(__MODULE__, opts) + end def get_current_epoch() do case send_query(:get_current_epoch) do diff --git a/lib/xogmios.ex b/lib/xogmios.ex index 392e04d..7168a97 100644 --- a/lib/xogmios.ex +++ b/lib/xogmios.ex @@ -1,3 +1,28 @@ defmodule Xogmios do - @moduledoc false + @moduledoc """ + This is the top level module for Xogmios + """ + + alias Xogmios.ChainSync + alias Xogmios.StateQuery + + def start_state_link(client, opts) do + StateQuery.start_link(client, opts) + end + + def start_chain_sync_link(client, opts) do + ChainSync.start_link(client, opts) + end + + defmacro __using__(:state_query) do + quote do + use Xogmios.StateQuery + end + end + + defmacro __using__(:chain_sync) do + quote do + use Xogmios.ChainSync + end + end end diff --git a/lib/xogmios/chain_sync.ex b/lib/xogmios/chain_sync.ex index 2c61ba8..51c6d17 100644 --- a/lib/xogmios/chain_sync.ex +++ b/lib/xogmios/chain_sync.ex @@ -3,13 +3,17 @@ defmodule Xogmios.ChainSync do This module interfaces with the Chain Synchronization protocol. """ - require Logger - alias Xogmios.ChainSync.Messages @callback handle_block(map(), any()) :: {:ok, :next_block, map()} | {:ok, map()} | {:ok, :close, map()} + def start_link(client, opts) do + url = Keyword.fetch!(opts, :url) + state = Keyword.merge(opts, handler: client) + :websocket_client.start_link(url, client, state) + end + defmacro __using__(_opts) do quote do @behaviour Xogmios.ChainSync @@ -18,8 +22,6 @@ defmodule Xogmios.ChainSync do require Logger - @name __MODULE__ - def handle_message(%{"id" => "start"} = message, state) do %{ "method" => "nextBlock", @@ -64,7 +66,6 @@ defmodule Xogmios.ChainSync do end def handle_message({:text, message}, state) do - # Logger.info("fallback handle message #{inspect(message)}") {:close, state} end end diff --git a/lib/xogmios/chain_sync/connection.ex b/lib/xogmios/chain_sync/connection.ex index 5411aa1..11e0c9a 100644 --- a/lib/xogmios/chain_sync/connection.ex +++ b/lib/xogmios/chain_sync/connection.ex @@ -24,17 +24,6 @@ defmodule Xogmios.ChainSync.Connection do } end - def start_connection(opts), - do: do_start_link(opts) - - def do_start_link(opts) do - url = Keyword.fetch!(opts, :url) - - state = Keyword.merge(opts, handler: __MODULE__) - - :websocket_client.start_link(url, __MODULE__, state) - end - @impl true def init(state) do initial_state = diff --git a/lib/xogmios/state_query.ex b/lib/xogmios/state_query.ex index 89e98aa..97d60f6 100644 --- a/lib/xogmios/state_query.ex +++ b/lib/xogmios/state_query.ex @@ -8,12 +8,29 @@ defmodule Xogmios.StateQuery do alias Xogmios.StateQuery.Response alias Xogmios.StateQuery.Server + def start_link(client, opts) do + GenServer.start_link(client, opts, name: client) + end + @query_messages %{ get_current_epoch: Messages.get_current_epoch(), get_era_start: Messages.get_era_start() } - def query_messages, do: @query_messages + @allowed_queries Map.keys(@query_messages) + + def fetch_query_message(query) when query in @allowed_queries, + do: Map.fetch(@query_messages, query) + + def fetch_query_message(query), + do: {:error, "Unsupported query #{inspect(query)}"} + + def call_query(client, message) do + case GenServer.call(client, {:send_message, message}) do + {:ok, response} -> {:ok, response} + {:error, reason} -> {:error, reason} + end + end defmacro __using__(_opts) do quote do @@ -27,21 +44,12 @@ defmodule Xogmios.StateQuery do """ @spec send_query(term(), term()) :: {:ok, any()} | {:error, any()} def send_query(client \\ __MODULE__, query) do - with {:ok, message} <- Map.fetch(StateQuery.query_messages(), query), - {:ok, %Response{} = response} <- GenServer.call(client, {:send_message, message}) do + with {:ok, message} <- StateQuery.fetch_query_message(query), + {:ok, %Response{} = response} <- StateQuery.call_query(client, message) do {:ok, response.result} - else - :error -> {:error, "Unsupported query"} - {:error, _reason} -> {:error, "Error sending query"} end end - def start_connection(opts), - do: do_start_link(opts) - - def do_start_link(args), - do: GenServer.start_link(__MODULE__, args, name: __MODULE__) - ## Callbacks @impl true @@ -59,7 +67,7 @@ defmodule Xogmios.StateQuery do @impl true def handle_call({:send_message, message}, from, state) do - send(state.ws_pid, {:store_caller, from}) + {:store_caller, _from} = send(state.ws_pid, {:store_caller, from}) :ok = :websocket_client.send(state.ws_pid, {:text, message}) {:noreply, state} end diff --git a/test/xogmios_test.exs b/test/chain_sync_test.exs similarity index 71% rename from test/xogmios_test.exs rename to test/chain_sync_test.exs index 8ef8ba4..af00558 100644 --- a/test/xogmios_test.exs +++ b/test/chain_sync_test.exs @@ -1,23 +1,24 @@ -defmodule XogmiosTest do +defmodule Xogmios.ChainSyncTest do use ExUnit.Case - @ws_url ChainSync.TestServer.get_url() + @ws_url TestServer.get_url() setup_all do - {:ok, _server} = ChainSync.TestServer.start() + {:ok, _server} = TestServer.start(handler: ChainSync.TestHandler) on_exit(fn -> - ChainSync.TestServer.shutdown() + TestServer.shutdown() end) :ok end defmodule DummyClient do - use Xogmios.ChainSync + use Xogmios, :chain_sync - def start_link(opts), - do: start_connection(opts) + def start_link(opts) do + Xogmios.start_chain_sync_link(__MODULE__, opts) + end @impl true def handle_block(_block, state) do diff --git a/test/state_query_test.exs b/test/state_query_test.exs new file mode 100644 index 0000000..6889134 --- /dev/null +++ b/test/state_query_test.exs @@ -0,0 +1,45 @@ +defmodule Xogmios.StateQueryTest do + use ExUnit.Case + + @ws_url TestServer.get_url() + + setup_all do + {:ok, _server} = TestServer.start(handler: StateQuery.TestHandler) + + on_exit(fn -> + TestServer.shutdown() + end) + + :ok + end + + defmodule DummyClient do + use Xogmios, :state_query + + def start_link(opts) do + Xogmios.start_state_link(__MODULE__, opts) + end + + def get_current_epoch() do + case send_query(:get_current_epoch) do + {:ok, result} -> result + {:error, reason} -> "Something went wrong #{inspect(reason)}" + end + end + + def get_bananas() do + case send_query(:get_bananas) do + {:ok, result} -> result + {:error, reason} -> "Something went wrong #{inspect(reason)}" + end + end + end + + test "returns current epoch" do + pid = start_supervised!({DummyClient, url: @ws_url}) + assert is_pid(pid) + Process.sleep(1_000) + assert DummyClient.get_current_epoch() == 333 + assert DummyClient.get_bananas() =~ "Something went wrong" + end +end diff --git a/test/support/state_query/test_handler.ex b/test/support/state_query/test_handler.ex new file mode 100644 index 0000000..8997e4d --- /dev/null +++ b/test/support/state_query/test_handler.ex @@ -0,0 +1,69 @@ +defmodule StateQuery.TestHandler do + @moduledoc false + + require Logger + + @behaviour :cowboy_websocket + @current_epoch 333 + + @impl true + def init(request, state) do + {:cowboy_websocket, request, state} + end + + @impl true + def websocket_init(state) do + {:ok, state} + end + + @impl true + # Sends response back to client + def websocket_handle({:text, payload}, state) do + case Jason.decode(payload) do + {:ok, %{"method" => "queryNetwork/tip"}} -> + payload = + Jason.encode!(%{ + "method" => "queryNetwork/tip", + "result" => %{"slot" => "123", "id" => "123"} + }) + + {:reply, {:text, payload}, state} + + {:ok, %{"method" => "acquireLedgerState"}} -> + payload = + Jason.encode!(%{ + "method" => "acquireLedgerState" + }) + + {:reply, {:text, payload}, state} + + {:ok, %{"method" => "queryLedgerState/epoch"}} -> + payload = + Jason.encode!(%{ + "method" => "can-be-anything", + "result" => @current_epoch + }) + + {:reply, {:text, payload}, state} + + result -> + IO.puts("Did not match #{inspect(result)}") + {:reply, {:text, payload}, state} + end + end + + @impl true + def terminate(_arg1, _arg2, _arg3) do + :ok + end + + @impl true + def websocket_info(:stop, state) do + {:stop, state} + end + + @impl true + def websocket_info(_message, state) do + {:stop, state} + end +end diff --git a/test/support/chain_sync/test_router.ex b/test/support/test_router.ex similarity index 77% rename from test/support/chain_sync/test_router.ex rename to test/support/test_router.ex index 2900ec7..3a5307e 100644 --- a/test/support/chain_sync/test_router.ex +++ b/test/support/test_router.ex @@ -1,4 +1,4 @@ -defmodule ChainSync.TestRouter do +defmodule TestRouter do @moduledoc false use Plug.Router @@ -14,7 +14,7 @@ defmodule ChainSync.TestRouter do dispatch = [ {:_, [ - {"/ws/[...]", ChainSync.TestHandler, []} + {"/ws/[...]", opts[:handler], []} ]} ] diff --git a/test/support/chain_sync/test_server.ex b/test/support/test_server.ex similarity index 77% rename from test/support/chain_sync/test_server.ex rename to test/support/test_server.ex index 81940c6..eac572f 100644 --- a/test/support/chain_sync/test_server.ex +++ b/test/support/test_server.ex @@ -1,4 +1,4 @@ -defmodule ChainSync.TestServer do +defmodule TestServer do @moduledoc false @default_port 8989 @@ -7,12 +7,12 @@ defmodule ChainSync.TestServer do "ws://localhost:#{port}/ws" end - def start(port \\ @default_port) do + def start(port \\ @default_port, handler: handler) do cowboy_server = Plug.Cowboy.http( WebSocket.Router, [scheme: :http], - ChainSync.TestRouter.options(port: port) + TestRouter.options(port: port, handler: handler) ) cowboy_server =