Skip to content

Commit 15c05d1

Browse files
authored
Add new async transaction submission endpoint (#373)
* Implement the new async transaction endpoint * Add new error approach for async responses * Add tests for async transaction * Add docs in readme * Fix format * Fix credo
1 parent 30270d6 commit 15c05d1

15 files changed

+295
-15
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,9 @@ See [**Stellar.Horizon.Accounts**](https://hexdocs.pm/stellar_sdk/Stellar.Horizo
583583
# submit a transaction
584584
Stellar.Horizon.Transactions.create(Stellar.Horizon.Server.testnet(), base64_tx_envelope)
585585

586+
# submit a transaction asynchronously
587+
Stellar.Horizon.Transactions.create_async(Stellar.Horizon.Server.testnet(), base64_tx_envelope)
588+
586589
# retrieve a transaction
587590
Stellar.Horizon.Transactions.retrieve(Stellar.Horizon.Server.testnet(), "5ebd5c0af4385500b53dd63b0ef5f6e8feef1a7e1c86989be3cdcce825f3c0cc")
588591

lib/horizon/ErrorMapper.ex

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
defmodule Stellar.Horizon.ErrorMapper do
2+
@moduledoc """
3+
Assigns errors returned by the HTTP client to a custom structure.
4+
"""
5+
alias Stellar.Horizon.AsyncTransactionError
6+
alias Stellar.Horizon.AsyncTransaction
7+
alias Stellar.Horizon.Error
8+
9+
@type error_source :: :horizon | :network
10+
@type error_body :: map() | atom() | String.t()
11+
@type error :: {error_source(), error_body()}
12+
13+
@type t :: {:error, struct()}
14+
15+
@spec build(error :: error()) :: t()
16+
def build(
17+
{:horizon,
18+
%{hash: _hash, errorResultXdr: _error_result_xdr, tx_status: _tx_status} = decoded_body}
19+
) do
20+
error = AsyncTransactionError.new(decoded_body)
21+
{:error, error}
22+
end
23+
24+
def build({:horizon, %{hash: _hash, tx_status: _tx_status} = decoded_body}) do
25+
error = AsyncTransaction.new(decoded_body)
26+
{:error, error}
27+
end
28+
29+
def build(error), do: {:error, Error.new(error)}
30+
end

lib/horizon/asyncTransaction.ex

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
defmodule Stellar.Horizon.AsyncTransaction do
2+
@moduledoc """
3+
Represents a `Asynchronous transaction` resource from Horizon API.
4+
"""
5+
6+
@behaviour Stellar.Horizon.Resource
7+
8+
alias Stellar.Horizon.Mapping
9+
10+
@type t :: %__MODULE__{
11+
hash: String.t() | nil,
12+
tx_status: String.t() | nil
13+
}
14+
15+
defstruct [
16+
:hash,
17+
:tx_status
18+
]
19+
20+
@impl true
21+
def new(attrs, opts \\ [])
22+
23+
def new(attrs, _opts), do: Mapping.build(%__MODULE__{}, attrs)
24+
end

lib/horizon/asyncTransactionError.ex

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
defmodule Stellar.Horizon.AsyncTransactionError do
2+
@moduledoc """
3+
Represents a `Asynchronous transaction error` resource from Horizon API.
4+
"""
5+
6+
@behaviour Stellar.Horizon.Resource
7+
8+
alias Stellar.Horizon.Mapping
9+
10+
@type t :: %__MODULE__{
11+
hash: String.t() | nil,
12+
tx_status: String.t() | nil,
13+
errorResultXdr: String.t() | nil
14+
}
15+
16+
defstruct [
17+
:hash,
18+
:tx_status,
19+
:errorResultXdr
20+
]
21+
22+
@impl true
23+
def new(attrs, opts \\ [])
24+
25+
def new(attrs, _opts), do: Mapping.build(%__MODULE__{}, attrs)
26+
end

lib/horizon/client/default.ex

+4-6
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ defmodule Stellar.Horizon.Client.Default do
77

88
@behaviour Stellar.Horizon.Client.Spec
99

10-
alias Stellar.Horizon.{Error, Server}
10+
alias Stellar.Horizon.{ErrorMapper, Server}
1111

1212
@type status :: pos_integer()
1313
@type headers :: [{binary(), binary()}, ...]
1414
@type body :: binary()
1515
@type success_response :: {:ok, status(), headers(), body()}
1616
@type error_response :: {:error, status(), headers(), body()} | {:error, any()}
1717
@type client_response :: success_response() | error_response()
18-
@type parsed_response :: {:ok, map()} | {:error, Error.t()}
18+
@type parsed_response :: {:ok, map()} | {:error, struct()}
1919

2020
@impl true
2121
def request(%Server{url: base_url}, method, path, headers \\ [], body \\ "", opts \\ []) do
@@ -34,13 +34,11 @@ defmodule Stellar.Horizon.Client.Default do
3434

3535
defp handle_response({:ok, status, _headers, body}) when status >= 400 and status <= 599 do
3636
decoded_body = json_library().decode!(body, keys: :atoms)
37-
error = Error.new({:horizon, decoded_body})
38-
{:error, error}
37+
ErrorMapper.build({:horizon, decoded_body})
3938
end
4039

4140
defp handle_response({:error, reason}) do
42-
error = Error.new({:network, reason})
43-
{:error, error}
41+
ErrorMapper.build({:network, reason})
4442
end
4543

4644
@spec http_client() :: atom()

lib/horizon/error.ex

+10-2
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@ defmodule Stellar.Horizon.Error do
99
@type detail :: String.t() | nil
1010
@type base64_xdr :: String.t()
1111
@type result_code :: String.t()
12+
1213
@type result_codes :: %{
1314
optional(:transaction) => result_code(),
1415
optional(:operations) => list(result_code())
1516
}
1617
@type extras :: %{
1718
optional(:envelope_xdr) => base64_xdr(),
1819
optional(:result_codes) => result_codes(),
19-
optional(:result_xdr) => base64_xdr()
20+
optional(:result_xdr) => base64_xdr(),
21+
optional(:error) => detail()
2022
}
2123
@type error_source :: :horizon | :network
2224
@type error_body :: map() | atom() | String.t()
@@ -30,7 +32,13 @@ defmodule Stellar.Horizon.Error do
3032
extras: extras()
3133
}
3234

33-
defstruct [:type, :title, :status_code, :detail, extras: %{}]
35+
defstruct [
36+
:type,
37+
:title,
38+
:status_code,
39+
:detail,
40+
extras: %{}
41+
]
3442

3543
@spec new(error :: error()) :: t()
3644
def new({:horizon, %{type: type, title: title, status: status_code, detail: detail} = error}) do

lib/horizon/request.ex

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ defmodule Stellar.Horizon.Request do
1111
At a minimum, a request must have the endpoint and method specified to be valid.
1212
"""
1313

14-
alias Stellar.Horizon.{Collection, Error, Server}
14+
alias Stellar.Horizon.{Collection, Server}
1515
alias Stellar.Horizon.Client, as: Horizon
1616

1717
@type server :: Server.t()
@@ -26,8 +26,8 @@ defmodule Stellar.Horizon.Request do
2626
@type opts :: Keyword.t()
2727
@type params :: Keyword.t()
2828
@type query_params :: list(atom())
29-
@type response :: {:ok, map()} | {:error, Error.t()}
30-
@type parsed_response :: {:ok, struct()} | {:error, Error.t()}
29+
@type response :: {:ok, map()} | {:error, struct()}
30+
@type parsed_response :: {:ok, struct()} | {:error, struct()}
3131

3232
@type t :: %__MODULE__{
3333
method: method(),

lib/horizon/transactions.ex

+33-2
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,24 @@ defmodule Stellar.Horizon.Transactions do
1212
Horizon API reference: https://developers.stellar.org/api/resources/transactions/
1313
"""
1414

15-
alias Stellar.Horizon.{Collection, Effect, Error, Operation, Transaction, Request, Server}
15+
alias Stellar.Horizon.{
16+
Collection,
17+
Effect,
18+
Operation,
19+
Transaction,
20+
AsyncTransaction,
21+
Request,
22+
Server
23+
}
1624

1725
@type server :: Server.t()
1826
@type hash :: String.t()
1927
@type options :: Keyword.t()
2028
@type resource :: Transaction.t() | Collection.t()
21-
@type response :: {:ok, resource()} | {:error, Error.t()}
29+
@type response :: {:ok, resource()} | {:error, struct()}
2230

2331
@endpoint "transactions"
32+
@endpoint_async "transactions_async"
2433

2534
@doc """
2635
Creates a transaction to the Stellar network.
@@ -152,4 +161,26 @@ defmodule Stellar.Horizon.Transactions do
152161
|> Request.perform()
153162
|> Request.results(collection: {Operation, &list_operations(server, hash, &1)})
154163
end
164+
165+
@doc """
166+
Creates a transaction to the Stellar network asynchronously.
167+
168+
## Parameters:
169+
* `server`: The Horizon server to query.
170+
* `tx`: The base64-encoded XDR of the transaction.
171+
172+
## Examples
173+
174+
iex> Transactions.create_async(Stellar.Horizon.Server.testnet(), "AAAAAgAAAACQcEK2yfQA9CHrX+2UMkRIb/1wzltKqHpbdIcJbp+b/QAAAGQAAiEYAAAAAQAAAAEAAAAAAAAAAAAAAABgXP3QAAAAAQAAABBUZXN0IFRyYW5zYWN0aW9uAAAAAQAAAAAAAAABAAAAAJBwQrbJ9AD0Ietf7ZQyREhv/XDOW0qoelt0hwlun5v9AAAAAAAAAAAF9eEAAAAAAAAAAAFun5v9AAAAQKdJnG8QRiv9xGp1Oq7ACv/xR2BnNqjfUHrGNua7m4tWbrun3+GmAj6ca3xz+4ZppWRTbvTUcCxvpbHERZ85QgY=")
175+
{:ok, %AsyncTransaction{}}
176+
"""
177+
@spec create_async(server :: server(), base64_envelope :: String.t()) :: response()
178+
def create_async(server, base64_envelope) do
179+
server
180+
|> Request.new(:post, @endpoint_async)
181+
|> Request.add_headers([{"Content-Type", "application/x-www-form-urlencoded"}])
182+
|> Request.add_body(tx: base64_envelope)
183+
|> Request.perform()
184+
|> Request.results(as: AsyncTransaction)
185+
end
155186
end
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
defmodule Stellar.Horizon.AsyncTransactionTest do
2+
use ExUnit.Case
3+
4+
alias Stellar.Test.Fixtures.Horizon
5+
6+
alias Stellar.Horizon.AsyncTransaction
7+
8+
setup do
9+
json_body = Horizon.fixture("async_transaction")
10+
attrs = Jason.decode!(json_body, keys: :atoms)
11+
12+
%{attrs: attrs}
13+
end
14+
15+
test "new/2", %{attrs: attrs} do
16+
%AsyncTransaction{
17+
hash: "12958c37b341802a19ddada4c2a56b453a9cba728b2eefdfbc0b622e37379222",
18+
tx_status: "PENDING"
19+
} = AsyncTransaction.new(attrs)
20+
end
21+
22+
test "new/2 empty_attrs" do
23+
%AsyncTransaction{
24+
hash: nil,
25+
tx_status: nil
26+
} = AsyncTransaction.new(%{})
27+
end
28+
end

0 commit comments

Comments
 (0)