diff --git a/apps/admin_api/lib/admin_api/v1/controllers/exchange_pair_controller.ex b/apps/admin_api/lib/admin_api/v1/controllers/exchange_pair_controller.ex index 7132b2d5f..0b31d6670 100644 --- a/apps/admin_api/lib/admin_api/v1/controllers/exchange_pair_controller.ex +++ b/apps/admin_api/lib/admin_api/v1/controllers/exchange_pair_controller.ex @@ -77,6 +77,9 @@ defmodule AdminAPI.V1.ExchangePairController do {:error, code} -> handle_error(conn, code) + + {:error, code, description} -> + handle_error(conn, code, description) end end @@ -98,6 +101,9 @@ defmodule AdminAPI.V1.ExchangePairController do {:error, code} -> handle_error(conn, code) + + {:error, code, description} -> + handle_error(conn, code, description) end end diff --git a/apps/admin_api/priv/spec.json b/apps/admin_api/priv/spec.json index db8d4e0ff..2158c2143 100644 --- a/apps/admin_api/priv/spec.json +++ b/apps/admin_api/priv/spec.json @@ -5621,7 +5621,14 @@ "type": "string" }, "rate": { - "type": "number" + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] }, "sync_opposite": { "type": "boolean" @@ -6088,7 +6095,14 @@ "type": "string" }, "rate": { - "type": "number" + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] } }, "required": [ diff --git a/apps/admin_api/priv/spec.yaml b/apps/admin_api/priv/spec.yaml index b2e0ee9c3..839705c48 100644 --- a/apps/admin_api/priv/spec.yaml +++ b/apps/admin_api/priv/spec.yaml @@ -1438,7 +1438,9 @@ paths: to_token_id: type: string rate: - type: number + oneOf: + - type: string + - type: number sync_opposite: type: boolean required: @@ -1472,7 +1474,9 @@ paths: id: type: string rate: - type: number + oneOf: + - type: string + - type: number required: - id example: diff --git a/apps/admin_api/priv/swagger/exchange_pair/request_bodies.yaml b/apps/admin_api/priv/swagger/exchange_pair/request_bodies.yaml index 2ad953be7..822061122 100644 --- a/apps/admin_api/priv/swagger/exchange_pair/request_bodies.yaml +++ b/apps/admin_api/priv/swagger/exchange_pair/request_bodies.yaml @@ -23,7 +23,9 @@ ExchangePairCreateBody: to_token_id: type: string rate: - type: number + oneOf: + - type: string + - type: number sync_opposite: type: boolean required: @@ -45,7 +47,9 @@ ExchangePairUpdateBody: id: type: string rate: - type: number + oneOf: + - type: string + - type: number required: - id example: diff --git a/apps/admin_api/test/admin_api/v1/controllers/exchange_pair_controller_test.exs b/apps/admin_api/test/admin_api/v1/controllers/exchange_pair_controller_test.exs index 63415cc57..f9484bc62 100644 --- a/apps/admin_api/test/admin_api/v1/controllers/exchange_pair_controller_test.exs +++ b/apps/admin_api/test/admin_api/v1/controllers/exchange_pair_controller_test.exs @@ -200,6 +200,21 @@ defmodule AdminAPI.V1.ExchangePairControllerTest do assert opposite["opposite_exchange_pair"]["id"] == pair["id"] end + test_with_auths "accepts the rate attribute as string" do + request_data = %{sync_opposite: true} |> insert_params() |> Map.put(:rate, "3.14159") + response = request("/exchange_pair.create", request_data) + + assert response["success"] == true + assert response["data"]["object"] == "list" + + pair = Enum.at(response["data"]["data"], 0) + + assert pair["object"] == "exchange_pair" + assert pair["from_token_id"] == request_data.from_token_id + assert pair["to_token_id"] == request_data.to_token_id + assert pair["rate"] == 3.14159 + end + test_with_auths "returns client:invalid_parameter error if given an exchange rate of 0" do request_data = insert_params(%{rate: 0}) response = request("/exchange_pair.create", request_data) @@ -236,6 +251,18 @@ defmodule AdminAPI.V1.ExchangePairControllerTest do "Invalid parameter provided. `rate` can't be blank." end + test_with_auths "returns client:invalid_parameter error if rate is not parsable" do + request_data = insert_params(%{rate: "1.2345rate"}) + response = request("/exchange_pair.create", request_data) + + assert response["success"] == false + assert response["data"]["object"] == "error" + assert response["data"]["code"] == "client:invalid_parameter" + + assert response["data"]["description"] == + "Invalid parameter provided. `rate` cannot be parsed. Got: \"1.2345rate\"" + end + test_with_auths "returns client:invalid_parameter error if from_token_id is not provided" do request_data = insert_params(%{from_token_id: nil}) response = request("/exchange_pair.create", request_data) @@ -397,6 +424,23 @@ defmodule AdminAPI.V1.ExchangePairControllerTest do assert opposite["opposite_exchange_pair"]["id"] == pair["id"] end + test_with_auths "accepts the rate attribute as string" do + exchange_pair = :exchange_pair |> insert() |> Repo.preload([:from_token, :to_token]) + + assert exchange_pair.rate != "999.99" + + # Prepare the update data while keeping only id the same + request_data = %{ + id: exchange_pair.id, + rate: "999.99" + } + + response = request("/exchange_pair.update", request_data) + + assert response["success"] == true + hd(response["data"]["data"])["rate"] == 999.99 + end + test_with_auths "reverts and returns error if sync_opposite: true but opposite pair is not found" do exchange_pair = :exchange_pair @@ -463,6 +507,18 @@ defmodule AdminAPI.V1.ExchangePairControllerTest do "Invalid parameter provided. `rate` must be greater than 0." end + test_with_auths "returns client:invalid_parameter error if rate is not parsable" do + pair = :exchange_pair |> insert() |> Repo.preload([:from_token, :to_token]) + response = request("/exchange_pair.update", %{id: pair.id, rate: "3.14159pi"}) + + assert response["success"] == false + assert response["data"]["object"] == "error" + assert response["data"]["code"] == "client:invalid_parameter" + + assert response["data"]["description"] == + "Invalid parameter provided. `rate` cannot be parsed. Got: \"3.14159pi\"" + end + defp assert_update_logs(logs, originator, target) do assert Enum.count(logs) == 1 diff --git a/apps/ewallet/lib/ewallet/gates/exchange_pair_gate.ex b/apps/ewallet/lib/ewallet/gates/exchange_pair_gate.ex index 3a6dacf7c..7ee73e430 100644 --- a/apps/ewallet/lib/ewallet/gates/exchange_pair_gate.ex +++ b/apps/ewallet/lib/ewallet/gates/exchange_pair_gate.ex @@ -23,7 +23,17 @@ defmodule EWallet.ExchangePairGate do @doc """ Inserts an exchange pair. """ - @spec insert(map()) :: {:ok, [%ExchangePair{}]} | {:error, atom() | Ecto.Changeset.t()} + @spec insert(map()) :: + {:ok, [%ExchangePair{}]} + | {:error, atom() | Ecto.Changeset.t()} + | {:error, atom(), String.t()} + def insert(%{"rate" => rate} = attrs) when is_binary(rate) do + case cast_rate(attrs) do + {:ok, casted} -> insert(casted) + error -> error + end + end + def insert(attrs) do Repo.transaction(fn -> with {:ok, direct} <- insert(:direct, attrs), @@ -68,7 +78,16 @@ defmodule EWallet.ExchangePairGate do Updates an exchange pair. """ @spec update(String.t(), map()) :: - {:ok, [%ExchangePair{}]} | {:error, atom() | Ecto.Changeset.t()} + {:ok, [%ExchangePair{}]} + | {:error, atom() | Ecto.Changeset.t()} + | {:error, atom(), String.t()} + def update(id, %{"rate" => rate} = attrs) when is_binary(rate) do + case cast_rate(attrs) do + {:ok, casted} -> update(id, casted) + error -> error + end + end + def update(id, attrs) do Repo.transaction(fn -> with {:ok, direct} <- update(:direct, id, attrs), @@ -119,6 +138,18 @@ defmodule EWallet.ExchangePairGate do defp update(:opposite, _, _), do: {:ok, nil} + defp cast_rate(%{"rate" => rate} = attrs) do + case Float.parse(rate) do + # Only a succesful parsing without any binary remainder shall proceed + {parsed, ""} -> + {:ok, Map.put(attrs, "rate", parsed)} + + _ -> + {:error, :invalid_parameter, + "Invalid parameter provided. `rate` cannot be parsed. Got: #{inspect(rate)}"} + end + end + @doc """ Deletes an exchange pair. """ diff --git a/apps/ewallet/test/ewallet/gates/exchange_pair_gate_test.exs b/apps/ewallet/test/ewallet/gates/exchange_pair_gate_test.exs index 97cba0e1f..b80cc6975 100644 --- a/apps/ewallet/test/ewallet/gates/exchange_pair_gate_test.exs +++ b/apps/ewallet/test/ewallet/gates/exchange_pair_gate_test.exs @@ -69,6 +69,43 @@ defmodule EWallet.ExchangePairGateTest do assert Enum.at(pairs, 1).rate == 1 / 2.0 end + test "accepts the rate as string" do + eth = insert(:token) + omg = insert(:token) + + {res, pairs} = + ExchangePairGate.insert(%{ + "rate" => "3.14159", + "from_token_id" => eth.id, + "to_token_id" => omg.id, + "sync_opposite" => true, + "originator" => %System{} + }) + + assert res == :ok + assert hd(pairs).rate == 3.14159 + end + + test "returns error if the string rate cannot be parsed" do + eth = insert(:token) + omg = insert(:token) + + {res, error, description} = + ExchangePairGate.insert(%{ + "rate" => "not a rate", + "from_token_id" => eth.id, + "to_token_id" => omg.id, + "sync_opposite" => true, + "originator" => %System{} + }) + + assert res == :error + assert error == :invalid_parameter + + assert description == + "Invalid parameter provided. `rate` cannot be parsed. Got: \"not a rate\"" + end + test "rollbacks if an error occurred along the way" do eth = insert(:token) omg = insert(:token)