diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 00000000..129efee1 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,35 @@ +name: StellarSDK CD + +on: + release: + types: + [published] + +jobs: + publish: + name: Publish Release to HEX PM + runs-on: ubuntu-latest + strategy: + matrix: + otp: ['23.3'] + elixir: ['1.11'] + env: + HEX_API_KEY: ${{ secrets.HEX_API_KEY }} + steps: + - uses: actions/checkout@v3 + - uses: erlef/setup-elixir@v1 + with: + otp-version: ${{ matrix.otp }} + elixir-version: ${{ matrix.elixir }} + - uses: actions/cache@v3 + with: + path: deps + key: ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} + restore-keys: | + ${{ runner.os }}-mix- + - name: Install Dependencies + run: | + rm -rf deps _build + mix deps.get + - name: Publish + run: HEX_API_KEY=$HEX_API_KEY mix hex.publish --yes diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8053d69f..5b88a0cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,12 +18,12 @@ jobs: env: MIX_ENV: test steps: - - uses: actions/checkout@v2 - - uses: erlef/setup-elixir@885971a72ed1f9240973bd92ab57af8c1aa68f24 + - uses: actions/checkout@v3 + - uses: erlef/setup-elixir@v1 with: otp-version: ${{ matrix.otp }} elixir-version: ${{ matrix.elixir }} - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: deps key: ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} diff --git a/CHANGELOG.md b/CHANGELOG.md index ba2fabdb..7e6f11eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,13 @@ # Changelog -## 0.7.1 (25.07.2022) +## 0.8.0 (27.07.2022) +* Add Fee Stats, Paths, and Order Books endpoints. +* Add `Keypair.valid_signature?/3` function. +* Fix documentation examples to create accounts and payments. +* Automate publishing of new releases to Hex.pm using CD. -- Add security policy to the repository +## 0.7.1 (25.07.2022) +* Add security policy to the repository ## 0.7.0 (27.05.2022) * Add CreateClaimableBalance, ClaimClaimableBalance, RevokeSponsorship operations. diff --git a/README.md b/README.md index 09273949..2f663cdc 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The **Stellar SDK** is composed of two complementary components: **`TxBuild`** + ```elixir def deps do [ - {:stellar_sdk, "~> 0.7.1"} + {:stellar_sdk, "~> 0.8.0"} ] end ``` @@ -655,6 +655,76 @@ Stellar.Horizon.Effects.all(limit: 10, order: :asc) See [**Stellar.Horizon.Effects**](https://hexdocs.pm/stellar_sdk/Stellar.Horizon.Effects.html#content) for more details. +### FeeStats +```elixir +# retrieve fee stats +Stellar.Horizon.FeeStats.retrieve() +``` + +See [**Stellar.Horizon.FeeStats**](https://developers.stellar.org/api/aggregations/fee-stats/single/) for more details. + +### Paths + +#### List paths +This will return information about potential path payments: +- [Required] source_account. The Stellar address of the sender. +- [Required] destination_asset_type. The type for the destination asset, **native**, **credit_alphanum4**, or **credit_alphanum12**. +- [Required] destination_amount. The destination amount specified in the search that found this path. +- [Optional] destination_account. The Stellar address of the reciever. +- [Optional] destination_asset_issuer. The Stellar address of the issuer of the destination asset. Required if the destination_asset_type is not **native**. +- [Optional] destination_asset_code. The code for the destination asset. + +```elixir +Stellar.Horizon.PaymentPaths.list_paths(source_account: "GBRSLTT74SKP62KJ7ENTMP5V4R7UGB6E5UQESNIIRWUNRCCUO4ZMFM4C", destination_asset_type: :native, destination_amount: 5) +``` + +#### List strict receive payment paths +- [Required] destination_asset_type. The type for the destination asset, **native**, **credit_alphanum4**, or **credit_alphanum12**. +- [Required] destination_amount. The amount of the destination asset that should be received. +- [Optional] source_account. The Stellar address of the sender. +- [Optional] source_assets. A comma-separated list of assets available to the sender. +- [Optional] destination_asset_issuer. The Stellar address of the issuer of the destination asset. Required if the **destination_asset_type** is not **native** +- [Optional] destination_asset_code. The code for the destination asset. Required if the **destination_asset_type** is not **native**. + +```elixir +Stellar.Horizon.PaymentPaths.list_receive_paths(destination_asset_type: :native, destination_amount: 5, source_account: "GBTKSXOTFMC5HR25SNL76MOVQW7GA3F6CQEY622ASLUV4VMLITI6TCOO") +``` + +#### List strict send payment paths +- [Required] source_asset_type. The type for the source asset, **native**, **credit_alphanum4**, or **credit_alphanum12**. +- [Required] source_amount. The amount of the source asset that should be sent. +- [Optional] source_asset_issuer. The Stellar address of the issuer of the source asset. Required if the **source_asset_type** is not native. +- [Optional] source_asset_code. The code for the source asset. Required if the **source_asset_type** is not **native**. +- [Optional] destination_account. The Stellar address of the reciever. +- [Optional] destination_assets. A comma-separated list of assets that the recipient can receive. + +```elixir +Stellar.Horizon.PaymentPaths.list_send_paths(source_asset_type: :native, source_amount: 5, destination_assets: "TEST:GA654JC6QLA3ZH4O5V7X5NPM7KEWHKRG5GJA4PETK4SOFBUJLCCN74KQ") +``` + +See [**Stellar.Horizon.Paths**](https://developers.stellar.org/api/aggregations/paths/) for more details. + +### Order Books + +#### Retrieve order Books +Provides an order book’s bids and asks: +- [Required] selling_asset. **:native** or **[code: "SELLING_ASSET_CODE", issuer: "SELLING_ASSET_ISSUER" ]**. +- [Required] buying_asset. **:native** or **[code: "BUYING_ASSET_CODE", issuer: "BUYING_ASSET_ISSUER" ]**. +- [Optional] limit. The maximum number of records returned + +```elixir +Stellar.Horizon.OrderBooks.retrieve(selling_asset: :native, buying_asset: :native) +Stellar.Horizon.OrderBooks.retrieve(selling_asset: :native, + buying_asset: [ + code: "BB1", + issuer: "GD5J6HLF5666X4AZLTFTXLY46J5SW7EXRKBLEYPJP33S33MXZGV6CWFN" + ], + limit: 2 + ) +``` + +See [**Stellar.Horizon.OrderBooks**](https://developers.stellar.org/api/aggregations/order-books/) for more details. + --- ## Development diff --git a/docs/examples/create_account.md b/docs/examples/create_account.md index 9e145eb0..6acbc3ac 100644 --- a/docs/examples/create_account.md +++ b/docs/examples/create_account.md @@ -19,11 +19,12 @@ signer_key_pair = Stellar.KeyPair.from_secret_seed("SECRET_SEED") signature = Stellar.TxBuild.Signature.new(signer_key_pair) # 5. submit the transaction to Horizon -{:ok, tx} = +{:ok, base64_envelope} = source_account |> Stellar.TxBuild.new(sequence_number: sequence_number) |> Stellar.TxBuild.add_operation(operation) |> Stellar.TxBuild.sign(signature) |> Stellar.TxBuild.envelope() - |> Stellar.Horizon.Transactions.create() + +{:ok, submitted_tx} = Stellar.Horizon.Transactions.create(base64_envelope) ``` diff --git a/docs/examples/payments.md b/docs/examples/payments.md index 5c54b33c..44f04a28 100644 --- a/docs/examples/payments.md +++ b/docs/examples/payments.md @@ -22,13 +22,14 @@ signer_key_pair = Stellar.KeyPair.from_secret_seed("SECRET_SEED") signature = Stellar.TxBuild.Signature.new(signer_key_pair) # 5. submit the transaction to Horizon -{:ok, tx} = +{:ok, base64_envelope} = source_account |> Stellar.TxBuild.new(sequence_number: sequence_number) |> Stellar.TxBuild.add_operation(operation) |> Stellar.TxBuild.sign(signature) |> Stellar.TxBuild.envelope() - |> Stellar.Horizon.Transactions.create() + +{:ok, submitted_tx} = Stellar.Horizon.Transactions.create(base64_envelope) ``` ## Alphanum asset payment @@ -53,11 +54,12 @@ signer_key_pair = Stellar.KeyPair.from_secret_seed("SECRET_SEED") signature = Stellar.TxBuild.Signature.new(signer_key_pair) # 5. submit the transaction to Horizon -{:ok, tx} = +{:ok, base64_envelope} = source_account |> Stellar.TxBuild.new(sequence_number: sequence_number) |> Stellar.TxBuild.add_operation(operation) |> Stellar.TxBuild.sign(signature) |> Stellar.TxBuild.envelope() - |> Stellar.Horizon.Transactions.create() -``` \ No newline at end of file + +{:ok, submitted_tx} = Stellar.Horizon.Transactions.create(base64_envelope) +``` diff --git a/lib/horizon/fee_stat.ex b/lib/horizon/fee_stat.ex new file mode 100644 index 00000000..3e6cd291 --- /dev/null +++ b/lib/horizon/fee_stat.ex @@ -0,0 +1,40 @@ +defmodule Stellar.Horizon.FeeStat do + @moduledoc """ + Represents a `FeeStat` resource from Horizon API. + """ + + @behaviour Stellar.Horizon.Resource + + alias Stellar.Horizon.Mapping + + @type t :: %__MODULE__{ + last_ledger: non_neg_integer(), + last_ledger_base_fee: non_neg_integer(), + ledger_capacity_usage: float(), + fee_charged: map(), + max_fee: map() + } + + defstruct [ + :last_ledger, + :last_ledger_base_fee, + :ledger_capacity_usage, + :fee_charged, + :max_fee + ] + + @mapping [ + last_ledger: :integer, + last_ledger_base_fee: :integer, + ledger_capacity_usage: :float + ] + + @impl true + def new(attrs, opts \\ []) + + def new(attrs, _opts) do + %__MODULE__{} + |> Mapping.build(attrs) + |> Mapping.parse(@mapping) + end +end diff --git a/lib/horizon/fee_stats.ex b/lib/horizon/fee_stats.ex new file mode 100644 index 00000000..80bebdf0 --- /dev/null +++ b/lib/horizon/fee_stats.ex @@ -0,0 +1,34 @@ +defmodule Stellar.Horizon.FeeStats do + @moduledoc """ + Exposes functions to interact with FeeStats in Horizon. + + You can: + * Retrieve the fee stats. + + Horizon API reference: https://developers.stellar.org/api/aggregations/fee-stats/ + """ + + alias Stellar.Horizon.{Error, FeeStat, Request} + + @type resource :: FeeStat.t() + @type response :: {:ok, resource()} | {:error, Error.t()} + + @endpoint "fee_stats" + + @doc """ + Retrieves information of the fee stats. + + ## Examples + + iex> FeeStats.retrieve() + {:ok, %FeeStat{}} + """ + + @spec retrieve :: response() + def retrieve do + :get + |> Request.new(@endpoint) + |> Request.perform() + |> Request.results(as: FeeStat) + end +end diff --git a/lib/horizon/order_book.ex b/lib/horizon/order_book.ex new file mode 100644 index 00000000..d9d584cb --- /dev/null +++ b/lib/horizon/order_book.ex @@ -0,0 +1,38 @@ +defmodule Stellar.Horizon.OrderBook do + @moduledoc """ + Represents an `OrderBook` resource from Horizon API. + """ + + @behaviour Stellar.Horizon.Resource + + alias Stellar.Horizon.Mapping + alias Stellar.Horizon.OrderBook.Price + + @type t :: %__MODULE__{ + bids: list(Price.t()), + asks: list(Price.t()), + base: map(), + counter: map() + } + + defstruct [ + :bids, + :asks, + :base, + :counter + ] + + @mapping [ + bids: {:list, :struct, Price}, + asks: {:list, :struct, Price} + ] + + @impl true + def new(attrs, opts \\ []) + + def new(attrs, _opts) do + %__MODULE__{} + |> Mapping.build(attrs) + |> Mapping.parse(@mapping) + end +end diff --git a/lib/horizon/order_book/price.ex b/lib/horizon/order_book/price.ex new file mode 100644 index 00000000..1cfc3f97 --- /dev/null +++ b/lib/horizon/order_book/price.ex @@ -0,0 +1,35 @@ +defmodule Stellar.Horizon.OrderBook.Price do + @moduledoc """ + Represents a `Price` for an order book. + """ + + @behaviour Stellar.Horizon.Resource + + alias Stellar.Horizon.Mapping + + @type t :: %__MODULE__{ + price_r: map(), + price: float(), + amount: float() + } + + defstruct [ + :price_r, + :price, + :amount + ] + + @mapping [ + price: :float, + amount: :float + ] + + @impl true + def new(attrs, opts \\ []) + + def new(attrs, _opts) do + %__MODULE__{} + |> Mapping.build(attrs) + |> Mapping.parse(@mapping) + end +end diff --git a/lib/horizon/order_books.ex b/lib/horizon/order_books.ex new file mode 100644 index 00000000..007143a8 --- /dev/null +++ b/lib/horizon/order_books.ex @@ -0,0 +1,75 @@ +defmodule Stellar.Horizon.OrderBooks do + @moduledoc """ + Exposes functions to interact with OrderBooks in Horizon. + + You can: + * Retrieve an order book’s bids and asks + + Horizon API reference: https://developers.stellar.org/api/aggregations/order-books/object/ + """ + + alias Stellar.Horizon.{Error, OrderBook, Request, RequestParams} + + @type args :: Keyword.t() + @type resource :: OrderBook.t() + @type response :: {:ok, resource()} | {:error, Error.t()} + + @endpoint "order_book" + + @doc """ + Retrieve order books + + ## Parameters + * `selling_asset`: :native or [code: `selling_asset_code`, issuer: `selling_asset_issuer` ] + * `buying_asset`: :native or [code: `buying_asset_code`, issuer: `buying_asset_issuer` ] + + ## Options + * `limit`: The maximum number of records returned + + ## Examples + + # Retrieve order books + iex> OrderBooks.retrieve(selling_asset: :native, buying_asset: :native) + {:ok, %OrderBook{bids: [%Price{}...], asks: [%Price{}...], ...} + + # Retrieve with more options + iex> OrderBooks.retrieve(selling_asset: :native, + buying_asset: [ + code: "BB1", + issuer: "GD5J6HLF5666X4AZLTFTXLY46J5SW7EXRKBLEYPJP33S33MXZGV6CWFN" + ], + limit: 2 + ) + {:ok, %OrderBook{bids: [%Price{}...], asks: [%Price{}...], ...} + """ + + @spec retrieve(args :: args()) :: response() + def retrieve(args \\ []) do + selling_asset = RequestParams.build_assets_params(args, :selling_asset) + buying_asset = RequestParams.build_assets_params(args, :buying_asset) + + params = + args + |> Keyword.take([:limit]) + |> Keyword.merge(selling_asset) + |> Keyword.merge(buying_asset) + + :get + |> Request.new(@endpoint) + |> Request.add_query(params, extra_params: allowed_query_options()) + |> Request.perform() + |> Request.results(as: OrderBook) + end + + @spec allowed_query_options() :: list() + defp allowed_query_options do + [ + :selling_asset_type, + :buying_asset_type, + :selling_asset_issuer, + :selling_asset_code, + :buying_asset_issuer, + :buying_asset_code + ] + end +end diff --git a/lib/horizon/path.ex b/lib/horizon/path.ex new file mode 100644 index 00000000..e59b7240 --- /dev/null +++ b/lib/horizon/path.ex @@ -0,0 +1,47 @@ +defmodule Stellar.Horizon.Path do + @moduledoc """ + Represents a `Path` resource from Horizon API. + """ + + @behaviour Stellar.Horizon.Resource + + alias Stellar.Horizon.Mapping + + @type t :: %__MODULE__{ + source_asset_type: String.t(), + source_asset_code: String.t(), + source_asset_issuer: String.t(), + source_amount: float(), + destination_asset_type: String.t(), + destination_asset_code: String.t(), + destination_asset_issuer: String.t(), + destination_amount: float(), + path: list(map()) + } + + defstruct [ + :source_asset_type, + :source_asset_code, + :source_asset_issuer, + :source_amount, + :destination_asset_type, + :destination_asset_code, + :destination_asset_issuer, + :destination_amount, + :path + ] + + @mapping [ + source_amount: :float, + destination_amount: :float + ] + + @impl true + def new(attrs, opts \\ []) + + def new(attrs, _opts) do + %__MODULE__{} + |> Mapping.build(attrs) + |> Mapping.parse(@mapping) + end +end diff --git a/lib/horizon/paths.ex b/lib/horizon/paths.ex new file mode 100644 index 00000000..141af261 --- /dev/null +++ b/lib/horizon/paths.ex @@ -0,0 +1,25 @@ +defmodule Stellar.Horizon.Paths do + @moduledoc """ + Represents a `Payments Paths` struct. + """ + + @behaviour Stellar.Horizon.Resource + + alias Stellar.Horizon.Mapping + alias Stellar.Horizon.Path + + @type t :: %__MODULE__{records: list(Path.t())} + + defstruct [:records] + + @mapping [records: {:list, :struct, Path}] + + @impl true + def new(attrs, opts \\ []) + + def new(attrs, _opts) do + %__MODULE__{} + |> Mapping.build(attrs) + |> Mapping.parse(@mapping) + end +end diff --git a/lib/horizon/payment_paths.ex b/lib/horizon/payment_paths.ex new file mode 100644 index 00000000..bfd9bc2e --- /dev/null +++ b/lib/horizon/payment_paths.ex @@ -0,0 +1,205 @@ +defmodule Stellar.Horizon.PaymentPaths do + @moduledoc """ + Exposes functions to interact with Paths in Horizon. + + You can: + * Lists the paths a payment can take based on the amount of an asset you want the recipient to receive. + * Lists the paths a payment can take based on the amount of an asset you want to send. + + Horizon API reference: https://developers.stellar.org/api/aggregations/paths/ + """ + + alias Stellar.Horizon.{Error, Paths, Request} + + @type args :: Keyword.t() + @type opt :: atom() + @type path :: String.t() + @type resource :: Paths.t() + @type response :: {:ok, resource()} | {:error, Error.t()} + + @endpoint "paths" + + @doc """ + List Payment Paths + + ## Parameters + * `source_account`: The Stellar address of the sender. + * `destination_asset_type`: The type for the destination asset. + * `destination_amount`: The amount of the destination asset that should be received. + + ## Options + * `destination_account`: The Stellar address of the reciever. + * `destination_asset_issuer`: The Stellar address of the issuer of the destination asset. Required if the `destination_asset_type` is not native + * `destination_asset_code`: The code for the destination asset. Required if the `destination_asset_type` is not native. + + ## Examples + + iex> PaymentPaths.list_paths(source_account: "GBRSLTT74SKP62KJ7ENTMP5V4R7UGB6E5UQESNIIRWUNRCCUO4ZMFM4C", + destination_asset_type: :native, + destination_amount: 5 + ) + {:ok, %Paths{records: [%Path{}, ...]}} + + # list with `destination_account` + iex> PaymentPaths.list_paths(source_account: "GBRSLTT74SKP62KJ7ENTMP5V4R7UGB6E5UQESNIIRWUNRCCUO4ZMFM4C", + destination_asset_type: :native, + destination_amount: 5, + destination_account: "GBRSLTT74SKP62KJ7ENTMP5V4R7UGB6E5UQESNIIRWUNRCCUO4ZMFM4C" + ) + {:ok, %Paths{records: [%Path{}, ...]}} + + # list with more options + iex> PaymentPaths.list_paths(source_account: "GBRSLTT74SKP62KJ7ENTMP5V4R7UGB6E5UQESNIIRWUNRCCUO4ZMFM4C", + destination_asset_type: "credit_alphanum4", + destination_amount: 5, + destination_account: "GBRSLTT74SKP62KJ7ENTMP5V4R7UGB6E5UQESNIIRWUNRCCUO4ZMFM4C", + destination_asset_code: "TEST", + destination_asset_issuer: "GA654JC6QLA3ZH4O5V7X5NPM7KEWHKRG5GJA4PETK4SOFBUJLCCN74KQ" + ) + {:ok, %Paths{records: [%Path{}, ...]}} + """ + + @spec list_paths(args :: args()) :: response() + def list_paths(args \\ []) do + :get + |> Request.new(@endpoint) + |> Request.add_query(args, extra_params: allowed_query_options(:list_paths)) + |> Request.perform() + |> Request.results(as: Paths) + end + + @doc """ + List Strict Receive Payment Paths + + ## Parameters + + Using either `source_account` or `source_assets` as an argument is required for strict receive path payments. + * `destination_asset_type`: The type for the destination asset. + * `destination_amount`: The amount of the destination asset that should be received. + + ## Options + + * `source_account`: The Stellar address of the sender. + * `source_assets`: A comma-separated list of assets available to the sender. + * `destination_asset_issuer`: The Stellar address of the issuer of the destination asset. Required if the `destination_asset_type` is not native + * `destination_asset_code`: The code for the destination asset. Required if the `destination_asset_type` is not native. + + ## Examples + + # list with `source_account` + iex> PaymentPaths.list_receive_paths(destination_asset_type: :native, + destination_amount: 5, + source_account: "GBTKSXOTFMC5HR25SNL76MOVQW7GA3F6CQEY622ASLUV4VMLITI6TCOO" + ) + {:ok, %Paths{records: [%Path{}, ...]}} + + + # list with `source_assets` + iex> PaymentPaths.list_receive_paths(destination_asset_type: :native, + destination_amount: 5, + source_assets: :native + ) + {:ok, %Paths{records: [%Path{}, ...]}} + + # list with more options + iex> PaymentPaths.list_receive_paths(destination_asset_type: :credit_alphanum4, + destination_amount: 5, + destination_asset_code: "BB1", + destination_asset_issuer: "GD5J6HLF5666X4AZLTFTXLY46J5SW7EXRKBLEYPJP33S33MXZGV6CWFN", + source_assets: "CNY:GAREELUB43IRHWEASCFBLKHURCGMHE5IF6XSE7EXDLACYHGRHM43RFOX" + ) + {:ok, %Paths{records: [%Path{}, ...]}} + """ + + @spec list_receive_paths(args :: args()) :: response() + def list_receive_paths(args \\ []) do + :get + |> Request.new(@endpoint, path: "strict-receive") + |> Request.add_query(args, extra_params: allowed_query_options(:list_receive)) + |> Request.perform() + |> Request.results(as: Paths) + end + + @doc """ + List Strict Send Payment Paths + + ## Parameters + Using either `destination_account` or `destination_assets` as an argument is required for strict send path payments. + * `source_asset_type`: The type for the source asset + * `source_amount`: The amount of the source asset that should be sent. + + ## Options + * `source_asset_issuer`: The Stellar address of the issuer of the source asset. Required if the `source_asset_type` is not native. + * `source_asset_code`: The code for the source asset. Required if the `source_asset_type` is not native. + * `destination_account`: The Stellar address of the reciever. + * `destination_assets`: A comma-separated list of assets that the recipient can receive. + + ## Examples + + * list with `destination_account` + iex> PaymentPaths.list_send_paths(source_asset_type: :native, + source_amount: 5, + destination_account: "GBTKSXOTFMC5HR25SNL76MOVQW7GA3F6CQEY622ASLUV4VMLITI6TCOO" + ) + {:ok, %Paths{records: [%Path{}, ...]}} + + * list with `destination_assets` + iex> PaymentPaths.list_send_paths(source_asset_type: :native, + source_amount: 5, + destination_assets: "TEST:GA654JC6QLA3ZH4O5V7X5NPM7KEWHKRG5GJA4PETK4SOFBUJLCCN74KQ" + ) + {:ok, %Paths{records: [%Path{}, ...]}} + + # list with more options + iex> PaymentPaths.list_send_paths(source_asset_type: :credit_alphanum4, + source_amount: 400, + destination_account: "GAYOLLLUIZE4DZMBB2ZBKGBUBZLIOYU6XFLW37GBP2VZD3ABNXCW4BVA", + source_asset_issuer: "GDVKY2GU2DRXWTBEYJJWSFXIGBZV6AZNBVVSUHEPZI54LIS6BA7DVVSP", + source_asset_code: "BRL" + ) + {:ok, %Paths{records: [%Path{}, ...]}} + """ + + @spec list_send_paths(args :: args()) :: response() + def list_send_paths(args \\ []) do + :get + |> Request.new(@endpoint, path: "strict-send") + |> Request.add_query(args, extra_params: allowed_query_options(:list_send)) + |> Request.perform() + |> Request.results(as: Paths) + end + + @spec allowed_query_options(opt :: opt()) :: list() + defp allowed_query_options(:list_paths) do + [ + :source_account, + :destination_asset_type, + :destination_amount, + :destination_account, + :destination_asset_issuer, + :destination_asset_code + ] + end + + defp allowed_query_options(:list_receive) do + [ + :destination_asset_type, + :destination_amount, + :source_account, + :source_assets, + :destination_asset_issuer, + :destination_asset_code + ] + end + + defp allowed_query_options(:list_send) do + [ + :source_asset_type, + :source_amount, + :source_asset_issuer, + :source_asset_code, + :destination_account, + :destination_assets + ] + end +end diff --git a/lib/horizon/request.ex b/lib/horizon/request.ex index 8e12346a..ac87d56a 100644 --- a/lib/horizon/request.ex +++ b/lib/horizon/request.ex @@ -108,6 +108,10 @@ defmodule Stellar.Horizon.Request do {:ok, Collection.new(results, {resource, paginate_fun})} end + def results({:ok, %{_embedded: embedded}}, as: resource) when not is_nil(embedded) do + {:ok, resource.new(embedded)} + end + def results({:ok, results}, as: resource), do: {:ok, resource.new(results)} def results({:error, error}, _resource), do: {:error, error} diff --git a/lib/horizon/request_params.ex b/lib/horizon/request_params.ex new file mode 100644 index 00000000..ece2614c --- /dev/null +++ b/lib/horizon/request_params.ex @@ -0,0 +1,33 @@ +defmodule Stellar.Horizon.RequestParams do + @moduledoc """ + Returns a Keyword list of formatted params + """ + + @type args :: Keyword.t() + @type type :: atom() + + @spec build_assets_params(args :: args(), type :: type()) :: Keyword.t() + def build_assets_params(args, type) when type in ~w(selling_asset buying_asset)a do + args + |> Keyword.get(type) + |> resolve_asset_params(type) + end + + def build_assets_params(_args, _type), do: [] + + @spec resolve_asset_params(args :: args(), type :: type()) :: Keyword.t() + defp resolve_asset_params([code: code, issuer: issuer], type) do + [ + "#{type}_type": resolve_asset_type(code), + "#{type}_code": code, + "#{type}_issuer": issuer + ] + end + + defp resolve_asset_params(:native, type), do: ["#{type}_type": :native] + defp resolve_asset_params(_args, _type), do: [] + + @spec resolve_asset_type(code :: String.t()) :: atom() + defp resolve_asset_type(code) when byte_size(code) < 5, do: :credit_alphanum4 + defp resolve_asset_type(_code), do: :credit_alphanum12 +end diff --git a/lib/keypair.ex b/lib/keypair.ex index d19d3aea..b1538fb6 100644 --- a/lib/keypair.ex +++ b/lib/keypair.ex @@ -32,6 +32,10 @@ defmodule Stellar.KeyPair do @impl true def sign(payload, secret), do: impl().sign(payload, secret) + @impl true + def valid_signature?(payload, signed_payload, public_key), + do: impl().valid_signature?(payload, signed_payload, public_key) + @impl true def validate_public_key(public_key), do: impl().validate_public_key(public_key) diff --git a/lib/keypair/default.ex b/lib/keypair/default.ex index 5efc061f..759f0f1e 100644 --- a/lib/keypair/default.ex +++ b/lib/keypair/default.ex @@ -59,6 +59,14 @@ defmodule Stellar.KeyPair.Default do def sign(_payload, _secret), do: {:error, :invalid_signature_payload} + @impl true + def valid_signature?(<>, <>, <>) do + raw_public_key = raw_public_key(public_key) + Ed25519.valid_signature?(signed_payload, payload, raw_public_key) + end + + def valid_signature?(_payload, _signed_payload, _public_key), do: false + @impl true def validate_public_key(public_key) do case StrKey.decode(public_key, :ed25519_public_key) do diff --git a/lib/keypair/spec.ex b/lib/keypair/spec.ex index 1d4251b9..e73c4015 100644 --- a/lib/keypair/spec.ex +++ b/lib/keypair/spec.ex @@ -22,6 +22,7 @@ defmodule Stellar.KeyPair.Spec do @callback validate_muxed_account(public_key()) :: validation() @callback validate_secret_seed(public_key()) :: validation() @callback sign(binary(), secret_seed()) :: binary() | error() + @callback valid_signature?(binary(), binary(), public_key()) :: boolean() @optional_callbacks from_raw_public_key: 1, from_raw_muxed_account: 1, diff --git a/mix.exs b/mix.exs index 1c70a1dc..3f91585b 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.7.1" + @version "0.8.0" def project do [ diff --git a/test/horizon/fee_stat_test.exs b/test/horizon/fee_stat_test.exs new file mode 100644 index 00000000..cda24508 --- /dev/null +++ b/test/horizon/fee_stat_test.exs @@ -0,0 +1,63 @@ +defmodule Stellar.Horizon.FeeStatTest do + use ExUnit.Case + + alias Stellar.Test.Fixtures.Horizon + alias Stellar.Horizon.FeeStat + + setup do + json_body = Horizon.fixture("fee_stats") + attrs = Jason.decode!(json_body, keys: :atoms) + + %{attrs: attrs} + end + + test "new/2", %{attrs: attrs} do + %FeeStat{ + fee_charged: %{ + max: "100", + min: "100", + mode: "100", + p10: "100", + p20: "100", + p30: "100", + p40: "100", + p50: "100", + p60: "100", + p70: "100", + p80: "100", + p90: "100", + p95: "100", + p99: "100" + }, + last_ledger: 155_545, + last_ledger_base_fee: 100, + ledger_capacity_usage: 0.01, + max_fee: %{ + max: "100", + min: "100", + mode: "100", + p10: "100", + p20: "100", + p30: "100", + p40: "100", + p50: "100", + p60: "100", + p70: "100", + p80: "100", + p90: "100", + p95: "100", + p99: "100" + } + } = FeeStat.new(attrs) + end + + test "new/2 empty_attrs" do + %FeeStat{ + fee_charged: nil, + last_ledger: nil, + last_ledger_base_fee: nil, + ledger_capacity_usage: nil, + max_fee: nil + } = FeeStat.new(%{}) + end +end diff --git a/test/horizon/fee_stats_test.exs b/test/horizon/fee_stats_test.exs new file mode 100644 index 00000000..3183aeda --- /dev/null +++ b/test/horizon/fee_stats_test.exs @@ -0,0 +1,74 @@ +defmodule Stellar.Horizon.Client.CannedFeeStatRequests do + alias Stellar.Test.Fixtures.Horizon + + @base_url "https://horizon-testnet.stellar.org" + + @spec request( + method :: atom(), + url :: String.t(), + headers :: list(), + body :: String.t(), + options :: list() + ) :: {:ok, non_neg_integer(), list(), String.t()} | {:error, atom()} + + def request(:get, @base_url <> "/fee_stats", _headers, _body, _opts) do + json_body = Horizon.fixture("fee_stats") + {:ok, 200, [], json_body} + end +end + +defmodule Stellar.Horizon.FeeStatsTest do + use ExUnit.Case + + alias Stellar.Horizon.Client.CannedFeeStatRequests + alias Stellar.Horizon.{FeeStat, FeeStats} + + setup do + Application.put_env(:stellar_sdk, :http_client, CannedFeeStatRequests) + + on_exit(fn -> + Application.delete_env(:stellar_sdk, :http_client) + end) + end + + test "retrieve/0" do + {:ok, + %FeeStat{ + fee_charged: %{ + max: "100", + min: "100", + mode: "100", + p10: "100", + p20: "100", + p30: "100", + p40: "100", + p50: "100", + p60: "100", + p70: "100", + p80: "100", + p90: "100", + p95: "100", + p99: "100" + }, + last_ledger: 155_545, + last_ledger_base_fee: 100, + ledger_capacity_usage: 0.01, + max_fee: %{ + max: "100", + min: "100", + mode: "100", + p10: "100", + p20: "100", + p30: "100", + p40: "100", + p50: "100", + p60: "100", + p70: "100", + p80: "100", + p90: "100", + p95: "100", + p99: "100" + } + }} = FeeStats.retrieve() + end +end diff --git a/test/horizon/order_book/price_test.exs b/test/horizon/order_book/price_test.exs new file mode 100644 index 00000000..af757b27 --- /dev/null +++ b/test/horizon/order_book/price_test.exs @@ -0,0 +1,27 @@ +defmodule Stellar.Horizon.OrderBook.PriceTest do + use ExUnit.Case + + alias Stellar.Horizon.OrderBook.Price + + setup do + %{ + attrs: %{ + price_r: %{n: 6_014_600, d: 102_275_119}, + price: "0.0588080", + amount: "0.1722469" + } + } + end + + test "new/2", %{attrs: attrs} do + %Price{ + price_r: %{n: 6_014_600, d: 102_275_119}, + price: 0.0588080, + amount: 0.1722469 + } = Price.new(attrs) + end + + test "new/2 empty_attrs" do + %Price{price_r: nil, price: nil, amount: nil} = Price.new(%{}) + end +end diff --git a/test/horizon/order_book_test.exs b/test/horizon/order_book_test.exs new file mode 100644 index 00000000..c39d0847 --- /dev/null +++ b/test/horizon/order_book_test.exs @@ -0,0 +1,49 @@ +defmodule Stellar.Horizon.OrderBookTest do + use ExUnit.Case + + alias Stellar.Test.Fixtures.Horizon + alias Stellar.Horizon.OrderBook + alias Stellar.Horizon.OrderBook.Price + + setup do + json_body = Horizon.fixture("order_book") + attrs = Jason.decode!(json_body, keys: :atoms) + + %{attrs: attrs} + end + + test "new/2", %{attrs: attrs} do + %OrderBook{ + asks: [ + %Price{ + amount: 8057.2710223, + price: 0.0590815, + price_r: %{d: 2_000_000, n: 118_163} + }, + %Price{ + amount: 1.0e4, + price: 0.060627, + price_r: %{d: 1_000_000, n: 60_627} + } + ], + base: %{asset_type: "native"}, + bids: [ + %Price{ + amount: 0.1722469, + price: 0.058808, + price_r: %{d: 102_275_119, n: 6_014_600} + }, + %Price{ + amount: 0.2991796, + price: 0.0572577, + price_r: %{d: 21_831_117, n: 1_250_000} + } + ], + counter: %{ + asset_code: "USD", + asset_issuer: "GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX", + asset_type: "credit_alphanum4" + } + } = OrderBook.new(attrs) + end +end diff --git a/test/horizon/order_books_test.exs b/test/horizon/order_books_test.exs new file mode 100644 index 00000000..d14241a6 --- /dev/null +++ b/test/horizon/order_books_test.exs @@ -0,0 +1,109 @@ +defmodule Stellar.Horizon.Client.CannedOrderBooksRequests do + alias Stellar.Test.Fixtures.Horizon + + @base_url "https://horizon-testnet.stellar.org" + + @spec request( + method :: atom(), + url :: String.t(), + headers :: list(), + body :: String.t(), + options :: list() + ) :: {:ok, non_neg_integer(), list(), String.t()} | {:error, atom} + + def request( + :get, + @base_url <> + "/order_book?limit=2&selling_asset_type=native&buying_asset_type=credit_alphanum4&buying_asset_code=USD&buying_asset_issuer=GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX", + _headers, + _body, + _opts + ) do + json_body = Horizon.fixture("order_book") + {:ok, 200, [], json_body} + end + + def request(:get, @base_url <> "/order_book", _headers, _body, _opts) do + json_error = Horizon.fixture("400_invalid_order_book") + {:ok, 400, [], json_error} + end +end + +defmodule Stellar.Horizon.OrderBooksTest do + use ExUnit.Case + + alias Stellar.Horizon.Client.CannedOrderBooksRequests + alias Stellar.Horizon.{OrderBooks, OrderBook, Error} + alias Stellar.Horizon.OrderBook.Price + + setup do + Application.put_env(:stellar_sdk, :http_client, CannedOrderBooksRequests) + + on_exit(fn -> + Application.delete_env(:stellar_sdk, :http_client) + end) + + %{ + selling_asset: :native, + buying_asset: [ + code: "USD", + issuer: "GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX" + ], + limit: 2 + } + end + + test "retrieve/1", %{ + selling_asset: selling_asset, + buying_asset: buying_asset, + limit: limit + } do + {:ok, + %OrderBook{ + asks: [ + %Price{ + amount: 8057.2710223, + price: 0.0590815, + price_r: %{d: 2_000_000, n: 118_163} + }, + %Price{ + amount: 1.0e4, + price: 0.060627, + price_r: %{d: 1_000_000, n: 60_627} + } + ], + base: %{asset_type: "native"}, + bids: [ + %Price{ + amount: 0.1722469, + price: 0.058808, + price_r: %{d: 102_275_119, n: 6_014_600} + }, + %Price{ + amount: 0.2991796, + price: 0.0572577, + price_r: %{d: 21_831_117, n: 1_250_000} + } + ], + counter: %{ + asset_code: "USD", + asset_issuer: "GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX", + asset_type: "credit_alphanum4" + } + }} = + OrderBooks.retrieve( + selling_asset: selling_asset, + buying_asset: buying_asset, + limit: limit + ) + end + + test "error" do + {:error, + %Error{ + status_code: 400, + title: "Invalid Order Book Parameters", + type: "https://stellar.org/horizon-errors/invalid_order_book" + }} = OrderBooks.retrieve(buying_asset: :error) + end +end diff --git a/test/horizon/path_test.exs b/test/horizon/path_test.exs new file mode 100644 index 00000000..759a1723 --- /dev/null +++ b/test/horizon/path_test.exs @@ -0,0 +1,48 @@ +defmodule Stellar.Horizon.PathTest do + use ExUnit.Case + + alias Stellar.Test.Fixtures.Horizon + alias Stellar.Horizon.Path + + setup do + json_body = Horizon.fixture("path") + attrs = Jason.decode!(json_body, keys: :atoms) + + %{attrs: attrs} + end + + test "new/2", %{attrs: attrs} do + %Path{ + destination_amount: 5.0, + destination_asset_code: "BB1", + destination_asset_issuer: "GD5J6HLF5666X4AZLTFTXLY46J5SW7EXRKBLEYPJP33S33MXZGV6CWFN", + destination_asset_type: "credit_alphanum4", + path: [ + %{ + asset_code: "NGNT", + asset_issuer: "GAWODAROMJ33V5YDFY3NPYTHVYQG7MJXVJ2ND3AOGIHYRWINES6ACCPD", + asset_type: "credit_alphanum4" + }, + %{asset_type: "native"} + ], + source_amount: 4.1900246, + source_asset_code: "USD", + source_asset_issuer: "GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX", + source_asset_type: "credit_alphanum4" + } = Path.new(attrs) + end + + test "new/2 empty_attrs" do + %Path{ + destination_amount: nil, + destination_asset_code: nil, + destination_asset_issuer: nil, + destination_asset_type: nil, + path: nil, + source_amount: nil, + source_asset_code: nil, + source_asset_issuer: nil, + source_asset_type: nil + } = Path.new(%{}) + end +end diff --git a/test/horizon/paths_test.exs b/test/horizon/paths_test.exs new file mode 100644 index 00000000..6be0e687 --- /dev/null +++ b/test/horizon/paths_test.exs @@ -0,0 +1,60 @@ +defmodule Stellar.Horizon.PathsTest do + use ExUnit.Case + + alias Stellar.Test.Fixtures.Horizon + alias Stellar.Horizon.{Paths, Path} + + setup do + json_body = Horizon.fixture("paths") + attrs = Jason.decode!(json_body, keys: :atoms) + + %{attrs: attrs} + end + + test "new/2", %{attrs: attrs} do + %Paths{ + records: [ + %Path{ + destination_amount: 5.0, + destination_asset_code: "BB1", + destination_asset_issuer: "GD5J6HLF5666X4AZLTFTXLY46J5SW7EXRKBLEYPJP33S33MXZGV6CWFN", + destination_asset_type: "credit_alphanum4", + path: [ + %{ + asset_code: "XCN", + asset_issuer: "GCNY5OXYSY4FKHOPT2SPOQZAOEIGXB5LBYW3HVU3OWSTQITS65M5RCNY", + asset_type: "credit_alphanum4" + }, + %{asset_type: "native"} + ], + source_amount: 28.9871131, + source_asset_code: "CNY", + source_asset_issuer: "GAREELUB43IRHWEASCFBLKHURCGMHE5IF6XSE7EXDLACYHGRHM43RFOX", + source_asset_type: "credit_alphanum4" + }, + %Path{ + destination_amount: 5.0, + destination_asset_code: "BB1", + destination_asset_issuer: "GD5J6HLF5666X4AZLTFTXLY46J5SW7EXRKBLEYPJP33S33MXZGV6CWFN", + destination_asset_type: "credit_alphanum4", + path: [ + %{ + asset_code: "ULT", + asset_issuer: "GC76RMFNNXBFDSJRBXCABWLHXDK4ITVQSMI56DC2ZJVC3YOLLPCKKULT", + asset_type: "credit_alphanum4" + }, + %{asset_type: "native"} + ], + source_amount: 29.0722784, + source_asset_code: "CNY", + source_asset_issuer: "GAREELUB43IRHWEASCFBLKHURCGMHE5IF6XSE7EXDLACYHGRHM43RFOX", + source_asset_type: "credit_alphanum4" + } + ] + } = Paths.new(attrs[:_embedded]) + end + + test "new/2 empty_attrs" do + %Paths{records: nil} = Paths.new(%{}) + end +end diff --git a/test/horizon/payment_paths_test.exs b/test/horizon/payment_paths_test.exs new file mode 100644 index 00000000..df79829d --- /dev/null +++ b/test/horizon/payment_paths_test.exs @@ -0,0 +1,202 @@ +defmodule Stellar.Horizon.Client.CannedPathRequests do + alias Stellar.Test.Fixtures.Horizon + + @base_url "https://horizon-testnet.stellar.org" + + @spec request( + method :: atom(), + url :: String.t(), + headers :: list(), + body :: String.t(), + options :: list() + ) :: {:ok, non_neg_integer(), list(), String.t()} | {:error, atom()} + + def request( + :get, + @base_url <> + "/paths?source_account=GBTKSXOTFMC5HR25SNL76MOVQW7GA3F6CQEY622ASLUV4VMLITI6TCOO&destination_asset_type=native&destination_amount=5", + _headers, + _body, + _opts + ) do + json_body = Horizon.fixture("paths") + {:ok, 200, [], json_body} + end + + def request( + :get, + @base_url <> + "/paths/strict-receive?destination_asset_type=native&destination_amount=5&source_account=GBTKSXOTFMC5HR25SNL76MOVQW7GA3F6CQEY622ASLUV4VMLITI6TCOO", + _headers, + _body, + _opts + ) do + json_body = Horizon.fixture("paths") + {:ok, 200, [], json_body} + end + + def request( + :get, + @base_url <> + "/paths/strict-send?source_asset_type=native&source_amount=5&destination_assets=TEST%3AGA654JC6QLA3ZH4O5V7X5NPM7KEWHKRG5GJA4PETK4SOFBUJLCCN74KQ", + _headers, + _body, + _opts + ) do + json_body = Horizon.fixture("paths") + {:ok, 200, [], json_body} + end + + def request( + :get, + @base_url <> "/paths/strict-receive?destination_amount=error", + _headers, + _body, + _opts + ) do + json_error = Horizon.fixture("400_invalid_receive_path") + {:ok, 400, [], json_error} + end + + def request(:get, @base_url <> "/paths/strict-send?source_amount=error", _headers, _body, _opts) do + json_error = Horizon.fixture("400_invalid_send_path") + {:ok, 400, [], json_error} + end +end + +defmodule Stellar.Horizon.PaymentPathsTest do + use ExUnit.Case + + alias Stellar.Horizon.Client.CannedPathRequests + alias Stellar.Horizon.{Path, Paths, PaymentPaths, Error} + + setup do + Application.put_env(:stellar_sdk, :http_client, CannedPathRequests) + + on_exit(fn -> + Application.delete_env(:stellar_sdk, :http_client) + end) + + %{ + source_account: "GBTKSXOTFMC5HR25SNL76MOVQW7GA3F6CQEY622ASLUV4VMLITI6TCOO", + destination_asset_type: :native, + destination_amount: 5, + destination_assets: "TEST:GA654JC6QLA3ZH4O5V7X5NPM7KEWHKRG5GJA4PETK4SOFBUJLCCN74KQ", + source_asset_type: :native, + source_amount: 5 + } + end + + test "list_paths/1", %{ + source_account: source_account, + destination_asset_type: destination_asset_type, + destination_amount: destination_amount + } do + {:ok, + %Paths{ + records: [ + %Path{ + destination_amount: 5.0, + destination_asset_issuer: "GD5J6HLF5666X4AZLTFTXLY46J5SW7EXRKBLEYPJP33S33MXZGV6CWFN", + source_amount: 28.9871131 + }, + %Path{ + destination_amount: 5.0, + destination_asset_issuer: "GD5J6HLF5666X4AZLTFTXLY46J5SW7EXRKBLEYPJP33S33MXZGV6CWFN", + source_amount: 29.0722784 + } + ] + }} = + PaymentPaths.list_paths( + source_account: source_account, + destination_asset_type: destination_asset_type, + destination_amount: destination_amount + ) + end + + test "list_receive_paths/1", %{ + source_account: source_account, + destination_asset_type: destination_asset_type, + destination_amount: destination_amount + } do + {:ok, + %Paths{ + records: [ + %Path{ + destination_asset_code: "BB1", + destination_asset_issuer: "GD5J6HLF5666X4AZLTFTXLY46J5SW7EXRKBLEYPJP33S33MXZGV6CWFN", + destination_asset_type: "credit_alphanum4" + }, + %Path{ + destination_asset_code: "BB1", + destination_asset_issuer: "GD5J6HLF5666X4AZLTFTXLY46J5SW7EXRKBLEYPJP33S33MXZGV6CWFN", + destination_asset_type: "credit_alphanum4" + } + ] + }} = + PaymentPaths.list_receive_paths( + destination_asset_type: destination_asset_type, + destination_amount: destination_amount, + source_account: source_account + ) + end + + test "list_send_paths/1", %{ + source_asset_type: source_asset_type, + source_amount: source_amount, + destination_assets: destination_assets + } do + {:ok, + %Paths{ + records: [ + %Stellar.Horizon.Path{ + destination_amount: 5.0, + path: [ + %{ + asset_code: "XCN", + asset_issuer: "GCNY5OXYSY4FKHOPT2SPOQZAOEIGXB5LBYW3HVU3OWSTQITS65M5RCNY", + asset_type: "credit_alphanum4" + }, + %{asset_type: "native"} + ] + }, + %Stellar.Horizon.Path{ + destination_amount: 5.0, + path: [ + %{ + asset_code: "ULT", + asset_issuer: "GC76RMFNNXBFDSJRBXCABWLHXDK4ITVQSMI56DC2ZJVC3YOLLPCKKULT", + asset_type: "credit_alphanum4" + }, + %{asset_type: "native"} + ] + } + ] + }} = + PaymentPaths.list_send_paths( + source_asset_type: source_asset_type, + source_amount: source_amount, + destination_assets: destination_assets + ) + end + + test "list strict receive error" do + {:error, + %Error{ + detail: "The request you sent was invalid in some way.", + extras: %{invalid_field: "destination_asset_type", reason: "Missing required field"}, + status_code: 400, + title: "Bad Request" + }} = PaymentPaths.list_receive_paths(destination_amount: "error") + end + + test "list strict send error" do + {:error, + %Error{ + detail: "The request you sent was invalid in some way.", + extras: %{invalid_field: "source_asset_type", reason: "Missing required field"}, + status_code: 400, + title: "Bad Request" + }} = PaymentPaths.list_send_paths(source_amount: "error") + end +end diff --git a/test/horizon/request_params_test.exs b/test/horizon/request_params_test.exs new file mode 100644 index 00000000..679f894c --- /dev/null +++ b/test/horizon/request_params_test.exs @@ -0,0 +1,55 @@ +defmodule Stellar.Horizon.RequestParamsTest do + use ExUnit.Case + + alias Stellar.Horizon.RequestParams + + setup do + %{ + selling_asset: :native, + buying_asset: [ + code: "BB1", + issuer: "GD5J6HLF5666X4AZLTFTXLY46J5SW7EXRKBLEYPJP33S33MXZGV6CWFN" + ] + } + end + + test "build_assets_params/2 empty params" do + [] = RequestParams.build_assets_params([], :selling_asset) + end + + test "build_assets_params/2 unauthorized type" do + [] = RequestParams.build_assets_params([selling_asset: :error], :test) + end + + test "build_assets_params/2", %{ + selling_asset: selling_asset, + buying_asset: buying_asset + } do + args = [selling_asset: selling_asset, buying_asset: buying_asset] + [selling_asset_type: :native] = RequestParams.build_assets_params(args, :selling_asset) + + [ + buying_asset_type: :credit_alphanum4, + buying_asset_code: "BB1", + buying_asset_issuer: "GD5J6HLF5666X4AZLTFTXLY46J5SW7EXRKBLEYPJP33S33MXZGV6CWFN" + ] = RequestParams.build_assets_params(args, :buying_asset) + end + + test "build_assets_params/2 with other asset code", %{ + selling_asset: selling_asset, + buying_asset: buying_asset + } do + args = [ + selling_asset: selling_asset, + buying_asset: Keyword.put(buying_asset, :code, "000000AWD") + ] + + [selling_asset_type: :native] = RequestParams.build_assets_params(args, :selling_asset) + + [ + buying_asset_type: :credit_alphanum12, + buying_asset_code: "000000AWD", + buying_asset_issuer: "GD5J6HLF5666X4AZLTFTXLY46J5SW7EXRKBLEYPJP33S33MXZGV6CWFN" + ] = RequestParams.build_assets_params(args, :buying_asset) + end +end diff --git a/test/horizon/request_test.exs b/test/horizon/request_test.exs index 2b7c99d8..7ccc5852 100644 --- a/test/horizon/request_test.exs +++ b/test/horizon/request_test.exs @@ -15,7 +15,7 @@ defmodule Stellar.Horizon.RequestTest do alias Stellar.Test.Fixtures.Horizon alias Stellar.Horizon.Client.CannedRequestImpl - alias Stellar.Horizon.{Collection, Error, Request, Transaction} + alias Stellar.Horizon.{Collection, Error, Request, Transaction, Paths, Path} setup do Application.put_env(:stellar_sdk, :http_client_impl, CannedRequestImpl) @@ -153,6 +153,19 @@ defmodule Stellar.Horizon.RequestTest do }} = Request.results({:ok, transactions}, collection: {Transaction, fn -> :ok end}) end + test "results/2 with _embedded attribute" do + body = Horizon.fixture("paths") + paths = Jason.decode!(body, keys: :atoms) + + {:ok, + %Paths{ + records: [ + %Path{source_amount: 28.9871131}, + %Path{source_amount: 29.0722784} + ] + }} = Request.results({:ok, paths}, as: Paths) + end + test "results/2 error" do {:error, %Error{}} = Request.results({:error, %Error{}}, collection: {Transaction, fn -> :ok end}) diff --git a/test/keypair/keypair_default_test.exs b/test/keypair/keypair_default_test.exs index 8236d9ba..e843348b 100644 --- a/test/keypair/keypair_default_test.exs +++ b/test/keypair/keypair_default_test.exs @@ -58,6 +58,20 @@ defmodule Stellar.KeyPair.DefaultTest do {:error, :invalid_signature_payload} = Default.sign(nil, secret) end + test "valid_signature?/3 with valid signature", %{ + public_key: public_key, + signature: signed_payload + } do + true = Default.valid_signature?(<<0, 0, 0, 0>>, signed_payload, public_key) + end + + test "valid_signature?/3 with invalid signature", %{ + public_key: public_key, + signature: signed_payload + } do + false = Default.valid_signature?(<<0, 0, 0, 1>>, signed_payload, public_key) + end + test "validate_public_key/1", %{public_key: public_key} do :ok = Default.validate_public_key(public_key) end diff --git a/test/keypair/keypair_test.exs b/test/keypair/keypair_test.exs index 5af789ad..3e63f5a7 100644 --- a/test/keypair/keypair_test.exs +++ b/test/keypair/keypair_test.exs @@ -45,6 +45,12 @@ defmodule Stellar.KeyPair.CannedKeyPairImpl do :ok end + @impl true + def valid_signature?(_payload, _signed_payload, _public_key) do + send(self(), {:valid_signature, "VALID_SIGNATURE"}) + :ok + end + @impl true def validate_public_key(_public_key) do send(self(), {:ok, "PUBLIC_KEY"}) @@ -112,6 +118,11 @@ defmodule Stellar.KeyPairTest do assert_receive({:signature, "SIGNATURE"}) end + test "valid_signature?/3" do + Stellar.KeyPair.valid_signature?(<<0, 0, 0, 0>>, "SIGNED_PAYLOAD", "PUBLIC_KEY") + assert_receive({:valid_signature, "VALID_SIGNATURE"}) + end + test "validate_public_key/1" do Stellar.KeyPair.validate_public_key("PUBLIC_KEY") assert_receive({:ok, "PUBLIC_KEY"}) diff --git a/test/support/fixtures/horizon/400_invalid_order_book.json b/test/support/fixtures/horizon/400_invalid_order_book.json new file mode 100644 index 00000000..acaef02b --- /dev/null +++ b/test/support/fixtures/horizon/400_invalid_order_book.json @@ -0,0 +1,6 @@ +{ + "type": "https://stellar.org/horizon-errors/invalid_order_book", + "title": "Invalid Order Book Parameters", + "status": 400, + "detail": "The parameters that specify what order book to view are invalid in some way. Please ensure that your type parameters (selling_asset_type and buying_asset_type) are one the following valid values: native, credit_alphanum4, credit_alphanum12. Also ensure that you have specified selling_asset_code and selling_asset_issuer if selling_asset_type is not 'native', as well as buying_asset_code and buying_asset_issuer if buying_asset_type is not 'native'" +} diff --git a/test/support/fixtures/horizon/400_invalid_receive_path.json b/test/support/fixtures/horizon/400_invalid_receive_path.json new file mode 100644 index 00000000..d8a44698 --- /dev/null +++ b/test/support/fixtures/horizon/400_invalid_receive_path.json @@ -0,0 +1,10 @@ +{ + "type": "https://stellar.org/horizon-errors/bad_request", + "title": "Bad Request", + "status": 400, + "detail": "The request you sent was invalid in some way.", + "extras": { + "invalid_field": "destination_asset_type", + "reason": "Missing required field" + } +} diff --git a/test/support/fixtures/horizon/400_invalid_send_path.json b/test/support/fixtures/horizon/400_invalid_send_path.json new file mode 100644 index 00000000..299d3228 --- /dev/null +++ b/test/support/fixtures/horizon/400_invalid_send_path.json @@ -0,0 +1,10 @@ +{ + "type": "https://stellar.org/horizon-errors/bad_request", + "title": "Bad Request", + "status": 400, + "detail": "The request you sent was invalid in some way.", + "extras": { + "invalid_field": "source_asset_type", + "reason": "Missing required field" + } +} diff --git a/test/support/fixtures/horizon/fee_stats.json b/test/support/fixtures/horizon/fee_stats.json new file mode 100644 index 00000000..d3b411c9 --- /dev/null +++ b/test/support/fixtures/horizon/fee_stats.json @@ -0,0 +1,37 @@ +{ + "last_ledger": "155545", + "last_ledger_base_fee": "100", + "ledger_capacity_usage": "0.01", + "fee_charged": { + "max": "100", + "min": "100", + "mode": "100", + "p10": "100", + "p20": "100", + "p30": "100", + "p40": "100", + "p50": "100", + "p60": "100", + "p70": "100", + "p80": "100", + "p90": "100", + "p95": "100", + "p99": "100" + }, + "max_fee": { + "max": "100", + "min": "100", + "mode": "100", + "p10": "100", + "p20": "100", + "p30": "100", + "p40": "100", + "p50": "100", + "p60": "100", + "p70": "100", + "p80": "100", + "p90": "100", + "p95": "100", + "p99": "100" + } +} diff --git a/test/support/fixtures/horizon/order_book.json b/test/support/fixtures/horizon/order_book.json new file mode 100644 index 00000000..aa986916 --- /dev/null +++ b/test/support/fixtures/horizon/order_book.json @@ -0,0 +1,46 @@ +{ + "bids": [ + { + "price_r": { + "n": 6014600, + "d": 102275119 + }, + "price": "0.0588080", + "amount": "0.1722469" + }, + { + "price_r": { + "n": 1250000, + "d": 21831117 + }, + "price": "0.0572577", + "amount": "0.2991796" + } + ], + "asks": [ + { + "price_r": { + "n": 118163, + "d": 2000000 + }, + "price": "0.0590815", + "amount": "8057.2710223" + }, + { + "price_r": { + "n": 60627, + "d": 1000000 + }, + "price": "0.0606270", + "amount": "10000.0000000" + } + ], + "base": { + "asset_type": "native" + }, + "counter": { + "asset_type": "credit_alphanum4", + "asset_code": "USD", + "asset_issuer": "GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX" + } +} diff --git a/test/support/fixtures/horizon/path.json b/test/support/fixtures/horizon/path.json new file mode 100644 index 00000000..4147334c --- /dev/null +++ b/test/support/fixtures/horizon/path.json @@ -0,0 +1,20 @@ +{ + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USD", + "source_asset_issuer": "GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX", + "source_amount": "4.1900246", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "BB1", + "destination_asset_issuer": "GD5J6HLF5666X4AZLTFTXLY46J5SW7EXRKBLEYPJP33S33MXZGV6CWFN", + "destination_amount": "5.0000000", + "path": [ + { + "asset_type": "credit_alphanum4", + "asset_code": "NGNT", + "asset_issuer": "GAWODAROMJ33V5YDFY3NPYTHVYQG7MJXVJ2ND3AOGIHYRWINES6ACCPD" + }, + { + "asset_type": "native" + } + ] +} diff --git a/test/support/fixtures/horizon/paths.json b/test/support/fixtures/horizon/paths.json new file mode 100644 index 00000000..84a311b5 --- /dev/null +++ b/test/support/fixtures/horizon/paths.json @@ -0,0 +1,46 @@ +{ + "_embedded": { + "records": [ + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "CNY", + "source_asset_issuer": "GAREELUB43IRHWEASCFBLKHURCGMHE5IF6XSE7EXDLACYHGRHM43RFOX", + "source_amount": "28.9871131", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "BB1", + "destination_asset_issuer": "GD5J6HLF5666X4AZLTFTXLY46J5SW7EXRKBLEYPJP33S33MXZGV6CWFN", + "destination_amount": "5.0000000", + "path": [ + { + "asset_type": "credit_alphanum4", + "asset_code": "XCN", + "asset_issuer": "GCNY5OXYSY4FKHOPT2SPOQZAOEIGXB5LBYW3HVU3OWSTQITS65M5RCNY" + }, + { + "asset_type": "native" + } + ] + }, + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "CNY", + "source_asset_issuer": "GAREELUB43IRHWEASCFBLKHURCGMHE5IF6XSE7EXDLACYHGRHM43RFOX", + "source_amount": "29.0722784", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "BB1", + "destination_asset_issuer": "GD5J6HLF5666X4AZLTFTXLY46J5SW7EXRKBLEYPJP33S33MXZGV6CWFN", + "destination_amount": "5.0000000", + "path": [ + { + "asset_type": "credit_alphanum4", + "asset_code": "ULT", + "asset_issuer": "GC76RMFNNXBFDSJRBXCABWLHXDK4ITVQSMI56DC2ZJVC3YOLLPCKKULT" + }, + { + "asset_type": "native" + } + ] + } + ] + } +}