From edf7ab26f99b785a8eb017e4a2558b0f26349fa2 Mon Sep 17 00:00:00 2001 From: Carlos Souza Date: Mon, 12 Aug 2024 11:39:24 -0400 Subject: [PATCH 1/4] Add client generator mix task --- lib/mix/tasks/xogmios.gen.client.ex | 213 ++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 lib/mix/tasks/xogmios.gen.client.ex diff --git a/lib/mix/tasks/xogmios.gen.client.ex b/lib/mix/tasks/xogmios.gen.client.ex new file mode 100644 index 0000000..ffaaed7 --- /dev/null +++ b/lib/mix/tasks/xogmios.gen.client.ex @@ -0,0 +1,213 @@ +defmodule Mix.Tasks.Xogmios.Gen.Client do + @shortdoc "Generates a Xogmios client module" + + @moduledoc """ + Generates a new Xogmios client module. + + $ mix xogmios.gen.client -p chain_sync MyClientModule + + The following CLI flags are required: + ```md + -p, --protocol The Ouroboros mini-protocol for which the client module + will be working with. This can be one of: chain_sync, + mempool_txs, state_query, tx_submission. + ``` + """ + + use Mix.Task + alias Mix.Shell.IO + + @impl true + def run(args) do + otp_app = + Mix.Project.config() + |> Keyword.get(:app) + |> Atom.to_string() + + case parse_options(args) do + %{protocol: protocol, client_module_name: client_module_name} -> + generate_client(otp_app, protocol, client_module_name) + + _ -> + raise "Missing required arguments. Run mix help xogmios.gen.client for usage instructions" + end + end + + defp generate_client(otp_app, protocol, client_module_name) do + project_root = File.cwd!() + filename = Macro.underscore(client_module_name) + path = Path.join([project_root, "lib", otp_app, "#{filename}.ex"]) + dirname = Path.dirname(path) + + unless File.exists?(dirname) do + raise "Required directory path #{dirname} does not exist. " + end + + write_file = + if File.exists?(path) do + IO.yes?("File already exists at #{path}. Overwrite?") + else + true + end + + if write_file do + create_client_file(path, otp_app, protocol, client_module_name) + IO.info("Successfully wrote out #{path}") + else + IO.info("Did not write file out to #{path}") + end + end + + defp parse_options(args) do + cli_options = [protocol: :string] + cli_aliases = [p: :protocol] + + parsed_args = OptionParser.parse(args, aliases: cli_aliases, strict: cli_options) + + case parsed_args do + {options, [client_module_name], [] = _errors} -> + options + |> Map.new() + |> Map.merge(%{client_module_name: client_module_name}) + + {_options, _remaining_args, errors} -> + raise "Invalid CLI args were provided: #{inspect(errors)}" + end + end + + defp create_client_file(path, otp_app, protocol, client_module_name) do + app_module_name = Macro.camelize(otp_app) + + assigns = [ + app_module_name: app_module_name, + protocol: protocol, + client_module_name: client_module_name + ] + + module_template = + xogmios_module_template(protocol) + |> EEx.eval_string(assigns: assigns) + + path + |> File.write!(module_template) + end + + defp xogmios_module_template("chain_sync") do + """ + defmodule <%= @app_module_name %>.<%= @client_module_name %> do + @moduledoc \"\"\" + This module syncs with the chain and reads new blocks. + + Be sure to add this module to your app's supervision tree like so: + + def start(_type, _args) do + children = [ + ..., + {<%= @app_module_name %>.<%= @client_module_name %>, url: System.fetch_env!("OGMIOS_URL")} + ] + ... + end + \"\"\" + + use Xogmios, :chain_sync + + def start_link(opts) do + # Syncs from current tip by default + initial_state = [] + ### See examples below on how to sync + ### from different points of the chain: + # initial_state = [sync_from: :babbage] + # 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 + + @impl true + def handle_block(block, state) do + IO.puts("handle_block \#{block["height"]}") + {:ok, :next_block, state} + end + end + """ + end + + defp xogmios_module_template("state_query") do + """ + defmodule <%= @app_module_name %>.<%= @client_module_name %> do + @moduledoc \"\"\" + This module queries against the known tip of the chain. + + Be sure to add this module to your app's supervision tree like so: + + def start(_type, _args) do + children = [ + ..., + {<%= @app_module_name %>.<%= @client_module_name %>, url: System.fetch_env!("OGMIOS_URL")} + ] + ... + end + + Then invoke functions: + * <%= @client_module_name %>.send_query("eraStart") + * <%= @client_module_name %>.send_query("queryNetwork/blockHeight") + \"\"\" + + use Xogmios, :state_query + alias Xogmios.StateQuery + + def start_link(opts) do + Xogmios.start_state_link(__MODULE__, opts) + end + + def send_query(pid \\\\\ __MODULE__, query_name) do + StateQuery.send_query(pid, query_name) + end + end + """ + end + + defp xogmios_module_template("mempool_txs") do + """ + defmodule <%= @app_module_name %>.<%= @client_module_name %> do + @moduledoc \"\"\" + This module prints transactions as they become available in the mempool. + + Be sure to add this module to your app's supervision tree like so: + + def start(_type, _args) do + children = [ + ..., + {<%= @app_module_name %>.<%= @client_module_name %>, url: System.fetch_env!("OGMIOS_URL")} + ] + ... + end + \"\"\" + + use Xogmios, :mempool_txs + + def start_link(opts) do + # set include_details: false (default) to retrieve + # only transaction id. + # set include_details: true to retrieve + # complete information about the transaction. + opts = Keyword.merge(opts, include_details: false) + Xogmios.start_mempool_txs_link(__MODULE__, opts) + end + + @impl true + def handle_transaction(transaction, state) do + IO.puts("transaction \#{\inspect(transaction)}") + + {:ok, :next_transaction, state} + end + end + """ + end +end From 9863e7e8bf983be39b434ac99a63282d784eed74 Mon Sep 17 00:00:00 2001 From: Carlos Souza Date: Mon, 12 Aug 2024 12:01:27 -0400 Subject: [PATCH 2/4] Update docs --- CHANGELOG.md | 1 + README.md | 27 +++++++++++++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7307595..2df52e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - Added partial support for Mempool monitoring mini-protocol. Allows reading transactions in the mempool. +- Added generators mix task for generating boilerplate code for client modules: `mix help xogmios.gen.client` ## [v0.4.1](https://github.com/wowica/xogmios/releases/tag/v0.4.1) (2024-06-05) diff --git a/README.md b/README.md index a255dd0..64374ae 100644 --- a/README.md +++ b/README.md @@ -21,19 +21,36 @@ Mini-Protocols supported by this library: See [Examples](#examples) section below for information on how to use this library. -## Installing +## Installation Add the dependency to `mix.exs`: ```elixir defp deps do [ - {:xogmios, "~> 0.4.1"} + {:xogmios, ">= 0.0.1"} ] end ``` -Add your client modules to your application's supervision tree as such: +Then run `mix deps.get` + +## Setup + +The mix task `xogmios.gen.client` is available to help generate the necessary +client code for each of the supported mini-protocols. Information on usage can +be found by running the following mix task: + +`mix help xogmios.gen.client`. + +For example, the following mix command generates a client module for the +ChainSync mini-protocol: + +`mix xogmios.gen.client -p chain_sync ChainSyncClient` + +A new file should be created at _./lib/my_app/chain_sync_client.ex_ + +Add this new module to your application's supervision tree as such: ```elixir # file: application.ex @@ -41,9 +58,7 @@ def start(_type, _args) do ogmios_url = System.fetch_env!("OGMIOS_URL") children = [ - {ChainSyncClient, url: ogmios_url}, - {StateQueryClient, url: ogmios_url}, - {TxSubmissionClient, url: ogmios_url} + {MyApp.ChainSyncClient, url: ogmios_url} ] #... end From ad17c2c354ae1c34517cb4d19e3ffb4d45255cd4 Mon Sep 17 00:00:00 2001 From: Carlos Souza Date: Mon, 12 Aug 2024 13:51:08 -0400 Subject: [PATCH 3/4] Ignore this module for now --- .dialyzer_ignore.exs | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .dialyzer_ignore.exs diff --git a/.dialyzer_ignore.exs b/.dialyzer_ignore.exs new file mode 100644 index 0000000..44d9cf4 --- /dev/null +++ b/.dialyzer_ignore.exs @@ -0,0 +1,3 @@ +[ + {"lib/mix/tasks/xogmios.gen.client.ex"} +] From 77a9e4cafcd4bc810be0ad02b84a03a5b7917763 Mon Sep 17 00:00:00 2001 From: Carlos Souza Date: Mon, 12 Aug 2024 14:00:49 -0400 Subject: [PATCH 4/4] Update docs. --- README.md | 25 +++++++++++++++++++++++++ lib/mix/tasks/xogmios.gen.client.ex | 3 ++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 64374ae..01cef8b 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,29 @@ ChainSync mini-protocol: A new file should be created at _./lib/my_app/chain_sync_client.ex_ +```elixir +defmodule MyApp.ChainSyncClient do + @moduledoc """ + This module syncs with the chain and reads new blocks + as they become available. + """ + + use Xogmios, :chain_sync + + def start_link(opts) do + initial_state = [] + opts = Keyword.merge(opts, initial_state) + Xogmios.start_chain_sync_link(__MODULE__, opts) + end + + @impl true + def handle_block(block, state) do + IO.puts("handle_block #{block["height"]}") + {:ok, :next_block, state} + end +end +``` + Add this new module to your application's supervision tree as such: ```elixir @@ -64,6 +87,8 @@ def start(_type, _args) do end ``` +Be sure the env `OGMIOS_URL` is populated and then start your mix application. + The value for the `url` option should be set to the address of your Ogmios instance. If you don't have access to an Ogmios endpoint, you can use https://demeter.run/ and start one for free. diff --git a/lib/mix/tasks/xogmios.gen.client.ex b/lib/mix/tasks/xogmios.gen.client.ex index ffaaed7..afd419f 100644 --- a/lib/mix/tasks/xogmios.gen.client.ex +++ b/lib/mix/tasks/xogmios.gen.client.ex @@ -96,7 +96,8 @@ defmodule Mix.Tasks.Xogmios.Gen.Client do """ defmodule <%= @app_module_name %>.<%= @client_module_name %> do @moduledoc \"\"\" - This module syncs with the chain and reads new blocks. + This module syncs with the chain and reads new blocks + as they become available. Be sure to add this module to your app's supervision tree like so: