Skip to content

Commit cb08e0d

Browse files
authored
feat:Add HTTP support for TxSubmission protocol (#67) (#70)
* Add TxSubmission.HTTP for stateless HTTP transaction operations * Add HTTP convenience module methods for submit_tx and evaluate_tx * Add comprehensive test suite with mocks and error handling * Support transaction submission and evaluation via HTTP * Include URL parsing and WebSocket-to-HTTP conversion * Add error handling for CBOR validation, signatures, and UTxO references * Fix test mock detection order for trigger keywords
1 parent 08a52e6 commit cb08e0d

6 files changed

Lines changed: 401 additions & 19 deletions

File tree

config/test.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ import Config
22

33
config :xogmios, Xogmios.HealthCheck, http_client: Xogmios.HealthCheck.HTTPClientMock
44
config :xogmios, Xogmios.StateQuery.HTTP, http_client: Xogmios.HTTP.ClientMock
5+
config :xogmios, Xogmios.TxSubmission.HTTP, http_client: Xogmios.HTTP.ClientMock

examples/http_client.ex

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,24 @@ defmodule HTTPClient do
4444
end
4545

4646
@doc """
47-
Demo function showing all HTTP state query operations
47+
Submit a transaction via HTTP (stateless)
48+
"""
49+
def submit_transaction(cbor, base_url \\ "http://localhost:1337") do
50+
HTTP.submit_tx(base_url, cbor)
51+
end
52+
53+
@doc """
54+
Evaluate transaction execution units via HTTP
55+
"""
56+
def evaluate_transaction(cbor, base_url \\ "http://localhost:1337") do
57+
HTTP.evaluate_tx(base_url, cbor)
58+
end
59+
60+
@doc """
61+
Demo function showing all HTTP operations
4862
"""
4963
def demo(base_url \\ "http://localhost:1337") do
50-
IO.puts("Testing Xogmios HTTP State Queries at #{base_url}")
64+
IO.puts("Testing Xogmios HTTP API at #{base_url}")
5165
IO.puts("")
5266

5367
IO.puts("State Queries:")
@@ -83,6 +97,27 @@ defmodule HTTPClient do
8397
IO.puts("Protocol parameters error: #{inspect(error)}")
8498
end
8599

100+
IO.puts("")
101+
IO.puts("Transaction Operations:")
102+
103+
sample_cbor = "sample_cbor_data_for_demo"
104+
105+
case submit_transaction(sample_cbor, base_url) do
106+
{:ok, %{"transaction" => %{"id" => tx_id}}} ->
107+
IO.puts("Transaction submitted: #{tx_id}")
108+
109+
{:ok, result} ->
110+
IO.puts("Transaction result: #{inspect(result)}")
111+
112+
{:error, error} ->
113+
IO.puts("Submit error: #{inspect(error)}")
114+
end
115+
116+
case evaluate_transaction(sample_cbor, base_url) do
117+
{:ok, evaluation} -> IO.puts("Evaluation: #{inspect(evaluation)}")
118+
{:error, error} -> IO.puts("Evaluation error: #{inspect(error)}")
119+
end
120+
86121
IO.puts("")
87122
IO.puts("Demo completed!")
88123
end

lib/xogmios/http.ex

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ defmodule Xogmios.HTTP do
55
This module serves as a unified entry point for users who want to use
66
Xogmios in a stateless manner via HTTP instead of WebSocket connections.
77
"""
8-
alias Xogmios.StateQuery.HTTP
8+
alias Xogmios.StateQuery
9+
alias Xogmios.TxSubmission
910

1011
@doc """
1112
Sends a state query via HTTP.
@@ -22,6 +23,36 @@ defmodule Xogmios.HTTP do
2223
"""
2324
@spec send_query(String.t(), String.t(), map()) :: {:ok, term()} | {:error, term()}
2425
def send_query(base_url, query, params \\ %{}) do
25-
HTTP.send_query(base_url, query, params)
26+
StateQuery.HTTP.send_query(base_url, query, params)
27+
end
28+
29+
@doc """
30+
Evaluates the execution units of scripts in a transaction via HTTP.
31+
32+
This function is stateless and doesn't require a running process.
33+
34+
## Examples
35+
36+
iex> Xogmios.HTTP.evaluate_tx("http://localhost:1337", cbor_data)
37+
{:ok, %{"evaluation" => %{"script1" => %{"memory" => 1000, "steps" => 500}}}}
38+
"""
39+
@spec evaluate_tx(String.t(), String.t()) :: {:ok, any()} | {:error, any()}
40+
def evaluate_tx(base_url, cbor) do
41+
TxSubmission.HTTP.evaluate_tx(base_url, cbor)
42+
end
43+
44+
@doc """
45+
Submits a transaction via HTTP.
46+
47+
This function is stateless and doesn't require a running process.
48+
49+
## Examples
50+
51+
iex> Xogmios.HTTP.submit_tx("http://localhost:1337", cbor_data)
52+
{:ok, %{"transaction" => %{"id" => "abc123..."}}}
53+
"""
54+
@spec submit_tx(String.t(), String.t()) :: {:ok, any()} | {:error, any()}
55+
def submit_tx(base_url, cbor) do
56+
TxSubmission.HTTP.submit_tx(base_url, cbor)
2657
end
2758
end

lib/xogmios/tx_submission/http.ex

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
defmodule Xogmios.TxSubmission.HTTP do
2+
@moduledoc """
3+
Stateless HTTP client for Tx Submission protocol.
4+
5+
This module provides a simpler alternative to the WebSocket-based API
6+
for one-off transaction submissions without maintaining persistent connections.
7+
"""
8+
9+
alias Xogmios.TxSubmission.Messages
10+
alias Xogmios.TxSubmission.Response
11+
12+
@http_client :httpc
13+
@request_timeout 30_000
14+
15+
@doc """
16+
Submits a transaction via HTTP and returns a response including the transaction id.
17+
18+
This function is stateless and doesn't require a running process.
19+
"""
20+
def submit_tx(base_url, cbor) do
21+
url = parse_url(base_url)
22+
message = Messages.submit_tx(cbor)
23+
24+
with {:ok, response_body} <- http_request(url, message),
25+
{:ok, %Response{} = response} <- parse_response(response_body) do
26+
{:ok, response.result}
27+
end
28+
end
29+
30+
@doc """
31+
Evaluates the execution units of scripts present in a given transaction.
32+
33+
This function is stateless and doesn't require a running process.
34+
"""
35+
def evaluate_tx(base_url, cbor) do
36+
url = parse_url(base_url)
37+
message = Messages.evaluate_tx(cbor)
38+
39+
with {:ok, response_body} <- http_request(url, message),
40+
{:ok, %Response{} = response} <- parse_response(response_body) do
41+
{:ok, response.result}
42+
end
43+
end
44+
45+
defp parse_url(url) do
46+
url
47+
|> String.trim_trailing("/")
48+
|> replace_protocol()
49+
end
50+
51+
defp replace_protocol(url) do
52+
url
53+
|> String.replace_prefix("ws://", "http://")
54+
|> String.replace_prefix("wss://", "https://")
55+
end
56+
57+
defp http_request(url, message) do
58+
headers = [
59+
{~c"Content-Type", ~c"application/json"},
60+
{~c"Accept", ~c"application/json"}
61+
]
62+
63+
case client().request(
64+
:post,
65+
{String.to_charlist(url), headers, ~c"application/json", message},
66+
[timeout: @request_timeout],
67+
[]
68+
) do
69+
{:ok, {{_, 200, _}, _headers, body}} ->
70+
{:ok, body}
71+
72+
{:ok, {{_, status_code, _}, _headers, body}} ->
73+
{:error, {:http_error, status_code, body}}
74+
75+
{:error, reason} ->
76+
{:error, {:request_failed, reason}}
77+
end
78+
end
79+
80+
defp parse_response(body) do
81+
case Jason.decode(body) do
82+
{:ok, %{"result" => result}} ->
83+
{:ok, %Response{result: result}}
84+
85+
{:ok, %{"error" => error}} ->
86+
{:error, error}
87+
88+
{:ok, unexpected} ->
89+
{:error, {:unexpected_response, unexpected}}
90+
91+
{:error, reason} ->
92+
{:error, {:decode_error, reason}}
93+
end
94+
end
95+
96+
defp client do
97+
Application.get_env(:xogmios, __MODULE__, [])
98+
|> Keyword.get(:http_client, @http_client)
99+
end
100+
end

0 commit comments

Comments
 (0)