Skip to content

Commit

Permalink
Add docs and update close response
Browse files Browse the repository at this point in the history
  • Loading branch information
caike committed Feb 13, 2024
1 parent 1b9a106 commit 39d1bb3
Show file tree
Hide file tree
Showing 9 changed files with 238 additions and 26 deletions.
70 changes: 66 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ Mini-Protocols supported by this library:
- [ ] Tx Submission


See [Examples](#examples) section below for information on how to use.
See [Examples](#examples) section below for information on how to use this library.

## Installing

Add the dependency to `mix.exs`:
Add the dependency to `mix.exs` (not yet available on Hex):

```elixir
defp deps do
Expand All @@ -28,11 +28,73 @@ defp deps do
end
```

Not yet available on Hex.
Add your client module(s) to your application's supervision tree as such:

```elixir
# file: application.ex
def start(_type, _args) do
children = [
{ChainSyncClient, url: "ws://..."},
{StateQueryClient, url: "ws://..."},
]
#...
end
```

The value for the `url` option should be set to the address of your Ogmios instance.

See section below for examples of client modules.

## Examples

See [ChainSyncClient](./examples/chain_sync_client.ex) and [StateQueryClient](./examples/state_query_client.ex)
The following is an example of a module that implement the Chain Sync behaviour. This module syncs with the tip of the chain, reads the next 3 blocks and then closes the connection with the server.

```elixir
defmodule ChainSyncClient do
use Xogmios, :chain_sync

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("final handle_block #{block["height"]}")
{:close, state}
end
end
```

The following example implements the State Query behaviour and runs queries against the tip of the chain.

```elixir
defmodule StateQueryClient do
use Xogmios, :state_query
alias Xogmios.StateQuery

def start_link(opts) do
Xogmios.start_state_link(__MODULE__, opts)
end

def get_current_epoch(pid \\ __MODULE__) do
StateQuery.send_query(pid, :get_current_epoch)
end

def get_era_start(pid \\ __MODULE__) do
StateQuery.send_query(pid, :get_era_start)
end
end
```

For examples of applications using this library, see [Blocks](https://github.com/wowica/blocks) and [xogmios_watcher](https://github.com/wowica/xogmios_watcher).

## Test

Expand Down
65 changes: 64 additions & 1 deletion lib/xogmios.ex
Original file line number Diff line number Diff line change
@@ -1,15 +1,78 @@
defmodule Xogmios do
@moduledoc """
This is the top level module for Xogmios
This is the top level module for Xogmios. It implements functions to be used by client
modules that wish to connect with Ogmios.
When used, the it expects one of the supported mini-protocols as argument. For example:
defmodule ChainSyncClient do
use Xogmios, :chain_sync
# ...
end
or
defmodule StateQueryClient do
use Xogmios, :state_query
# ...
end
"""

alias Xogmios.ChainSync
alias Xogmios.StateQuery

@doc """
Starts a new State Query process linked to the current process
"""
def start_state_link(client, opts) do
StateQuery.start_link(client, opts)
end

@doc """
Starts a new Chain Sync process linked to the current process.
The `sync_from` option can be passed as part of `opts` to define at which point
the chain should be synced from.
This option accepts either:
a) An atom from the list: `:origin`, `:byron`,
`:shelley`, `:allegra`, `:mary`, `:alonzo`, `:babbage`.
For example:
```elixir
def start_link(opts) do
initial_state = [sync_from: :babbage]
opts = Keyword.merge(opts, initial_state)
Xogmios.start_chain_sync_link(__MODULE__, opts)
end
```
This will sync with the chain starting from the first block of the Babbage era.
b) A point in the chain, given its `slot` and `id`. For example:
```elixir
def start_link(opts) do
initial_state = [
sync_from: %{
point: %{
slot: 114_127_654,
id: "b0ff1e2bfc326a7f7378694b1f2693233058032bfb2798be2992a0db8b143099"
}
}
]
opts = Keyword.merge(opts, initial_state)
Xogmios.start_chain_sync_link(__MODULE__, opts)
end
```
This will sync with the chain starting from the first block **after** the specified point.
All other options passed as part of `opts` will be available in the `state` argument for `c:Xogmios.ChainSync.handle_block/2`.
See `ChainSyncClient` on this project's README for an example.
"""
def start_chain_sync_link(client, opts) do
ChainSync.start_link(client, opts)
end
Expand Down
46 changes: 39 additions & 7 deletions lib/xogmios/chain_sync.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,49 @@ defmodule Xogmios.ChainSync do

alias Xogmios.ChainSync.Messages

@callback handle_block(map(), any()) ::
{:ok, :next_block, map()} | {:ok, map()} | {:ok, :close, map()}
@callback handle_connect(map()) ::
{:ok, map()}
@callback handle_disconnect(String.t(), map()) ::
{:ok, map()} | {:reconnect, non_neg_integer(), map()}
@doc """
Invoked when a new block is emitted. This callback is required.
Returning `{:ok, :next_block, new_state}` will request the next block once it's made available.
Returning `{:ok, new_state}` will not request anymore blocks.
Returning `{:ok, :close, new_state}` will close the connection to the server.
"""
@callback handle_block(block :: map(), state) ::
{:ok, :next_block, new_state}
| {:ok, new_state}
| {:close, new_state}
when state: term(), new_state: term()

@doc """
Invoked upon connecting to the server. This callback is optional.
"""
@callback handle_connect(state) :: {:ok, new_state}
when state: term(), new_state: term()

@doc """
Invoked upon disconnecting from the server. This callback is optional.
Returning `{:ok, new_state}` will allow the connection to close.
Returning `{:reconnect, interval_in_ms}` will attempt a reconnection after `interval_in_ms`
"""
@callback handle_disconnect(reason :: String.t(), state) ::
{:ok, new_state}
| {:reconnect, interval_in_ms :: non_neg_integer(), new_state}
when state: term(), new_state: term()

# The keepalive option is used to maintain the connection active.
# This is important because proxies might close idle connections after a few seconds.
@keepalive_in_ms 5_000

@doc """
Starts a new Chain Sync process linked to the current process.
This function should not be called directly, but rather via `Xogmios.start_chain_sync_link/2`
"""
@spec start_link(module(), start_options :: Keyword.t()) :: {:ok, pid()} | {:error, term()}
def start_link(client, opts) do
{url, opts} = Keyword.pop(opts, :url)
initial_state = Keyword.merge(opts, handler: client)
Expand Down Expand Up @@ -90,7 +122,7 @@ defmodule Xogmios.ChainSync do
{:ok, new_state} ->
{:ok, new_state}

{:ok, :close, new_state} ->
{:close, new_state} ->
{:close, "finished", new_state}

response ->
Expand Down
32 changes: 29 additions & 3 deletions lib/xogmios/chain_sync/messages.ex
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
defmodule Xogmios.ChainSync.Messages do
@moduledoc """
This module returns messages for the Chain Synchronization protocol
This module contains messages for the Chain Synchronization protocol
"""

alias Jason.DecodeError

@doc """
Initial message to the server.
Once the response from this initial message is received, then
the client proceeds with the appropriate syncing strategy.
"""
def next_block_start() do
# The `id:"start"` is returned as part of the message response,
# and helps the client determine that this is a "nextBlock" response
# to the initial message.
json = ~S"""
{
"jsonrpc": "2.0",
Expand All @@ -18,6 +27,9 @@ defmodule Xogmios.ChainSync.Messages do
json
end

@doc """
Request next block.
"""
def next_block() do
json = ~S"""
{
Expand All @@ -30,6 +42,10 @@ defmodule Xogmios.ChainSync.Messages do
json
end

@doc """
Syncs the client to the given point in the chain,
indicated by `slot` and `id`.
"""
def find_intersection(slot, id) do
json = ~s"""
{
Expand All @@ -50,9 +66,12 @@ defmodule Xogmios.ChainSync.Messages do
json
end

@doc """
Syncs the client to the origin of the chain.
"""
def find_origin do
# For finding origin, any value can be passed as a point as long as "origin"
# is the first value.
# For finding origin, any value can be passed as
# a point as long as "origin" is the first value.
json = ~S"""
{
"jsonrpc": "2.0",
Expand Down Expand Up @@ -84,6 +103,13 @@ defmodule Xogmios.ChainSync.Messages do
babbage: {72_316_796, "c58a24ba8203e7629422a24d9dc68ce2ed495420bf40d9dab124373655161a20"}
}

@doc """
Syncs with a particular era bound.
Values accepted are #{Map.keys(@era_bounds) |> Enum.map(&"`:#{&1}`") |> Enum.join(",\n ")}
The client will sync with the first block of the given era.
"""
def last_block_from(era_name) when is_atom(era_name) do
case @era_bounds[era_name] do
{last_slot, last_block_id} -> find_intersection(last_slot, last_block_id)
Expand Down
17 changes: 14 additions & 3 deletions lib/xogmios/state_query.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ defmodule Xogmios.StateQuery do
alias Xogmios.StateQuery.Response
alias Xogmios.StateQuery.Server

@doc """
Starts a new State Query process linked to the current process.
This function should not be called directly, but rather via `Xogmios.start_state_link/2`
"""
@spec start_link(module(), start_options :: Keyword.t()) :: GenServer.on_start()
def start_link(client, opts) do
GenServer.start_link(client, opts, name: client)
end
Expand All @@ -19,9 +25,14 @@ defmodule Xogmios.StateQuery do
@allowed_queries Map.keys(@query_messages)

@doc """
Sends a State Query call to the server and returns a response. This function is synchornous and takes two arguments:
1. (Optional) A process reference. If none given, it defaults to __MODULE__.
2. The query to run. It currently accepts the following values: `:get_current_epoch`, `:get_era_start`.
Sends a State Query call to the server and returns a response.
This function is synchronous and takes two arguments:
1. (Optional) A process reference. If none given, it defaults to the linked process `__MODULE__`.
2. The query to run. Support for [all available queries](https://ogmios.dev/mini-protocols/local-state-query/#network)
is actively being worked on. For the time being, it only accepts the following values: #{@allowed_queries |> Enum.map(&inspect/1) |> Enum.map(&"`#{&1}`") |> Enum.join(",")}
"""
@spec send_query(pid() | atom(), atom()) :: {:ok, any()} | {:error, any()}
def send_query(client \\ __MODULE__, query) do
Expand Down
12 changes: 8 additions & 4 deletions lib/xogmios/state_query/messages.ex
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
defmodule Xogmios.StateQuery.Messages do
@moduledoc false
# This module returns messages for the State Query protocol
@moduledoc """
This module contains messages for the State Query protocol
"""

alias Jason.DecodeError

@doc """
Returns point to be used by acquire_ledger_state/1
Returns point to be used by `acquire_ledger_state/1`
"""
def get_tip do
json = ~S"""
Expand Down Expand Up @@ -41,7 +42,7 @@ defmodule Xogmios.StateQuery.Messages do
end

@doc """
Returns current epoch
Query current epoch
"""
def get_current_epoch do
json = ~S"""
Expand All @@ -55,6 +56,9 @@ defmodule Xogmios.StateQuery.Messages do
json
end

@doc """
Query start of the current era
"""
def get_era_start do
json = ~S"""
{
Expand Down
Loading

0 comments on commit 39d1bb3

Please sign in to comment.