Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve ergonomics #8

Merged
merged 4 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 1 addition & 71 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
22 changes: 16 additions & 6 deletions examples/chain_sync_client.ex
Original file line number Diff line number Diff line change
@@ -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:

Expand All @@ -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
10 changes: 6 additions & 4 deletions examples/state_query_client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 26 additions & 1 deletion lib/xogmios.ex
Original file line number Diff line number Diff line change
@@ -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
11 changes: 6 additions & 5 deletions lib/xogmios/chain_sync.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,8 +22,6 @@ defmodule Xogmios.ChainSync do

require Logger

@name __MODULE__

def handle_message(%{"id" => "start"} = message, state) do
%{
"method" => "nextBlock",
Expand Down Expand Up @@ -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
Expand Down
11 changes: 0 additions & 11 deletions lib/xogmios/chain_sync/connection.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
34 changes: 21 additions & 13 deletions lib/xogmios/state_query.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
15 changes: 8 additions & 7 deletions test/xogmios_test.exs → test/chain_sync_test.exs
Original file line number Diff line number Diff line change
@@ -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
Expand Down
45 changes: 45 additions & 0 deletions test/state_query_test.exs
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading