diff --git a/README.md b/README.md index e937963..e3c30d8 100644 --- a/README.md +++ b/README.md @@ -323,6 +323,72 @@ Soroban.RPC.get_transaction(server, hash) ``` +#### Get Transactions + +The getTransactions method return a detailed list of transactions starting from the user specified starting point. + +**Parameters** + +- `server`: `Soroban.RPC.Server` struct - The Soroban-RPC server to interact with. +- `TransactionsPayload`: + + - `start_ledger`: Stringified ledger sequence number to fetch events after (inclusive). This method will return an error if start_ledger is less than the oldest ledger stored in this node, or greater than the latest ledger seen by this node. If a cursor is included in the request, start_ledger must be omitted. + + - `cursor`: A string ID that points to a specific location in a collection of responses and is pulled from the paging_token value of a record. When a cursor is provided Soroban-RPC will not include the element whose id matches the cursor in the response. Only elements which appear after the cursor are included. + + - `limit`: The maximum number of records returned. For getTransactions, this ranges from 1 to 200 and defaults to 50. + +**Example** + +```elixir +alias Soroban.RPC.TransactionsPayload + +server = Soroban.RPC.Server.testnet() + +start_ledger = 600000 +limit = 2 + +transactions_payload = TransactionsPayload.new(start_ledger: start_ledger, limit: limit) + +Soroban.RPC.get_transactions(server, transactions_payload) + +{:ok, + %Soroban.RPC.GetTransactionsResponse{ + transactions: [ + %{ + status: "FAILED", + applicationOrder: 1, + feeBump: false, + envelopeXdr: + "AAAAAgAAAACDz21Q3CTITlGqRus3/96/05EDivbtfJncNQKt64BTbAAAASwAAKkyAAXlMwAAAAEAAAAAAAAAAAAAAABmWeASAAAAAQAAABR3YWxsZXQ6MTcxMjkwNjMzNjUxMAAAAAEAAAABAAAAAIPPbVDcJMhOUapG6zf/3r/TkQOK9u18mdw1Aq3rgFNsAAAAAQAAAABwOSvou8mtwTtCkysVioO35TSgyRir2+WGqO8FShG/GAAAAAFVQUgAAAAAAO371tlrHUfK+AvmQvHje1jSUrvJb3y3wrJ7EplQeqTkAAAAAAX14QAAAAAAAAAAAeuAU2wAAABAn+6A+xXvMasptAm9BEJwf5Y9CLLQtV44TsNqS8ocPmn4n8Rtyb09SBiFoMv8isYgeQU5nAHsIwBNbEKCerusAQ==", + resultXdr: "AAAAAAAAAGT/////AAAAAQAAAAAAAAAB////+gAAAAA=", + resultMetaXdr: + "AAAAAwAAAAAAAAACAAAAAwAc0RsAAAAAAAAAAIPPbVDcJMhOUapG6zf/3r/TkQOK9u18mdw1Aq3rgFNsAAAAF0YpYBQAAKkyAAXlMgAAAAsAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAABzRGgAAAABmWd/VAAAAAAAAAAEAHNEbAAAAAAAAAACDz21Q3CTITlGqRus3/96/05EDivbtfJncNQKt64BTbAAAABdGKWAUAACpMgAF5TMAAAALAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAc0RsAAAAAZlnf2gAAAAAAAAAAAAAAAAAAAAA=", + ledger: 1_888_539, + createdAt: 1_717_166_042 + }, + %{ + status: "SUCCESS", + applicationOrder: 2, + feeBump: false, + envelopeXdr: + "AAAAAgAAAAC4EZup+ewCs/doS3hKbeAa4EviBHqAFYM09oHuLtqrGAAPQkAAGgQZAAAANgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAABB90WssODNIgi6BHveqzxTRmIpvAFRyVNM+Hm2GVuCcAAAAAAAAAAAq6aHAHZ2sd9aPbRsskrlXMLWIwqs4Sv2Bk+VwuIR+9wAAABdIdugAAAAAAAAAAAIu2qsYAAAAQERzKOqYYiPXNwsiL8ADAG/f45RBssmf3umGzw4qKkLGlObuPdX0buWmTGrhI13SG38F2V8Mp9DI+eDkcCjMSAOGVuCcAAAAQHnm0o/r+Gsl+6oqBgSbqoSY37gflvQB3zZRghuir0N75UVerd0Q50yG5Zfu08i2crhx6uk+5HYTl8/Sa7uZ+Qc=", + resultXdr: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=", + resultMetaXdr: + "AAAAAwAAAAAAAAACAAAAAwAc0RsAAAAAAAAAALgRm6n57AKz92hLeEpt4BrgS+IEeoAVgzT2ge4u2qsYAAAAADwzS2gAGgQZAAAANQAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAABzPVAAAAABmWdZ2AAAAAAAAAAEAHNEbAAAAAAAAAAC4EZup+ewCs/doS3hKbeAa4EviBHqAFYM09oHuLtqrGAAAAAA8M0toABoEGQAAADYAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAc0RsAAAAAZlnf2gAAAAAAAAABAAAAAwAAAAMAHNEaAAAAAAAAAAAQfdFrLDgzSIIugR73qs8U0ZiKbwBUclTTPh5thlbgnABZJUSd0V2hAAAAawAAAlEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAaBGEAAAAAZkspCwAAAAAAAAABABzRGwAAAAAAAAAAEH3Rayw4M0iCLoEe96rPFNGYim8AVHJU0z4ebYZW4JwAWSUtVVp1oQAAAGsAAAJRAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAwAAAAAAGgRhAAAAAGZLKQsAAAAAAAAAAAAc0RsAAAAAAAAAACrpocAdnax31o9tGyySuVcwtYjCqzhK/YGT5XC4hH73AAAAF0h26AAAHNEbAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + ledger: 1_888_539, + createdAt: 1_717_166_042 + } + ], + latest_ledger: 1_888_542, + latest_ledger_close_timestamp: 1_717_166_057, + oldest_ledger: 1_871_263, + oldest_ledger_close_timestamp: 1_717_075_350, + cursor: "8111217537191937" + }} + +``` + #### Get Health General node health check. diff --git a/lib/rpc.ex b/lib/rpc.ex index 2173306..23e61c2 100644 --- a/lib/rpc.ex +++ b/lib/rpc.ex @@ -11,6 +11,7 @@ defmodule Soroban.RPC do GetLedgerEntriesResponse, GetNetwork, GetTransaction, + GetTransactions, GetVersionInfo, SendTransaction, SimulateTransaction @@ -37,6 +38,7 @@ defmodule Soroban.RPC do as: :request defdelegate get_transaction(server, hash), to: GetTransaction, as: :request + defdelegate get_transactions(server, hash), to: GetTransactions, as: :request defdelegate get_ledger_entries(server, keys), to: GetLedgerEntries, as: :request defdelegate get_events(server, payload), to: GetEvents, as: :request defdelegate get_health(server), to: GetHealth, as: :request diff --git a/lib/rpc/endpoints/events_payload/events_payload.ex b/lib/rpc/endpoints/events_payload/events_payload.ex index 62a2488..600272a 100644 --- a/lib/rpc/endpoints/events_payload/events_payload.ex +++ b/lib/rpc/endpoints/events_payload/events_payload.ex @@ -2,20 +2,15 @@ defmodule Soroban.RPC.EventsPayload do @moduledoc """ `EventsPayload` struct definition. """ + import Soroban.RPC.Helper alias Soroban.RPC.EventFilter @type args :: Keyword.t() - @type cursor :: binary() | nil - @type cursor_validation :: {:ok, cursor()} - @type limit :: number() | nil - @type limit_validation :: {:ok, limit()} @type error :: {:error, atom()} @type start_ledger :: non_neg_integer() | nil - @type start_ledger_validation :: {:ok, start_ledger()} | error() @type filters :: list(EventFilter.t()) | nil @type filters_validation :: {:ok, filters()} | error() @type pagination :: map() | nil - @type pagination_validation :: {:ok, pagination()} | error() @type request_args :: map() | :error @type t :: %__MODULE__{ start_ledger: start_ledger(), @@ -66,23 +61,6 @@ defmodule Soroban.RPC.EventsPayload do def to_request_args(_struct), do: :error - @spec validate_start_ledger(start_ledger :: start_ledger()) :: start_ledger_validation() - defp validate_start_ledger(start_ledger) when is_number(start_ledger) and start_ledger >= 0, - do: {:ok, start_ledger} - - defp validate_start_ledger(nil), do: {:ok, nil} - defp validate_start_ledger(_start_ledger), do: {:error, :invalid_start_ledger} - - @spec validate_cursor(cursor :: cursor()) :: cursor_validation() - defp validate_cursor(cursor) when is_binary(cursor), do: {:ok, cursor} - defp validate_cursor(nil), do: {:ok, nil} - defp validate_cursor(_cursor), do: {:error, :invalid_cursor} - - @spec validate_limit(limit :: limit()) :: limit_validation() - defp validate_limit(limit) when is_number(limit), do: {:ok, limit} - defp validate_limit(nil), do: {:ok, nil} - defp validate_limit(_limit), do: {:error, :invalid_limit} - @spec validate_filters(filters :: filters()) :: filters_validation() defp validate_filters([%EventFilter{} = filter | _] = filters) when length(filters) in 1..5 do if Enum.any?(filters, fn f -> f.__struct__ != filter.__struct__ end), diff --git a/lib/rpc/endpoints/get_events.ex b/lib/rpc/endpoints/get_events.ex index 39f505e..a6c9613 100644 --- a/lib/rpc/endpoints/get_events.ex +++ b/lib/rpc/endpoints/get_events.ex @@ -4,8 +4,7 @@ defmodule Soroban.RPC.GetEvents do """ @behaviour Soroban.RPC.Endpoint.Spec - alias Soroban.RPC.EventsPayload - alias Soroban.RPC.{GetEventsResponse, Request} + alias Soroban.RPC.{EventsPayload, GetEventsResponse, Request} @endpoint "getEvents" diff --git a/lib/rpc/endpoints/get_transactions.ex b/lib/rpc/endpoints/get_transactions.ex new file mode 100644 index 0000000..688c5b4 --- /dev/null +++ b/lib/rpc/endpoints/get_transactions.ex @@ -0,0 +1,22 @@ +defmodule Soroban.RPC.GetTransactions do + @moduledoc """ + GetTransactions request implementation for Soroban RPC. + """ + @behaviour Soroban.RPC.Endpoint.Spec + + alias Soroban.RPC.{GetTransactionsResponse, Request, TransactionsPayload} + + @endpoint "getTransactions" + + @impl true + def request(server, %TransactionsPayload{} = transactions_payload) do + payload = TransactionsPayload.to_request_args(transactions_payload) + + server + |> Request.new(@endpoint) + |> Request.add_headers([{"Content-Type", "application/json"}]) + |> Request.add_params(payload) + |> Request.perform() + |> Request.results(as: GetTransactionsResponse) + end +end diff --git a/lib/rpc/endpoints/helper.ex b/lib/rpc/endpoints/helper.ex new file mode 100644 index 0000000..336b306 --- /dev/null +++ b/lib/rpc/endpoints/helper.ex @@ -0,0 +1,30 @@ +defmodule Soroban.RPC.Helper do + @moduledoc """ + Helper functions for RPC endpoints. + """ + + @type error :: {:error, atom()} + @type start_ledger :: non_neg_integer() | nil + @type start_ledger_validation :: {:ok, start_ledger()} | error() + @type cursor :: binary() | nil + @type cursor_validation :: {:ok, cursor()} + @type limit :: number() | nil + @type limit_validation :: {:ok, limit()} + + @spec validate_start_ledger(start_ledger :: start_ledger()) :: start_ledger_validation() + def validate_start_ledger(start_ledger) when is_number(start_ledger) and start_ledger >= 0, + do: {:ok, start_ledger} + + def validate_start_ledger(nil), do: {:ok, nil} + def validate_start_ledger(_start_ledger), do: {:error, :invalid_start_ledger} + + @spec validate_cursor(cursor :: cursor()) :: cursor_validation() + def validate_cursor(cursor) when is_binary(cursor), do: {:ok, cursor} + def validate_cursor(nil), do: {:ok, nil} + def validate_cursor(_cursor), do: {:error, :invalid_cursor} + + @spec validate_limit(limit :: limit()) :: limit_validation() + def validate_limit(limit) when is_number(limit), do: {:ok, limit} + def validate_limit(nil), do: {:ok, nil} + def validate_limit(_limit), do: {:error, :invalid_limit} +end diff --git a/lib/rpc/endpoints/spec.ex b/lib/rpc/endpoints/spec.ex index 04fb390..685f0ad 100644 --- a/lib/rpc/endpoints/spec.ex +++ b/lib/rpc/endpoints/spec.ex @@ -3,10 +3,10 @@ defmodule Soroban.RPC.Endpoint.Spec do Specifies the callbacks to build the Soroban's endpoints. """ - alias Soroban.RPC.{Error, EventsPayload, HTTPError, Server} + alias Soroban.RPC.{Error, EventsPayload, HTTPError, Server, TransactionsPayload} @type response :: {:ok, struct()} | {:error, Error.t() | HTTPError.t()} - @type params :: String.t() | EventsPayload.t() | keyword() | nil + @type params :: String.t() | EventsPayload.t() | keyword() | nil | TransactionsPayload.t() @callback request(server :: Server.t(), params :: params()) :: response() end diff --git a/lib/rpc/endpoints/transactions_payload/transactions_payload.ex b/lib/rpc/endpoints/transactions_payload/transactions_payload.ex new file mode 100644 index 0000000..9a1b6cb --- /dev/null +++ b/lib/rpc/endpoints/transactions_payload/transactions_payload.ex @@ -0,0 +1,45 @@ +defmodule Soroban.RPC.TransactionsPayload do + @moduledoc """ + `TransactionsPayload` struct definition. + """ + import Soroban.RPC.Helper + + @type args :: Keyword.t() + @type start_ledger :: non_neg_integer() | nil + @type pagination :: map() | nil + @type request_args :: map() | :error + @type t :: %__MODULE__{ + start_ledger: start_ledger(), + pagination: pagination() + } + + defstruct [:start_ledger, :pagination] + + @spec new(args :: args()) :: t() + def new(args) when is_list(args) do + start_ledger = Keyword.get(args, :start_ledger) + cursor = Keyword.get(args, :cursor) + limit = Keyword.get(args, :limit) + + with {:ok, start_ledger} <- validate_start_ledger(start_ledger), + {:ok, cursor} <- validate_cursor(cursor), + {:ok, limit} <- validate_limit(limit) do + %__MODULE__{ + start_ledger: start_ledger, + pagination: %{cursor: cursor, limit: limit} + } + end + end + + def new(_args), do: {:error, :invalid_args} + + @spec to_request_args(t()) :: request_args() + def to_request_args(%__MODULE__{ + start_ledger: start_ledger, + pagination: pagination + }) do + %{startLedger: start_ledger, filters: nil, pagination: pagination} + end + + def to_request_args(_struct), do: :error +end diff --git a/lib/rpc/responses/get_transactions_response.ex b/lib/rpc/responses/get_transactions_response.ex new file mode 100644 index 0000000..2dd24e4 --- /dev/null +++ b/lib/rpc/responses/get_transactions_response.ex @@ -0,0 +1,33 @@ +defmodule Soroban.RPC.GetTransactionsResponse do + @moduledoc """ + `GetTransactionsResponse` struct definition. + """ + @behaviour Soroban.RPC.Response.Spec + + @type transactions :: list(map()) + @type latest_ledger :: non_neg_integer() + @type latest_ledger_close_timestamp :: String.t() + @type oldest_ledger :: non_neg_integer() + @type oldest_ledger_close_timestamp :: String.t() + @type cursor :: String.t() + @type t :: %__MODULE__{ + latest_ledger: latest_ledger(), + latest_ledger_close_timestamp: latest_ledger_close_timestamp(), + oldest_ledger: oldest_ledger(), + oldest_ledger_close_timestamp: oldest_ledger_close_timestamp(), + cursor: cursor(), + transactions: transactions() + } + + defstruct [ + :transactions, + :latest_ledger, + :latest_ledger_close_timestamp, + :oldest_ledger, + :oldest_ledger_close_timestamp, + :cursor + ] + + @impl true + def new(attrs), do: struct(%__MODULE__{}, attrs) +end diff --git a/test/rpc/endpoints/get_transactions_test.exs b/test/rpc/endpoints/get_transactions_test.exs new file mode 100644 index 0000000..0215b03 --- /dev/null +++ b/test/rpc/endpoints/get_transactions_test.exs @@ -0,0 +1,84 @@ +defmodule Soroban.RPC.GetTransactionsCannedClientImpl do + @moduledoc false + + @behaviour Soroban.RPC.Client.Spec + + @impl true + def request(_endpoint, _url, _headers, _body, _opts) do + send(self(), {:request, "RESPONSE"}) + + {:ok, + %{ + transactions: [ + %{ + status: "FAILED", + applicationOrder: 1, + feeBump: false, + envelopeXdr: + "AAAAAgAAAACDz21Q3CTITlGqRus3/96/05EDivbtfJncNQKt64BTbAAAASwAAKkyAAXlMwAAAAEAAAAAAAAAAAAAAABmWeASAAAAAQAAABR3YWxsZXQ6MTcxMjkwNjMzNjUxMAAAAAEAAAABAAAAAIPPbVDcJMhOUapG6zf/3r/TkQOK9u18mdw1Aq3rgFNsAAAAAQAAAABwOSvou8mtwTtCkysVioO35TSgyRir2+WGqO8FShG/GAAAAAFVQUgAAAAAAO371tlrHUfK+AvmQvHje1jSUrvJb3y3wrJ7EplQeqTkAAAAAAX14QAAAAAAAAAAAeuAU2wAAABAn+6A+xXvMasptAm9BEJwf5Y9CLLQtV44TsNqS8ocPmn4n8Rtyb09SBiFoMv8isYgeQU5nAHsIwBNbEKCerusAQ==", + resultXdr: "AAAAAAAAAGT/////AAAAAQAAAAAAAAAB////+gAAAAA=", + resultMetaXdr: + "AAAAAwAAAAAAAAACAAAAAwAc0RsAAAAAAAAAAIPPbVDcJMhOUapG6zf/3r/TkQOK9u18mdw1Aq3rgFNsAAAAF0YpYBQAAKkyAAXlMgAAAAsAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAABzRGgAAAABmWd/VAAAAAAAAAAEAHNEbAAAAAAAAAACDz21Q3CTITlGqRus3/96/05EDivbtfJncNQKt64BTbAAAABdGKWAUAACpMgAF5TMAAAALAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAc0RsAAAAAZlnf2gAAAAAAAAAAAAAAAAAAAAA=", + ledger: 1_888_539, + createdAt: 1_717_166_042 + }, + %{ + status: "SUCCESS", + applicationOrder: 2, + feeBump: false, + envelopeXdr: + "AAAAAgAAAAC4EZup+ewCs/doS3hKbeAa4EviBHqAFYM09oHuLtqrGAAPQkAAGgQZAAAANgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAABB90WssODNIgi6BHveqzxTRmIpvAFRyVNM+Hm2GVuCcAAAAAAAAAAAq6aHAHZ2sd9aPbRsskrlXMLWIwqs4Sv2Bk+VwuIR+9wAAABdIdugAAAAAAAAAAAIu2qsYAAAAQERzKOqYYiPXNwsiL8ADAG/f45RBssmf3umGzw4qKkLGlObuPdX0buWmTGrhI13SG38F2V8Mp9DI+eDkcCjMSAOGVuCcAAAAQHnm0o/r+Gsl+6oqBgSbqoSY37gflvQB3zZRghuir0N75UVerd0Q50yG5Zfu08i2crhx6uk+5HYTl8/Sa7uZ+Qc=", + resultXdr: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=", + resultMetaXdr: + "AAAAAwAAAAAAAAACAAAAAwAc0RsAAAAAAAAAALgRm6n57AKz92hLeEpt4BrgS+IEeoAVgzT2ge4u2qsYAAAAADwzS2gAGgQZAAAANQAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAABzPVAAAAABmWdZ2AAAAAAAAAAEAHNEbAAAAAAAAAAC4EZup+ewCs/doS3hKbeAa4EviBHqAFYM09oHuLtqrGAAAAAA8M0toABoEGQAAADYAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAc0RsAAAAAZlnf2gAAAAAAAAABAAAAAwAAAAMAHNEaAAAAAAAAAAAQfdFrLDgzSIIugR73qs8U0ZiKbwBUclTTPh5thlbgnABZJUSd0V2hAAAAawAAAlEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAaBGEAAAAAZkspCwAAAAAAAAABABzRGwAAAAAAAAAAEH3Rayw4M0iCLoEe96rPFNGYim8AVHJU0z4ebYZW4JwAWSUtVVp1oQAAAGsAAAJRAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAwAAAAAAGgRhAAAAAGZLKQsAAAAAAAAAAAAc0RsAAAAAAAAAACrpocAdnax31o9tGyySuVcwtYjCqzhK/YGT5XC4hH73AAAAF0h26AAAHNEbAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + ledger: 1_888_539, + createdAt: 1_717_166_042 + } + ], + latest_ledger: 1_888_542, + latest_ledger_close_timestamp: 1_717_166_057, + oldest_ledger: 1_871_263, + oldest_ledger_close_timestamp: 1_717_075_350, + cursor: "8111217537191937" + }} + end +end + +defmodule Soroban.RPC.GetTransactionsTest do + use ExUnit.Case + + alias Soroban.RPC.{ + GetTransactions, + GetTransactionsCannedClientImpl, + GetTransactionsResponse, + Server, + TransactionsPayload + } + + setup do + Application.put_env(:soroban, :http_client_impl, GetTransactionsCannedClientImpl) + + on_exit(fn -> + Application.delete_env(:soroban, :http_client_impl) + end) + + start_ledger = 1000 + limit = 2 + + transaction_payload = TransactionsPayload.new(start_ledger: start_ledger, limit: limit) + + %{payload: transaction_payload, server: Server.testnet()} + end + + test "request/2", %{payload: payload, server: server} do + {:ok, + %GetTransactionsResponse{ + transactions: _transactions, + latest_ledger: 1_888_542, + latest_ledger_close_timestamp: 1_717_166_057, + oldest_ledger: 1_871_263, + oldest_ledger_close_timestamp: 1_717_075_350, + cursor: "8111217537191937" + }} = GetTransactions.request(server, payload) + end +end diff --git a/test/rpc/endpoints/transactions_payload/transactions_payload_test.exs b/test/rpc/endpoints/transactions_payload/transactions_payload_test.exs new file mode 100644 index 0000000..1c1c6e2 --- /dev/null +++ b/test/rpc/endpoints/transactions_payload/transactions_payload_test.exs @@ -0,0 +1,112 @@ +defmodule Soroban.RPC.TransactionsPayloadTest do + use ExUnit.Case + alias Soroban.RPC.TransactionsPayload + + setup do + start_ledger = 674_736 + cursor = "1234-1" + limit = 2 + + payload = + TransactionsPayload.new( + start_ledger: start_ledger, + cursor: cursor, + limit: limit + ) + + %{ + start_ledger: start_ledger, + cursor: cursor, + limit: limit, + payload: payload + } + end + + describe "new/1" do + test "with valid values", %{ + start_ledger: start_ledger, + cursor: cursor, + limit: limit + } do + %TransactionsPayload{ + start_ledger: ^start_ledger, + pagination: %{cursor: ^cursor, limit: ^limit} + } = + TransactionsPayload.new( + start_ledger: start_ledger, + cursor: cursor, + limit: limit + ) + end + + test "with invalid struct" do + assert TransactionsPayload.new(nil) == {:error, :invalid_args} + end + + test "with nil start_ledger", %{ + cursor: cursor, + limit: limit + } do + %TransactionsPayload{ + start_ledger: nil, + pagination: %{cursor: ^cursor, limit: ^limit} + } = + TransactionsPayload.new( + start_ledger: nil, + cursor: cursor, + limit: limit + ) + end + + test "with nil cursor", %{ + start_ledger: start_ledger, + limit: limit + } do + %TransactionsPayload{ + start_ledger: ^start_ledger, + pagination: %{cursor: nil, limit: ^limit} + } = + TransactionsPayload.new( + start_ledger: start_ledger, + cursor: nil, + limit: limit + ) + end + + test "with nil limit", %{ + start_ledger: start_ledger, + cursor: cursor + } do + %TransactionsPayload{ + start_ledger: ^start_ledger, + pagination: %{cursor: ^cursor, limit: nil} + } = + TransactionsPayload.new( + start_ledger: start_ledger, + cursor: cursor, + limit: nil + ) + end + end + + describe "to_request_args/1" do + test "with valid struct", %{ + start_ledger: start_ledger, + cursor: cursor, + limit: limit, + payload: payload + } do + expected = %{ + startLedger: start_ledger, + filters: nil, + pagination: %{cursor: cursor, limit: limit} + } + + assert TransactionsPayload.to_request_args(payload) == expected + end + + test "with an invalid struct" do + assert TransactionsPayload.to_request_args(nil) == :error + end + end +end diff --git a/test/rpc/responses/get_transactions_response_test.exs b/test/rpc/responses/get_transactions_response_test.exs new file mode 100644 index 0000000..21f957e --- /dev/null +++ b/test/rpc/responses/get_transactions_response_test.exs @@ -0,0 +1,66 @@ +defmodule Soroban.RPC.GetTransactionsResponseTest do + use ExUnit.Case + + alias Soroban.RPC.GetTransactionsResponse + + setup do + result = %{ + transactions: [ + %{ + status: "FAILED", + applicationOrder: 1, + feeBump: false, + envelopeXdr: + "AAAAAgAAAACDz21Q3CTITlGqRus3/96/05EDivbtfJncNQKt64BTbAAAASwAAKkyAAXlMwAAAAEAAAAAAAAAAAAAAABmWeASAAAAAQAAABR3YWxsZXQ6MTcxMjkwNjMzNjUxMAAAAAEAAAABAAAAAIPPbVDcJMhOUapG6zf/3r/TkQOK9u18mdw1Aq3rgFNsAAAAAQAAAABwOSvou8mtwTtCkysVioO35TSgyRir2+WGqO8FShG/GAAAAAFVQUgAAAAAAO371tlrHUfK+AvmQvHje1jSUrvJb3y3wrJ7EplQeqTkAAAAAAX14QAAAAAAAAAAAeuAU2wAAABAn+6A+xXvMasptAm9BEJwf5Y9CLLQtV44TsNqS8ocPmn4n8Rtyb09SBiFoMv8isYgeQU5nAHsIwBNbEKCerusAQ==", + resultXdr: "AAAAAAAAAGT/////AAAAAQAAAAAAAAAB////+gAAAAA=", + resultMetaXdr: + "AAAAAwAAAAAAAAACAAAAAwAc0RsAAAAAAAAAAIPPbVDcJMhOUapG6zf/3r/TkQOK9u18mdw1Aq3rgFNsAAAAF0YpYBQAAKkyAAXlMgAAAAsAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAABzRGgAAAABmWd/VAAAAAAAAAAEAHNEbAAAAAAAAAACDz21Q3CTITlGqRus3/96/05EDivbtfJncNQKt64BTbAAAABdGKWAUAACpMgAF5TMAAAALAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAc0RsAAAAAZlnf2gAAAAAAAAAAAAAAAAAAAAA=", + ledger: 1_888_539, + createdAt: 1_717_166_042 + }, + %{ + status: "SUCCESS", + applicationOrder: 2, + feeBump: false, + envelopeXdr: + "AAAAAgAAAAC4EZup+ewCs/doS3hKbeAa4EviBHqAFYM09oHuLtqrGAAPQkAAGgQZAAAANgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAABB90WssODNIgi6BHveqzxTRmIpvAFRyVNM+Hm2GVuCcAAAAAAAAAAAq6aHAHZ2sd9aPbRsskrlXMLWIwqs4Sv2Bk+VwuIR+9wAAABdIdugAAAAAAAAAAAIu2qsYAAAAQERzKOqYYiPXNwsiL8ADAG/f45RBssmf3umGzw4qKkLGlObuPdX0buWmTGrhI13SG38F2V8Mp9DI+eDkcCjMSAOGVuCcAAAAQHnm0o/r+Gsl+6oqBgSbqoSY37gflvQB3zZRghuir0N75UVerd0Q50yG5Zfu08i2crhx6uk+5HYTl8/Sa7uZ+Qc=", + resultXdr: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=", + resultMetaXdr: + "AAAAAwAAAAAAAAACAAAAAwAc0RsAAAAAAAAAALgRm6n57AKz92hLeEpt4BrgS+IEeoAVgzT2ge4u2qsYAAAAADwzS2gAGgQZAAAANQAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAABzPVAAAAABmWdZ2AAAAAAAAAAEAHNEbAAAAAAAAAAC4EZup+ewCs/doS3hKbeAa4EviBHqAFYM09oHuLtqrGAAAAAA8M0toABoEGQAAADYAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAc0RsAAAAAZlnf2gAAAAAAAAABAAAAAwAAAAMAHNEaAAAAAAAAAAAQfdFrLDgzSIIugR73qs8U0ZiKbwBUclTTPh5thlbgnABZJUSd0V2hAAAAawAAAlEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAaBGEAAAAAZkspCwAAAAAAAAABABzRGwAAAAAAAAAAEH3Rayw4M0iCLoEe96rPFNGYim8AVHJU0z4ebYZW4JwAWSUtVVp1oQAAAGsAAAJRAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAwAAAAAAGgRhAAAAAGZLKQsAAAAAAAAAAAAc0RsAAAAAAAAAACrpocAdnax31o9tGyySuVcwtYjCqzhK/YGT5XC4hH73AAAAF0h26AAAHNEbAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + ledger: 1_888_539, + createdAt: 1_717_166_042 + } + ], + latest_ledger: 1_888_542, + latest_ledger_close_timestamp: 1_717_166_057, + oldest_ledger: 1_871_263, + oldest_ledger_close_timestamp: 1_717_075_350, + cursor: "8111217537191937" + } + + %{result: result} + end + + describe "new/1" do + test "result transaction", %{ + result: + %{ + transactions: transactions, + latest_ledger: latest_ledger, + latest_ledger_close_timestamp: latest_ledger_close_timestamp, + oldest_ledger: oldest_ledger, + oldest_ledger_close_timestamp: oldest_ledger_close_timestamp, + cursor: cursor + } = result + } do + assert %GetTransactionsResponse{ + transactions: ^transactions, + latest_ledger: ^latest_ledger, + latest_ledger_close_timestamp: ^latest_ledger_close_timestamp, + oldest_ledger: ^oldest_ledger, + oldest_ledger_close_timestamp: ^oldest_ledger_close_timestamp, + cursor: ^cursor + } = GetTransactionsResponse.new(result) + end + end +end diff --git a/test/rpc_test.exs b/test/rpc_test.exs index 213ba2a..2f8b5b4 100644 --- a/test/rpc_test.exs +++ b/test/rpc_test.exs @@ -245,6 +245,52 @@ defmodule Soroban.RPC.CannedRPCGetVersionInfoClientImpl do end end +defmodule Soroban.RPC.CannedRPCGetTransactionsClientImpl do + @moduledoc false + + @behaviour Soroban.RPC.Client.Spec + + @impl true + def request(_endpoint, _url, _headers, _body, _opts) do + send(self(), {:request, "RESPONSE"}) + + {:ok, + %{ + transactions: [ + %{ + status: "FAILED", + applicationOrder: 1, + feeBump: false, + envelopeXdr: + "AAAAAgAAAACDz21Q3CTITlGqRus3/96/05EDivbtfJncNQKt64BTbAAAASwAAKkyAAXlMwAAAAEAAAAAAAAAAAAAAABmWeASAAAAAQAAABR3YWxsZXQ6MTcxMjkwNjMzNjUxMAAAAAEAAAABAAAAAIPPbVDcJMhOUapG6zf/3r/TkQOK9u18mdw1Aq3rgFNsAAAAAQAAAABwOSvou8mtwTtCkysVioO35TSgyRir2+WGqO8FShG/GAAAAAFVQUgAAAAAAO371tlrHUfK+AvmQvHje1jSUrvJb3y3wrJ7EplQeqTkAAAAAAX14QAAAAAAAAAAAeuAU2wAAABAn+6A+xXvMasptAm9BEJwf5Y9CLLQtV44TsNqS8ocPmn4n8Rtyb09SBiFoMv8isYgeQU5nAHsIwBNbEKCerusAQ==", + resultXdr: "AAAAAAAAAGT/////AAAAAQAAAAAAAAAB////+gAAAAA=", + resultMetaXdr: + "AAAAAwAAAAAAAAACAAAAAwAc0RsAAAAAAAAAAIPPbVDcJMhOUapG6zf/3r/TkQOK9u18mdw1Aq3rgFNsAAAAF0YpYBQAAKkyAAXlMgAAAAsAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAABzRGgAAAABmWd/VAAAAAAAAAAEAHNEbAAAAAAAAAACDz21Q3CTITlGqRus3/96/05EDivbtfJncNQKt64BTbAAAABdGKWAUAACpMgAF5TMAAAALAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAc0RsAAAAAZlnf2gAAAAAAAAAAAAAAAAAAAAA=", + ledger: 1_888_539, + createdAt: 1_717_166_042 + }, + %{ + status: "SUCCESS", + applicationOrder: 2, + feeBump: false, + envelopeXdr: + "AAAAAgAAAAC4EZup+ewCs/doS3hKbeAa4EviBHqAFYM09oHuLtqrGAAPQkAAGgQZAAAANgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAABB90WssODNIgi6BHveqzxTRmIpvAFRyVNM+Hm2GVuCcAAAAAAAAAAAq6aHAHZ2sd9aPbRsskrlXMLWIwqs4Sv2Bk+VwuIR+9wAAABdIdugAAAAAAAAAAAIu2qsYAAAAQERzKOqYYiPXNwsiL8ADAG/f45RBssmf3umGzw4qKkLGlObuPdX0buWmTGrhI13SG38F2V8Mp9DI+eDkcCjMSAOGVuCcAAAAQHnm0o/r+Gsl+6oqBgSbqoSY37gflvQB3zZRghuir0N75UVerd0Q50yG5Zfu08i2crhx6uk+5HYTl8/Sa7uZ+Qc=", + resultXdr: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=", + resultMetaXdr: + "AAAAAwAAAAAAAAACAAAAAwAc0RsAAAAAAAAAALgRm6n57AKz92hLeEpt4BrgS+IEeoAVgzT2ge4u2qsYAAAAADwzS2gAGgQZAAAANQAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAABzPVAAAAABmWdZ2AAAAAAAAAAEAHNEbAAAAAAAAAAC4EZup+ewCs/doS3hKbeAa4EviBHqAFYM09oHuLtqrGAAAAAA8M0toABoEGQAAADYAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAc0RsAAAAAZlnf2gAAAAAAAAABAAAAAwAAAAMAHNEaAAAAAAAAAAAQfdFrLDgzSIIugR73qs8U0ZiKbwBUclTTPh5thlbgnABZJUSd0V2hAAAAawAAAlEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAaBGEAAAAAZkspCwAAAAAAAAABABzRGwAAAAAAAAAAEH3Rayw4M0iCLoEe96rPFNGYim8AVHJU0z4ebYZW4JwAWSUtVVp1oQAAAGsAAAJRAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAwAAAAAAGgRhAAAAAGZLKQsAAAAAAAAAAAAc0RsAAAAAAAAAACrpocAdnax31o9tGyySuVcwtYjCqzhK/YGT5XC4hH73AAAAF0h26AAAHNEbAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + ledger: 1_888_539, + createdAt: 1_717_166_042 + } + ], + latest_ledger: 1_888_542, + latest_ledger_close_timestamp: 1_717_166_057, + oldest_ledger: 1_871_263, + oldest_ledger_close_timestamp: 1_717_075_350, + cursor: "8111217537191937" + }} + end +end + defmodule Soroban.RPCTest do use ExUnit.Case @@ -258,6 +304,7 @@ defmodule Soroban.RPCTest do CannedRPCGetLedgerEntriesForAccountClientImpl, CannedRPCGetNetworkClientImpl, CannedRPCGetTransactionClientImpl, + CannedRPCGetTransactionsClientImpl, CannedRPCGetVersionInfoClientImpl, CannedRPCSendTransactionClientImpl, CannedRPCSimulateTransactionClientImpl, @@ -269,10 +316,12 @@ defmodule Soroban.RPCTest do GetLedgerEntriesResponse, GetNetworkResponse, GetTransactionResponse, + GetTransactionsResponse, SendTransactionResponse, Server, SimulateTransactionResponse, - TopicFilter + TopicFilter, + TransactionsPayload } alias Soroban.Types.Symbol @@ -408,6 +457,37 @@ defmodule Soroban.RPCTest do end end + describe "get_transactions/2" do + setup do + Application.put_env(:soroban, :http_client_impl, CannedRPCGetTransactionsClientImpl) + + on_exit(fn -> + Application.delete_env(:soroban, :http_client_impl) + end) + + start_ledger = 1000 + limit = 2 + cursor = "8111217537191937" + + transaction_payload = + TransactionsPayload.new(start_ledger: start_ledger, limit: limit, cursor: cursor) + + %{payload: transaction_payload} + end + + test "request/2", %{server: server, payload: payload} do + {:ok, + %GetTransactionsResponse{ + transactions: _transactions, + latest_ledger: 1_888_542, + latest_ledger_close_timestamp: 1_717_166_057, + oldest_ledger: 1_871_263, + oldest_ledger_close_timestamp: 1_717_075_350, + cursor: "8111217537191937" + }} = RPC.get_transactions(server, payload) + end + end + describe "get_health/1" do setup do Application.put_env(:soroban, :http_client_impl, CannedRPCGetHealthClientImpl)