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

Submit tx #19

Merged
merged 7 commits into from
Feb 22, 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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

- Support for Tx Submission

## [v0.1.0](https://github.com/wowica/xogmios/releases/tag/v0.1.0) (2024-02-13)

### Added

- Support for Chain Sync protocol
- Partial support for Ledger State Queries:
- epoch
- eraStart
31 changes: 28 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ Mini-Protocols supported by this library:

- [x] Chain Synchronization
- [x] State Query (partially supported)
- [x] Tx Submission
- [ ] Mempool Monitoring
- [ ] Tx Submission


See [Examples](#examples) section below for information on how to use this library.
Expand Down Expand Up @@ -50,7 +50,9 @@ See section below for examples of client modules.

## Examples

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.
### Chain Sync

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
Expand All @@ -76,7 +78,9 @@ defmodule ChainSyncClient do
end
```

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

The following illustrates working with the **State Query** protocol. It runs queries against the tip of the chain.

```elixir
defmodule StateQueryClient do
Expand All @@ -97,6 +101,27 @@ defmodule StateQueryClient do
end
```

### Tx Submission

The following illustrates working with the **Transaction Submission** protocol. It submits a signed transaction, represented as a CBOR, to the Ogmios server.

```elixir
defmodule TxSubmissionClient do
use Xogmios, :tx_submission
alias Xogmios.TxSubmission

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

def submit_tx(pid \\ __MODULE__, cbor) do
# The CBOR must be a valid transaction,
# properly built and signed
TxSubmission.submit_tx(pid, cbor)
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
20 changes: 19 additions & 1 deletion lib/xogmios.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ defmodule Xogmios do

alias Xogmios.ChainSync
alias Xogmios.StateQuery
alias Xogmios.TxSubmission

@doc """
Starts a new State Query process linked to the current process
Starts a new State Query process linked to the current process.

`opts` are be passed to the underlying GenServer.
"""
def start_state_link(client, opts) do
StateQuery.start_link(client, opts)
Expand Down Expand Up @@ -77,6 +80,15 @@ defmodule Xogmios do
ChainSync.start_link(client, opts)
end

@doc """
Starts a new Tx Submission process linked to the current process.

`opts` are be passed to the underlying GenServer.
"""
def start_tx_submission_link(client, opts) do
TxSubmission.start_link(client, opts)
end

defmacro __using__(:state_query) do
quote do
use Xogmios.StateQuery
Expand All @@ -88,4 +100,10 @@ defmodule Xogmios do
use Xogmios.ChainSync
end
end

defmacro __using__(:tx_submission) do
quote do
use Xogmios.TxSubmission
end
end
end
79 changes: 79 additions & 0 deletions lib/xogmios/tx_submission.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
defmodule Xogmios.TxSubmission do
@moduledoc """
This module interfaces with the Tx Submission protocol.
"""

alias Xogmios.TxSubmission.Messages
alias Xogmios.TxSubmission.Response
alias Xogmios.TxSubmission.Server

@tx_submit_timeout 2_000

@doc """
Starts a new Tx Submission process linked to the current process.

This function should not be called directly, but rather via `Xogmios.start_tx_submission_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

@doc """
Submits a transaction to the server and returns a response including the transaction id.

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 CBOR of a signed transaction.
"""
@spec submit_tx(pid() | atom(), String.t()) :: {:ok, any()} | {:error, any()}
def submit_tx(client \\ __MODULE__, cbor) do
with {:ok, message} <- build_message(cbor),
{:ok, %Response{} = response} <- call_tx_submission(client, message) do
{:ok, response.result}
end
end

defp build_message(cbor),
do: {:ok, Messages.submit_tx(cbor)}

defp call_tx_submission(client, message) do
try do
case GenServer.call(client, {:submit_tx, message}, @tx_submit_timeout) do
{:ok, response} -> {:ok, response}
{:error, reason} -> {:error, reason}
end
catch
:exit, {:timeout, _} -> {:error, :timeout}
end
end

defmacro __using__(_opts) do
quote do
use GenServer

## Callbacks

@impl true
def init(args) do
url = Keyword.fetch!(args, :url)

case :websocket_client.start_link(url, Server, []) do
{:ok, ws_pid} ->
{:ok, %{ws_pid: ws_pid, response: nil, caller: nil}}

{:error, _} = error ->
error
end
end

@impl true
def handle_call({:submit_tx, message}, from, state) do
{:store_caller, _from} = send(state.ws_pid, {:store_caller, from})
:ok = :websocket_client.send(state.ws_pid, {:text, message})
{:noreply, state}
end
end
end
end
34 changes: 34 additions & 0 deletions lib/xogmios/tx_submission/messages.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
defmodule Xogmios.TxSubmission.Messages do
@moduledoc """
This module contains messages for the Tx Submission protocol
"""

alias Jason.DecodeError

@doc """
Submits signed transaction represented by given cbor argument
"""
def submit_tx(cbor) do
json = ~s"""
{
"jsonrpc": "2.0",
"method": "submitTransaction",
"params": {
"transaction": {
"cbor": "#{cbor}"
}
}
}
"""

validate_json!(json)
json
end

defp validate_json!(json) do
case Jason.decode(json) do
{:ok, _decoded} -> :ok
{:error, %DecodeError{} = error} -> raise "Invalid JSON: #{inspect(error)}"
end
end
end
6 changes: 6 additions & 0 deletions lib/xogmios/tx_submission/response.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
defmodule Xogmios.TxSubmission.Response do
@moduledoc false
# This module provides a common interface for responses from a Tx Submissions

defstruct [:result]
end
76 changes: 76 additions & 0 deletions lib/xogmios/tx_submission/server.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
defmodule Xogmios.TxSubmission.Server do
@moduledoc false

@behaviour :websocket_client

require Logger

alias Xogmios.TxSubmission.Response

defp handle_message(
%{"method" => _method, "result" => result},
state
) do
GenServer.reply(state.caller, {:ok, %Response{result: result}})
{:ok, state}
end

defp handle_message(%{"error" => %{"message" => message}}, state) do
GenServer.reply(state.caller, {:error, message})
{:ok, state}
end

defp handle_message(message, state) do
Logger.info("Unhandled message: #{inspect(message)}")
{:ok, state}
end

@impl true
def init(_args) do
{:once, %{caller: nil}}
end

@impl true
def onconnect(_arg0, state) do
{:ok, state}
end

@impl true
def ondisconnect(_reason, state) do
{:ok, state}
end

@impl true
def websocket_handle({:text, raw_message}, _conn, state) do
case Jason.decode(raw_message) do
{:ok, message} ->
handle_message(message, state)

{:error, reason} ->
Logger.warning("Error decoding message #{inspect(reason)}")
{:ok, state}
end
end

@impl true
def websocket_handle(_message, _conn, state) do
{:ok, state}
end

@impl true
def websocket_info({:store_caller, caller}, _req, state) do
# Stores caller of the query so that GenServer.reply knows
# who to return the response to
{:ok, %{state | caller: caller}}
end

@impl true
def websocket_info(_any, _arg1, state) do
{:ok, state}
end

@impl true
def websocket_terminate(_arg0, _arg1, _state) do
:ok
end
end
Loading
Loading