From 39d1bb382eba088ebbaeb8687a8f9beeae2b0935 Mon Sep 17 00:00:00 2001 From: Carlos Souza Date: Tue, 13 Feb 2024 16:08:00 -0500 Subject: [PATCH] Add docs and update close response --- README.md | 70 +++++++++++++++++++++++++++-- lib/xogmios.ex | 65 ++++++++++++++++++++++++++- lib/xogmios/chain_sync.ex | 46 ++++++++++++++++--- lib/xogmios/chain_sync/messages.ex | 32 +++++++++++-- lib/xogmios/state_query.ex | 17 +++++-- lib/xogmios/state_query/messages.ex | 12 +++-- mix.exs | 14 ++++-- mix.lock | 6 +++ test/chain_sync_test.exs | 2 +- 9 files changed, 238 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 4bcd708..116ba08 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/lib/xogmios.ex b/lib/xogmios.ex index 7168a97..db2c2ca 100644 --- a/lib/xogmios.ex +++ b/lib/xogmios.ex @@ -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 diff --git a/lib/xogmios/chain_sync.ex b/lib/xogmios/chain_sync.ex index 6fcddc0..de199c5 100644 --- a/lib/xogmios/chain_sync.ex +++ b/lib/xogmios/chain_sync.ex @@ -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) @@ -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 -> diff --git a/lib/xogmios/chain_sync/messages.ex b/lib/xogmios/chain_sync/messages.ex index e7bb629..14d7e05 100644 --- a/lib/xogmios/chain_sync/messages.ex +++ b/lib/xogmios/chain_sync/messages.ex @@ -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", @@ -18,6 +27,9 @@ defmodule Xogmios.ChainSync.Messages do json end + @doc """ + Request next block. + """ def next_block() do json = ~S""" { @@ -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""" { @@ -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", @@ -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) diff --git a/lib/xogmios/state_query.ex b/lib/xogmios/state_query.ex index d8ffd1a..9bbba8e 100644 --- a/lib/xogmios/state_query.ex +++ b/lib/xogmios/state_query.ex @@ -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 @@ -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 diff --git a/lib/xogmios/state_query/messages.ex b/lib/xogmios/state_query/messages.ex index 71a8b9b..e788e2e 100644 --- a/lib/xogmios/state_query/messages.ex +++ b/lib/xogmios/state_query/messages.ex @@ -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""" @@ -41,7 +42,7 @@ defmodule Xogmios.StateQuery.Messages do end @doc """ - Returns current epoch + Query current epoch """ def get_current_epoch do json = ~S""" @@ -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""" { diff --git a/mix.exs b/mix.exs index 8675a54..42262f9 100644 --- a/mix.exs +++ b/mix.exs @@ -24,9 +24,7 @@ defmodule Xogmios.MixProject do ] ], package: package(), - - # Docs - name: "Xogmios" + docs: docs() ] end @@ -46,6 +44,7 @@ defmodule Xogmios.MixProject do {:cowboy, "~> 2.10", only: :test}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:dialyxir, "~> 1.4.1", only: [:dev, :test], runtime: false}, + {:ex_doc, "~> 0.31", only: :dev, runtime: false}, {:jason, "~> 1.4"}, {:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false}, {:plug, "~> 1.15", only: :test}, @@ -60,4 +59,13 @@ defmodule Xogmios.MixProject do links: %{"GitHub" => @source_url} ] end + + defp docs do + [ + main: "readme", + name: "Xogmios", + source_url: @source_url, + extras: ["README.md"] + ] + end end diff --git a/mix.lock b/mix.lock index 13f1743..0e75739 100644 --- a/mix.lock +++ b/mix.lock @@ -5,11 +5,17 @@ "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, "credo": {:hex, :credo, "1.7.2", "fdee3a7cb553d8f2e773569181f0a4a2bb7d192e27e325404cc31b354f59d68c", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "dd15d6fbc280f6cf9b269f41df4e4992dee6615939653b164ef951f60afcb68e"}, "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "ex_doc": {:hex, :ex_doc, "0.31.1", "8a2355ac42b1cc7b2379da9e40243f2670143721dd50748bf6c3b1184dae2089", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3178c3a407c557d8343479e1ff117a96fd31bafe52a039079593fb0524ef61b0"}, "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.4", "29563475afa9b8a2add1b7a9c8fb68d06ca7737648f28398e04461f008b69521", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f4ed47ecda66de70dd817698a703f8816daa91272e7e45812469498614ae8b29"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mix_audit": {:hex, :mix_audit, "2.1.1", "653aa6d8f291fc4b017aa82bdb79a4017903902ebba57960ef199cbbc8c008a1", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "541990c3ab3a7bb8c4aaa2ce2732a4ae160ad6237e5dcd5ad1564f4f85354db1"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "plug": {:hex, :plug, "1.15.2", "94cf1fa375526f30ff8770837cb804798e0045fd97185f0bb9e5fcd858c792a3", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02731fa0c2dcb03d8d21a1d941bdbbe99c2946c0db098eee31008e04c6283615"}, "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"}, "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, diff --git a/test/chain_sync_test.exs b/test/chain_sync_test.exs index af00558..772b230 100644 --- a/test/chain_sync_test.exs +++ b/test/chain_sync_test.exs @@ -23,7 +23,7 @@ defmodule Xogmios.ChainSyncTest do @impl true def handle_block(_block, state) do send(state.test_handler, :handle_block) - {:ok, :close, state} + {:close, state} end end