diff --git a/CHANGELOG.md b/CHANGELOG.md index e4bc2f5..67b60af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.22.0 (16.08.2024) + +- Add hash to handle transaction timeout response. See [PR #374](https://github.com/kommitters/stellar_sdk/pull/374) +- Add new async transaction submission endpoint. See [PR #373](https://github.com/kommitters/stellar_sdk/pull/373) + ## 0.21.2 (23.07.2024) - Update stellar base dependency. diff --git a/README.md b/README.md index 23e8339..5713dd1 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ The **Stellar SDK** is composed of two complementary components: **`TxBuild`** + ```elixir def deps do [ - {:stellar_sdk, "~> 0.21.2"} + {:stellar_sdk, "~> 0.22.0"} ] end ``` @@ -583,6 +583,9 @@ See [**Stellar.Horizon.Accounts**](https://hexdocs.pm/stellar_sdk/Stellar.Horizo # submit a transaction Stellar.Horizon.Transactions.create(Stellar.Horizon.Server.testnet(), base64_tx_envelope) +# submit a transaction asynchronously +Stellar.Horizon.Transactions.create_async(Stellar.Horizon.Server.testnet(), base64_tx_envelope) + # retrieve a transaction Stellar.Horizon.Transactions.retrieve(Stellar.Horizon.Server.testnet(), "5ebd5c0af4385500b53dd63b0ef5f6e8feef1a7e1c86989be3cdcce825f3c0cc") diff --git a/lib/horizon/ErrorMapper.ex b/lib/horizon/ErrorMapper.ex new file mode 100644 index 0000000..9624427 --- /dev/null +++ b/lib/horizon/ErrorMapper.ex @@ -0,0 +1,30 @@ +defmodule Stellar.Horizon.ErrorMapper do + @moduledoc """ + Assigns errors returned by the HTTP client to a custom structure. + """ + alias Stellar.Horizon.AsyncTransactionError + alias Stellar.Horizon.AsyncTransaction + alias Stellar.Horizon.Error + + @type error_source :: :horizon | :network + @type error_body :: map() | atom() | String.t() + @type error :: {error_source(), error_body()} + + @type t :: {:error, struct()} + + @spec build(error :: error()) :: t() + def build( + {:horizon, + %{hash: _hash, errorResultXdr: _error_result_xdr, tx_status: _tx_status} = decoded_body} + ) do + error = AsyncTransactionError.new(decoded_body) + {:error, error} + end + + def build({:horizon, %{hash: _hash, tx_status: _tx_status} = decoded_body}) do + error = AsyncTransaction.new(decoded_body) + {:error, error} + end + + def build(error), do: {:error, Error.new(error)} +end diff --git a/lib/horizon/asyncTransaction.ex b/lib/horizon/asyncTransaction.ex new file mode 100644 index 0000000..8415173 --- /dev/null +++ b/lib/horizon/asyncTransaction.ex @@ -0,0 +1,24 @@ +defmodule Stellar.Horizon.AsyncTransaction do + @moduledoc """ + Represents a `Asynchronous transaction` resource from Horizon API. + """ + + @behaviour Stellar.Horizon.Resource + + alias Stellar.Horizon.Mapping + + @type t :: %__MODULE__{ + hash: String.t() | nil, + tx_status: String.t() | nil + } + + defstruct [ + :hash, + :tx_status + ] + + @impl true + def new(attrs, opts \\ []) + + def new(attrs, _opts), do: Mapping.build(%__MODULE__{}, attrs) +end diff --git a/lib/horizon/asyncTransactionError.ex b/lib/horizon/asyncTransactionError.ex new file mode 100644 index 0000000..799a5dc --- /dev/null +++ b/lib/horizon/asyncTransactionError.ex @@ -0,0 +1,26 @@ +defmodule Stellar.Horizon.AsyncTransactionError do + @moduledoc """ + Represents a `Asynchronous transaction error` resource from Horizon API. + """ + + @behaviour Stellar.Horizon.Resource + + alias Stellar.Horizon.Mapping + + @type t :: %__MODULE__{ + hash: String.t() | nil, + tx_status: String.t() | nil, + errorResultXdr: String.t() | nil + } + + defstruct [ + :hash, + :tx_status, + :errorResultXdr + ] + + @impl true + def new(attrs, opts \\ []) + + def new(attrs, _opts), do: Mapping.build(%__MODULE__{}, attrs) +end diff --git a/lib/horizon/client/default.ex b/lib/horizon/client/default.ex index 52d7456..2077016 100644 --- a/lib/horizon/client/default.ex +++ b/lib/horizon/client/default.ex @@ -7,7 +7,7 @@ defmodule Stellar.Horizon.Client.Default do @behaviour Stellar.Horizon.Client.Spec - alias Stellar.Horizon.{Error, Server} + alias Stellar.Horizon.{ErrorMapper, Server} @type status :: pos_integer() @type headers :: [{binary(), binary()}, ...] @@ -15,7 +15,7 @@ defmodule Stellar.Horizon.Client.Default do @type success_response :: {:ok, status(), headers(), body()} @type error_response :: {:error, status(), headers(), body()} | {:error, any()} @type client_response :: success_response() | error_response() - @type parsed_response :: {:ok, map()} | {:error, Error.t()} + @type parsed_response :: {:ok, map()} | {:error, struct()} @impl true def request(%Server{url: base_url}, method, path, headers \\ [], body \\ "", opts \\ []) do @@ -34,13 +34,11 @@ defmodule Stellar.Horizon.Client.Default do defp handle_response({:ok, status, _headers, body}) when status >= 400 and status <= 599 do decoded_body = json_library().decode!(body, keys: :atoms) - error = Error.new({:horizon, decoded_body}) - {:error, error} + ErrorMapper.build({:horizon, decoded_body}) end defp handle_response({:error, reason}) do - error = Error.new({:network, reason}) - {:error, error} + ErrorMapper.build({:network, reason}) end @spec http_client() :: atom() diff --git a/lib/horizon/error.ex b/lib/horizon/error.ex index b0ae0dd..51c3e2e 100644 --- a/lib/horizon/error.ex +++ b/lib/horizon/error.ex @@ -9,14 +9,18 @@ defmodule Stellar.Horizon.Error do @type detail :: String.t() | nil @type base64_xdr :: String.t() @type result_code :: String.t() + @type hash :: String.t() + @type result_codes :: %{ optional(:transaction) => result_code(), optional(:operations) => list(result_code()) } @type extras :: %{ + optional(:hash) => hash(), optional(:envelope_xdr) => base64_xdr(), optional(:result_codes) => result_codes(), - optional(:result_xdr) => base64_xdr() + optional(:result_xdr) => base64_xdr(), + optional(:error) => detail() } @type error_source :: :horizon | :network @type error_body :: map() | atom() | String.t() @@ -30,7 +34,13 @@ defmodule Stellar.Horizon.Error do extras: extras() } - defstruct [:type, :title, :status_code, :detail, extras: %{}] + defstruct [ + :type, + :title, + :status_code, + :detail, + extras: %{} + ] @spec new(error :: error()) :: t() def new({:horizon, %{type: type, title: title, status: status_code, detail: detail} = error}) do diff --git a/lib/horizon/request.ex b/lib/horizon/request.ex index 715af0a..ce030db 100644 --- a/lib/horizon/request.ex +++ b/lib/horizon/request.ex @@ -11,7 +11,7 @@ defmodule Stellar.Horizon.Request do At a minimum, a request must have the endpoint and method specified to be valid. """ - alias Stellar.Horizon.{Collection, Error, Server} + alias Stellar.Horizon.{Collection, Server} alias Stellar.Horizon.Client, as: Horizon @type server :: Server.t() @@ -26,8 +26,8 @@ defmodule Stellar.Horizon.Request do @type opts :: Keyword.t() @type params :: Keyword.t() @type query_params :: list(atom()) - @type response :: {:ok, map()} | {:error, Error.t()} - @type parsed_response :: {:ok, struct()} | {:error, Error.t()} + @type response :: {:ok, map()} | {:error, struct()} + @type parsed_response :: {:ok, struct()} | {:error, struct()} @type t :: %__MODULE__{ method: method(), diff --git a/lib/horizon/transactions.ex b/lib/horizon/transactions.ex index 8a11ec5..2e9711e 100644 --- a/lib/horizon/transactions.ex +++ b/lib/horizon/transactions.ex @@ -12,15 +12,24 @@ defmodule Stellar.Horizon.Transactions do Horizon API reference: https://developers.stellar.org/api/resources/transactions/ """ - alias Stellar.Horizon.{Collection, Effect, Error, Operation, Transaction, Request, Server} + alias Stellar.Horizon.{ + Collection, + Effect, + Operation, + Transaction, + AsyncTransaction, + Request, + Server + } @type server :: Server.t() @type hash :: String.t() @type options :: Keyword.t() @type resource :: Transaction.t() | Collection.t() - @type response :: {:ok, resource()} | {:error, Error.t()} + @type response :: {:ok, resource()} | {:error, struct()} @endpoint "transactions" + @endpoint_async "transactions_async" @doc """ Creates a transaction to the Stellar network. @@ -152,4 +161,26 @@ defmodule Stellar.Horizon.Transactions do |> Request.perform() |> Request.results(collection: {Operation, &list_operations(server, hash, &1)}) end + + @doc """ + Creates a transaction to the Stellar network asynchronously. + + ## Parameters: + * `server`: The Horizon server to query. + * `tx`: The base64-encoded XDR of the transaction. + + ## Examples + + iex> Transactions.create_async(Stellar.Horizon.Server.testnet(), "AAAAAgAAAACQcEK2yfQA9CHrX+2UMkRIb/1wzltKqHpbdIcJbp+b/QAAAGQAAiEYAAAAAQAAAAEAAAAAAAAAAAAAAABgXP3QAAAAAQAAABBUZXN0IFRyYW5zYWN0aW9uAAAAAQAAAAAAAAABAAAAAJBwQrbJ9AD0Ietf7ZQyREhv/XDOW0qoelt0hwlun5v9AAAAAAAAAAAF9eEAAAAAAAAAAAFun5v9AAAAQKdJnG8QRiv9xGp1Oq7ACv/xR2BnNqjfUHrGNua7m4tWbrun3+GmAj6ca3xz+4ZppWRTbvTUcCxvpbHERZ85QgY=") + {:ok, %AsyncTransaction{}} + """ + @spec create_async(server :: server(), base64_envelope :: String.t()) :: response() + def create_async(server, base64_envelope) do + server + |> Request.new(:post, @endpoint_async) + |> Request.add_headers([{"Content-Type", "application/x-www-form-urlencoded"}]) + |> Request.add_body(tx: base64_envelope) + |> Request.perform() + |> Request.results(as: AsyncTransaction) + end end diff --git a/mix.exs b/mix.exs index 73a3c9b..d2365ce 100644 --- a/mix.exs +++ b/mix.exs @@ -2,7 +2,7 @@ defmodule Stellar.MixProject do use Mix.Project @github_url "https://github.com/kommitters/stellar_sdk" - @version "0.21.2" + @version "0.22.0" def project do [ diff --git a/test/horizon/async_transaction_test.exs b/test/horizon/async_transaction_test.exs new file mode 100644 index 0000000..d36e5e0 --- /dev/null +++ b/test/horizon/async_transaction_test.exs @@ -0,0 +1,28 @@ +defmodule Stellar.Horizon.AsyncTransactionTest do + use ExUnit.Case + + alias Stellar.Test.Fixtures.Horizon + + alias Stellar.Horizon.AsyncTransaction + + setup do + json_body = Horizon.fixture("async_transaction") + attrs = Jason.decode!(json_body, keys: :atoms) + + %{attrs: attrs} + end + + test "new/2", %{attrs: attrs} do + %AsyncTransaction{ + hash: "12958c37b341802a19ddada4c2a56b453a9cba728b2eefdfbc0b622e37379222", + tx_status: "PENDING" + } = AsyncTransaction.new(attrs) + end + + test "new/2 empty_attrs" do + %AsyncTransaction{ + hash: nil, + tx_status: nil + } = AsyncTransaction.new(%{}) + end +end diff --git a/test/horizon/client/default_test.exs b/test/horizon/client/default_test.exs index dfad977..22a2c71 100644 --- a/test/horizon/client/default_test.exs +++ b/test/horizon/client/default_test.exs @@ -104,8 +104,15 @@ defmodule Stellar.Horizon.Client.DefaultTest do end test "timeout", %{server: server} do - {:error, %Error{title: "Timeout", status_code: 504}} = - Default.request(server, :post, "/transactions?tx=timeout") + {:error, + %Error{ + title: "Transaction Submission Timeout", + status_code: 504, + extras: %{ + hash: _hash, + envelope_xdr: _envelope_xdr + } + }} = Default.request(server, :post, "/transactions?tx=timeout") end test "network_error", %{server: server} do diff --git a/test/horizon/transactions_test.exs b/test/horizon/transactions_test.exs index b748a9e..5084d96 100644 --- a/test/horizon/transactions_test.exs +++ b/test/horizon/transactions_test.exs @@ -4,7 +4,6 @@ defmodule Stellar.Horizon.Client.CannedTransactionRequests do alias Stellar.Test.Fixtures.Horizon @base_url "https://horizon-testnet.stellar.org" - @spec request( method :: atom(), url :: String.t(), @@ -91,6 +90,37 @@ defmodule Stellar.Horizon.Client.CannedTransactionRequests do json_body = Horizon.fixture("transaction") {:ok, 200, [], json_body} end + + def request( + :post, + @base_url <> "/transactions_async", + _headers, + "tx=AAAAAgAAAABp0mQhoc0djMaRJSbxa417D7Lx8Dw%2ByWmvhALa5UuYkgAAAGQADk7SAAAAUQAAAAAAAAABAAAABE1FTU8AAAABAAAAAAAAAAEAAAAAv6Mnl0vbOahrXvJAay9nTrMHQ1pZcvYeA4wrv0xOeA4AAAAAAAAAAAL68IAAAAAAAAAAAA%3D%3D", + _opts + ) do + json_error = Horizon.fixture("400_async_transaction_error_result_xdr") + {:ok, 400, [], json_error} + end + + def request(:post, @base_url <> "/transactions_async", _headers, "tx=tx_malformed", _opts) do + json_error = Horizon.fixture("400_transaction_malformed") + {:ok, 400, [], json_error} + end + + def request(:post, @base_url <> "/transactions_async", _headers, "tx=duplicate", _opts) do + json_error = Horizon.fixture("409_async_transaction_duplicate") + {:ok, 409, [], json_error} + end + + def request(:post, @base_url <> "/transactions_async", _headers, "tx=try_again_later", _opts) do + json_error = Horizon.fixture("409_async_transaction_try_again_later") + {:ok, 503, [], json_error} + end + + def request(:post, @base_url <> "/transactions_async", _headers, "tx=" <> _envelope, _opts) do + json_body = Horizon.fixture("async_transaction") + {:ok, 201, [], json_body} + end end defmodule Stellar.Horizon.TransactionsTest do @@ -104,6 +134,8 @@ defmodule Stellar.Horizon.TransactionsTest do Error, Operation, Transaction, + AsyncTransaction, + AsyncTransactionError, Transactions, Transaction.Preconditions, Transaction.TimeBounds, @@ -120,11 +152,29 @@ defmodule Stellar.Horizon.TransactionsTest do Application.delete_env(:stellar_sdk, :http_client) end) + async_transaction_error = %{ + # An erroneously generated base64_envelope may be generated when, e.g. the transaction is not signed. + bad_base64_envelope: + "AAAAAgAAAABp0mQhoc0djMaRJSbxa417D7Lx8Dw+yWmvhALa5UuYkgAAAGQADk7SAAAAUQAAAAAAAAABAAAABE1FTU8AAAABAAAAAAAAAAEAAAAAv6Mnl0vbOahrXvJAay9nTrMHQ1pZcvYeA4wrv0xOeA4AAAAAAAAAAAL68IAAAAAAAAAAAA==", + tx_status: "ERROR", + hash: "12958c37b341802a19ddada4c2a56b453a9cba728b2eefdfbc0b622e37379222", + errorResultXdr: "AAAAAAAAAGT////6AAAAAA==" + } + + async_transaction = %{ + base64_envelope: + "AAAAAgAAAABp0mQhoc0djMaRJSbxa417D7Lx8Dw+yWmvhALa5UuYkgAAAGQADk7SAAAAUQAAAAAAAAABAAAABE1FTU8AAAABAAAAAAAAAAEAAAAAv6Mnl0vbOahrXvJAay9nTrMHQ1pZcvYeA4wrv0xOeA4AAAAAAAAAAAL68IAAAAAAAAAAAeVLmJIAAABAk2E4qeHPsMKzj3kuCBoFC9stkVhOWpoJ3Fr5qf5zDu2eSz8blBi+4msu+PV8pg5e2MdymSOEBbPY2XLJcefRCw==", + tx_status: "PENDING", + hash: "12958c37b341802a19ddada4c2a56b453a9cba728b2eefdfbc0b622e37379222" + } + %{ source_account: "GCXMWUAUF37IWOOV2FRDKWEX3O2IHLM2FYH4WPI4PYUKAIFQEUU5X3TD", base64_envelope: "AAAAAJ2kP2xLaOVLj6DRwX1mMyA0mubYnYvu0g8OdoDqxXuFAAAAZADjfzAACzBMAAAAAQAAAAAAAAAAAAAAAF4vYIYAAAABAAAABjI5ODQyNAAAAAAAAQAAAAAAAAABAAAAAKdeYELovtcnTxqPEVsdbxHLMoMRalZsK7lo/+3ARzUZAAAAAAAAAADUFJPYAAAAAAAAAAHqxXuFAAAAQBpLpQyh+mwDd5nDSxTaAh5wopBBUaSD1eOK9MdiO+4kWKVTqSr/Ko3kYE/+J42Opsewf81TwINONPbY2CtPggE=", - hash: "132c440e984ab97d895f3477015080aafd6c4375f6a70a87327f7f95e13c4e31" + hash: "132c440e984ab97d895f3477015080aafd6c4375f6a70a87327f7f95e13c4e31", + async_transaction: async_transaction, + async_transaction_error: async_transaction_error } end @@ -133,6 +183,16 @@ defmodule Stellar.Horizon.TransactionsTest do Transactions.create(Server.testnet(), base64_envelope) end + test "create_async/1", %{ + async_transaction: %{base64_envelope: base64_envelope, hash: hash, tx_status: tx_status} + } do + {:ok, + %Stellar.Horizon.AsyncTransaction{ + hash: ^hash, + tx_status: ^tx_status + }} = Transactions.create_async(Server.testnet(), base64_envelope) + end + test "retrieve/1", %{ hash: hash, base64_envelope: base64_envelope, @@ -257,4 +317,49 @@ defmodule Stellar.Horizon.TransactionsTest do extras: %{result_codes: %{transaction: "tx_insufficient_fee"}} }} = Transactions.create(Server.testnet(), "bad") end + + test "malformed async transaction error" do + {:error, + %Error{ + type: "https://stellar.org/horizon-errors/transaction_malformed", + title: "Transaction Malformed", + status_code: 400, + detail: _detail, + extras: %{envelope_xdr: "tx_malformed", error: %{}} + }} = Transactions.create_async(Server.testnet(), "tx_malformed") + end + + test "async transaction duplicate" do + # This happens when the same request is sent twice in a row to enpoint ‘transaction_async’. + {:error, + %AsyncTransaction{ + hash: "822a283337b124f82b0b8725b39738d2f3e86b699e25b9b646809336384bf41c", + tx_status: "DUPLICATE" + }} = Transactions.create_async(Server.testnet(), "duplicate") + end + + test "async transaction try_again_later" do + # This happens when the same request is spammed multiple times to enpoint ‘transaction_async’. + {:error, + %AsyncTransaction{ + hash: "822a283337b124f82b0b8725b39738d2f3e86b699e25b9b646809336384bf41c", + tx_status: "TRY_AGAIN_LATER" + }} = Transactions.create_async(Server.testnet(), "try_again_later") + end + + test "async transaction errorResultXdr", %{ + async_transaction_error: %{ + bad_base64_envelope: bad_base64_envelope, + hash: hash, + tx_status: tx_status, + errorResultXdr: error_result_xdr + } + } do + {:error, + %AsyncTransactionError{ + hash: ^hash, + tx_status: ^tx_status, + errorResultXdr: ^error_result_xdr + }} = Transactions.create_async(Server.testnet(), bad_base64_envelope) + end end diff --git a/test/support/fixtures/horizon/400_async_transaction_error_result_xdr.json b/test/support/fixtures/horizon/400_async_transaction_error_result_xdr.json new file mode 100644 index 0000000..dce75c8 --- /dev/null +++ b/test/support/fixtures/horizon/400_async_transaction_error_result_xdr.json @@ -0,0 +1,5 @@ +{ + "errorResultXdr": "AAAAAAAAAGT////6AAAAAA==", + "tx_status": "ERROR", + "hash": "12958c37b341802a19ddada4c2a56b453a9cba728b2eefdfbc0b622e37379222" +} diff --git a/test/support/fixtures/horizon/400_transaction_malformed.json b/test/support/fixtures/horizon/400_transaction_malformed.json new file mode 100644 index 0000000..97f358d --- /dev/null +++ b/test/support/fixtures/horizon/400_transaction_malformed.json @@ -0,0 +1,10 @@ +{ + "type": "https://stellar.org/horizon-errors/transaction_malformed", + "title": "Transaction Malformed", + "status": 400, + "detail": "Horizon could not decode the transaction envelope in this request. A transaction should be an XDR TransactionEnvelope struct encoded using base64. The envelope read from this request is echoed in the `extras.envelope_xdr` field of this response for your convenience.", + "extras": { + "envelope_xdr": "tx_malformed", + "error": {} + } +} diff --git a/test/support/fixtures/horizon/409_async_transaction_duplicate.json b/test/support/fixtures/horizon/409_async_transaction_duplicate.json new file mode 100644 index 0000000..ed37760 --- /dev/null +++ b/test/support/fixtures/horizon/409_async_transaction_duplicate.json @@ -0,0 +1,4 @@ +{ + "tx_status": "DUPLICATE", + "hash": "822a283337b124f82b0b8725b39738d2f3e86b699e25b9b646809336384bf41c" +} diff --git a/test/support/fixtures/horizon/409_async_transaction_try_again_later.json b/test/support/fixtures/horizon/409_async_transaction_try_again_later.json new file mode 100644 index 0000000..0804f1c --- /dev/null +++ b/test/support/fixtures/horizon/409_async_transaction_try_again_later.json @@ -0,0 +1,4 @@ +{ + "tx_status": "TRY_AGAIN_LATER", + "hash": "822a283337b124f82b0b8725b39738d2f3e86b699e25b9b646809336384bf41c" +} diff --git a/test/support/fixtures/horizon/504.json b/test/support/fixtures/horizon/504.json index ac18995..20627c2 100644 --- a/test/support/fixtures/horizon/504.json +++ b/test/support/fixtures/horizon/504.json @@ -1,6 +1,10 @@ { - "type": "https://stellar.org/horizon-errors/timeout", - "title": "Timeout", + "type": "https://developers.stellar.org/docs/data/horizon/api-reference/errors/http-status-codes/horizon-specific/timeout", + "title": "Transaction Submission Timeout", "status": 504, - "detail": "Your request timed out before completing. Please try your request again. If you are submitting a transaction make sure you are sending exactly the same transaction (with the same sequence number)." + "detail": "Your transaction submission request has timed out. This does not necessarily mean the submission has failed. Before resubmitting, please use the transaction hash provided in `extras.hash` to poll the GET /transactions endpoint for sometime and check if it was included in a ledger.", + "extras": { + "hash": "12958c37b341802a19ddada4c2a56b453a9cba728b2eefdfbc0b622e37379222", + "envelope_xdr": "AAAAAJ2kP2xLaOVLj6DRwX1mMyA0mubYnYvu0g8OdoDqxXuFAAAAZADjfzAACzBMAAAAAQAAAAAAAAAAAAAAAF4vYIYAAAABAAAABjI5ODQyNAAAAAAAAQAAAAAAAAABAAAAAKdeYELovtcnTxqPEVsdbxHLMoMRalZsK7lo/+3ARzUZAAAAAAAAAADUFJPYAAAAAAAAAAHqxXuFAAAAQBpLpQyh+mwDd5nDSxTaAh5wopBBUaSD1eOK9MdiO+4kWKVTqSr/Ko3kYE/+J42Opsewf81TwINONPbY2CtPggE=" + } } diff --git a/test/support/fixtures/horizon/async_transaction.json b/test/support/fixtures/horizon/async_transaction.json new file mode 100644 index 0000000..188e561 --- /dev/null +++ b/test/support/fixtures/horizon/async_transaction.json @@ -0,0 +1,4 @@ +{ + "tx_status": "PENDING", + "hash": "12958c37b341802a19ddada4c2a56b453a9cba728b2eefdfbc0b622e37379222" +}