From 1240826275790933a6fec7a97d87ec7051194691 Mon Sep 17 00:00:00 2001 From: bchamagne Date: Thu, 11 Jan 2024 12:41:56 +0100 Subject: [PATCH] quotes API is using UCID instead of symbol --- README.md | 29 ++++---- config/config.exs | 19 +++-- lib/archethic_fas/quotes.ex | 26 +++---- lib/archethic_fas/quotes/cache.ex | 18 ++--- lib/archethic_fas/quotes/currency.ex | 70 ------------------- lib/archethic_fas/quotes/provider.ex | 6 +- .../quotes/provider/coin_market_cap.ex | 35 ++-------- lib/archethic_fas/quotes/ucid.ex | 15 ++++ lib/archethic_fas/route/v1/quotes_latest.ex | 53 ++++++++++---- test/archethic_fas/quotes/currency_test.exs | 31 -------- .../quotes/provider/coin_market_cap_test.exs | 22 +++--- test/archethic_fas/quotes/ucid_test.exs | 6 ++ 12 files changed, 127 insertions(+), 203 deletions(-) delete mode 100644 lib/archethic_fas/quotes/currency.ex create mode 100644 lib/archethic_fas/quotes/ucid.ex delete mode 100644 test/archethic_fas/quotes/currency_test.exs create mode 100644 test/archethic_fas/quotes/ucid_test.exs diff --git a/README.md b/README.md index 0284104..0d352de 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # ArchethicFrontApiServer An API server that provides various off-chain resources. -Such as cryptocurrencies prices. +Such as cryptoassets prices. ## Envs @@ -9,13 +9,14 @@ Such as cryptocurrencies prices. ## Quotes -Cryptocurrencies handled: +Cryptoassets are identified by [Unified Cryptoasset ID (UCID)](https://support.coinmarketcap.com/hc/en-us/articles/20092704479515). +Available cryptoassets in this API: -- bitcoin -- bnb -- eth -- matic -- uco +- uco: 6887 +- matic: 3890 +- bnb: 1839 +- btc: 1 +- eth: 1027 - ... more later Providers requested: @@ -25,17 +26,17 @@ Providers requested: ### Latest -Return the latest available quotes from given cryptocurrencies. The result is an aggregate of multiple providers. +Return the latest available quotes from given cryptoassets. The result is an aggregate of multiple providers. **The values are cached for an entire minute.** -`GET /api/v1/quotes/latest?currency=uco,bitcoin,bnb,matic,eth` +`GET /api/v1/quotes/latest?ucids=6887,1,1027,3890,1839` ```json { - "bitcoin":46886.44559469423, - "bnb":301.88655780971703, - "eth":2263.032408397367, - "matic":0.790940929057782, - "uco":0.04767200156279931 + "1": 46886.44559469423, + "1839": 301.88655780971703, + "1027": 2263.032408397367, + "3890": 0.790940929057782, + "6887": 0.04767200156279931 } ``` diff --git a/config/config.exs b/config/config.exs index 448df9d..af9cf97 100644 --- a/config/config.exs +++ b/config/config.exs @@ -4,12 +4,17 @@ import Config # to minimize any confusion that may arise from assets that share identical tickers/symbols. config :archethic_fas, api_port: 3000, - ucids: %{ - uco: 6887, - matic: 3890, - bnb: 1839, - btc: 1, - eth: 1027 - } + ucids: [ + # uco + 6887, + # matic + 3890, + # bnb + 1839, + # btc + 1, + # eth + 1027 + ] import_config("#{Mix.env()}.exs") diff --git a/lib/archethic_fas/quotes.ex b/lib/archethic_fas/quotes.ex index 6fb2a41..b1d6ae3 100644 --- a/lib/archethic_fas/quotes.ex +++ b/lib/archethic_fas/quotes.ex @@ -1,24 +1,24 @@ defmodule ArchethicFAS.Quotes do @moduledoc """ - Everything related to crypto assets quotes + Everything related to cryptoassets quotes """ alias __MODULE__.Cache - alias __MODULE__.Currency + alias __MODULE__.UCID alias __MODULE__.Provider.CoinMarketCap @doc """ - Return the latest quotes of given currencies. + Return the latest quotes of given cryptoassets. Behind a cache. """ - @spec get_latest(list(Currency.t())) :: - {:ok, %{Currency.t() => float()}} | {:error, String.t()} - def get_latest(currencies) do + @spec get_latest(list(UCID.t())) :: + {:ok, %{UCID.t() => float()}} | {:error, String.t()} + def get_latest(ucids) do case Cache.get_latest() do {:ok, all_quotes} -> {:ok, - Map.filter(all_quotes, fn {currency, _} -> - currency in currencies + Map.filter(all_quotes, fn {ucid, _} -> + ucid in ucids end)} {:error, reason} -> @@ -27,12 +27,12 @@ defmodule ArchethicFAS.Quotes do end @doc """ - Return the latest quotes of given currencies. + Return the latest quotes of given cryptoassets Direct from providers. """ - @spec fetch_latest(list(Currency.t())) :: - {:ok, %{Currency.t() => float()}} | {:error, String.t()} - def fetch_latest(currencies) do - CoinMarketCap.fetch_latest(currencies) + @spec fetch_latest(list(UCID.t())) :: + {:ok, %{UCID.t() => float()}} | {:error, String.t()} + def fetch_latest(ucids) do + CoinMarketCap.fetch_latest(ucids) end end diff --git a/lib/archethic_fas/quotes/cache.ex b/lib/archethic_fas/quotes/cache.ex index 7e15256..173aa34 100644 --- a/lib/archethic_fas/quotes/cache.ex +++ b/lib/archethic_fas/quotes/cache.ex @@ -4,7 +4,7 @@ defmodule ArchethicFAS.Quotes.Cache do """ alias ArchethicFAS.Quotes - alias ArchethicFAS.Quotes.Currency + alias ArchethicFAS.Quotes.UCID use GenServer require Logger @@ -17,20 +17,14 @@ defmodule ArchethicFAS.Quotes.Cache do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end - @spec get_latest() :: {:ok, %{Currency.t() => float()}} | {:error, String.t()} + @spec get_latest() :: {:ok, %{UCID.t() => float()}} | {:error, String.t()} def get_latest() do case :ets.lookup(@table, :latest) do [{:latest, value}] -> - value + {:ok, value} [] -> - case hydrate() do - {:ok, value} -> - value - - {:error, _reason} -> - {:error, "Value not cached yet"} - end + hydrate() end end @@ -45,9 +39,9 @@ defmodule ArchethicFAS.Quotes.Cache do end def handle_call(:hydrate, _from, state) do - case Quotes.fetch_latest(Currency.list()) do + case Quotes.fetch_latest(UCID.list()) do {:ok, result} -> - :ets.insert(@table, {:latest, {:ok, result}}) + :ets.insert(@table, {:latest, result}) {:reply, {:ok, result}, state} e = {:error, reason} -> diff --git a/lib/archethic_fas/quotes/currency.ex b/lib/archethic_fas/quotes/currency.ex deleted file mode 100644 index 5912887..0000000 --- a/lib/archethic_fas/quotes/currency.ex +++ /dev/null @@ -1,70 +0,0 @@ -defmodule ArchethicFAS.Quotes.Currency do - @moduledoc """ - Everything related to currency - """ - - @ucids Application.compile_env!(:archethic_fas, :ucids) - - @type t :: :eth | :bnb | :matic | :btc | :uco - - @doc """ - Return all handled currencies - """ - @spec list() :: list(t()) - def list() do - Map.keys(@ucids) - end - - @doc """ - Transform a string into a currency - """ - @spec cast(String.t()) :: {:ok, t()} | :error - def cast("eth"), do: {:ok, :eth} - def cast("bnb"), do: {:ok, :bnb} - def cast("matic"), do: {:ok, :matic} - def cast("btc"), do: {:ok, :btc} - def cast("uco"), do: {:ok, :uco} - def cast(_), do: :error - - @spec cast_many(list(String.t())) :: - {:ok, list(t())} | {:error, {:invalid_currency, String.t()}} - def cast_many(currencies) do - do_cast_many(currencies, []) - end - - @doc """ - Return the UCID of the currency. - This assumes a config-present currency. - """ - @spec to_ucid(t()) :: pos_integer() - def to_ucid(currency) do - Map.fetch!(@ucids, currency) - end - - @doc """ - Return the currency of the UCID. - This assumes a config-present UCID. - """ - @spec from_ucid(pos_integer()) :: t() - def from_ucid(ucid) do - Map.filter(@ucids, fn - {_, ^ucid} -> true - _ -> false - end) - |> Map.keys() - |> hd() - end - - # order does not matter - defp do_cast_many([], acc), do: {:ok, acc} - - defp do_cast_many([str | rest], acc) do - case cast(str) do - :error -> - {:error, {:invalid_currency, str}} - - {:ok, currency} -> - do_cast_many(rest, [currency | acc]) - end - end -end diff --git a/lib/archethic_fas/quotes/provider.ex b/lib/archethic_fas/quotes/provider.ex index 562e764..ecb97e2 100644 --- a/lib/archethic_fas/quotes/provider.ex +++ b/lib/archethic_fas/quotes/provider.ex @@ -1,8 +1,8 @@ defmodule ArchethicFAS.Quotes.Provider do @moduledoc false - alias ArchethicFAS.Quotes.Currency + alias ArchethicFAS.Quotes.UCID - @callback fetch_latest(list(Currency.t())) :: - {:ok, %{Currency.t() => float()}} | {:error, String.t()} + @callback fetch_latest(list(UCID.t())) :: + {:ok, %{UCID.t() => float()}} | {:error, String.t()} end diff --git a/lib/archethic_fas/quotes/provider/coin_market_cap.ex b/lib/archethic_fas/quotes/provider/coin_market_cap.ex index 5f2c658..7a2c0f5 100644 --- a/lib/archethic_fas/quotes/provider/coin_market_cap.ex +++ b/lib/archethic_fas/quotes/provider/coin_market_cap.ex @@ -1,7 +1,7 @@ defmodule ArchethicFAS.Quotes.Provider.CoinMarketCap do @moduledoc false - alias ArchethicFAS.Quotes.Currency + alias ArchethicFAS.Quotes.UCID alias ArchethicFAS.Quotes.Provider @behaviour Provider @@ -11,36 +11,11 @@ defmodule ArchethicFAS.Quotes.Provider.CoinMarketCap do @doc """ Return the latest quotes of given currencies on this provider """ - @spec fetch_latest(list(Currency.t())) :: - {:ok, %{Currency.t() => float()}} | {:error, String.t()} - def fetch_latest(currencies) do - ucids = currencies_to_ucids(currencies) + @spec fetch_latest(list(UCID.t())) :: + {:ok, %{UCID.t() => float()}} | {:error, String.t()} + def fetch_latest([]), do: {:ok, %{}} - case do_fetch_latest(ucids) do - {:ok, prices_by_ucid} -> - {:ok, convert_ucids_to_currencies(prices_by_ucid)} - - {:error, reason} -> - {:error, reason} - end - end - - defp currencies_to_ucids(currencies) do - currencies - |> Enum.map(&Currency.to_ucid/1) - end - - defp convert_ucids_to_currencies(map) do - map - |> Enum.map(fn {ucid, usd_price} -> - {Currency.from_ucid(ucid), usd_price} - end) - |> Enum.into(%{}) - end - - defp do_fetch_latest([]), do: {:ok, %{}} - - defp do_fetch_latest(ucids) do + def fetch_latest(ucids) do query = URI.encode_query(%{id: Enum.join(ucids, ",")}) path = "/v2/cryptocurrency/quotes/latest?#{query}" opts = [transport_opts: conf(:transport_opts, [])] diff --git a/lib/archethic_fas/quotes/ucid.ex b/lib/archethic_fas/quotes/ucid.ex new file mode 100644 index 0000000..302b969 --- /dev/null +++ b/lib/archethic_fas/quotes/ucid.ex @@ -0,0 +1,15 @@ +defmodule ArchethicFAS.Quotes.UCID do + @moduledoc false + + @ucids Application.compile_env!(:archethic_fas, :ucids) + + @type t :: pos_integer() + + @doc """ + Return all handled ucids + """ + @spec list() :: list(t()) + def list() do + @ucids + end +end diff --git a/lib/archethic_fas/route/v1/quotes_latest.ex b/lib/archethic_fas/route/v1/quotes_latest.ex index 3867cf5..1dcaa68 100644 --- a/lib/archethic_fas/route/v1/quotes_latest.ex +++ b/lib/archethic_fas/route/v1/quotes_latest.ex @@ -3,7 +3,7 @@ defmodule ArchethicFAS.Route.V1.QuotesLatest do Return the latest quotes for given currencies """ alias ArchethicFAS.Quotes - alias ArchethicFAS.Quotes.Currency + alias ArchethicFAS.Quotes.UCID require Logger import Plug.Conn @@ -13,20 +13,24 @@ defmodule ArchethicFAS.Route.V1.QuotesLatest do def call(conn, _opts) do conn = fetch_query_params(conn) - with {:ok, currencies_str} <- extract_currencies_from_query_string(conn), - {:ok, currencies} <- Currency.cast_many(currencies_str), - {:ok, quotes} <- Quotes.get_latest(currencies) do + with {:ok, ucids} <- extract_ucids(conn), + :ok <- check_ucids(ucids), + {:ok, quotes} <- Quotes.get_latest(ucids) do conn |> put_resp_content_type("application/json") |> send_resp(200, Jason.encode!(quotes)) else - {:error, :no_currency_specified} -> + {:error, :missing_ucids_parameter} -> conn - |> send_resp(400, "Bad request: missing currency parameter") + |> send_resp(400, "Bad request: missing ucids parameter") - {:error, {:invalid_currency, currency}} -> + {:error, {:invalid_ucid, str}} -> conn - |> send_resp(400, "Bad request: invalid currency: #{currency}") + |> send_resp(400, "Bad request: invalid ucid: #{str}") + + {:error, {:non_handled_ucids, ucids}} -> + conn + |> send_resp(400, "Bad request: non handled ucids: #{Enum.join(ucids, ", ")}") {:error, reason} -> Logger.warning("/v1/quotes/latest failed: #{reason}") @@ -36,13 +40,38 @@ defmodule ArchethicFAS.Route.V1.QuotesLatest do end end - defp extract_currencies_from_query_string(conn) do + defp extract_ucids(conn) do case conn.query_params - |> Map.get("currency", "") + |> Map.get("ucids", "") |> String.split(",") |> Enum.map(&String.trim/1) do - [""] -> {:error, :no_currency_specified} - strs -> {:ok, strs} + [""] -> {:error, :missing_ucids_parameter} + strs -> strings_to_integers(strs) + end + end + + defp strings_to_integers(sts, acc \\ []) + defp strings_to_integers([], acc), do: {:ok, acc} + + defp strings_to_integers([str | rest], acc) do + case Integer.parse(str) do + {int, ""} -> + strings_to_integers(rest, [int | acc]) + + _ -> + {:error, {:invalid_ucid, str}} + end + end + + defp check_ucids(ucids) do + difference = MapSet.difference(MapSet.new(ucids), MapSet.new(UCID.list())) + + case MapSet.size(difference) do + 0 -> + :ok + + _ -> + {:error, {:non_handled_ucids, MapSet.to_list(difference)}} end end end diff --git a/test/archethic_fas/quotes/currency_test.exs b/test/archethic_fas/quotes/currency_test.exs deleted file mode 100644 index 6537834..0000000 --- a/test/archethic_fas/quotes/currency_test.exs +++ /dev/null @@ -1,31 +0,0 @@ -defmodule ArchethicFAS.Quotes.CurrencyTest do - alias ArchethicFAS.Quotes.Currency - - use ExUnit.Case - doctest Currency - - describe "cast/1" do - test "should return ok for known currencies" do - assert {:ok, :eth} = Currency.cast("eth") - assert {:ok, :bnb} = Currency.cast("bnb") - assert {:ok, :matic} = Currency.cast("matic") - assert {:ok, :btc} = Currency.cast("btc") - assert {:ok, :uco} = Currency.cast("uco") - end - - test "should return error for anything else" do - assert :error = Currency.cast("unknown") - end - end - - describe "cast_many/1" do - test "should return ok for known currencies" do - assert {:ok, [:bnb, :eth]} = Currency.cast_many(["eth", "bnb"]) - end - - test "should return error for anything else" do - assert {:error, {:invalid_currency, "unknown"}} = - Currency.cast_many(["eth", "unknown", "bnb"]) - end - end -end diff --git a/test/archethic_fas/quotes/provider/coin_market_cap_test.exs b/test/archethic_fas/quotes/provider/coin_market_cap_test.exs index 43d3f5f..2f27ad0 100644 --- a/test/archethic_fas/quotes/provider/coin_market_cap_test.exs +++ b/test/archethic_fas/quotes/provider/coin_market_cap_test.exs @@ -14,22 +14,22 @@ defmodule ArchethicFAS.Quotes.Provider.CoinMarketCapTest do end test "should return a OK response" do - assert {:ok, map} = CoinMarketCap.fetch_latest([:eth]) - assert [:eth] = Map.keys(map) + assert {:ok, map} = CoinMarketCap.fetch_latest([1027]) + assert [1027] = Map.keys(map) assert Enum.all?(Map.values(map), &is_float/1) - assert {:ok, map} = CoinMarketCap.fetch_latest([:matic]) - assert [:matic] = Map.keys(map) + assert {:ok, map} = CoinMarketCap.fetch_latest([3890]) + assert [3890] = Map.keys(map) assert Enum.all?(Map.values(map), &is_float/1) - assert {:ok, map} = CoinMarketCap.fetch_latest([:btc]) - assert [:btc] = Map.keys(map) + assert {:ok, map} = CoinMarketCap.fetch_latest([1]) + assert [1] = Map.keys(map) assert Enum.all?(Map.values(map), &is_float/1) - assert {:ok, map} = CoinMarketCap.fetch_latest([:bnb]) - assert [:bnb] = Map.keys(map) + assert {:ok, map} = CoinMarketCap.fetch_latest([1839]) + assert [1839] = Map.keys(map) assert Enum.all?(Map.values(map), &is_float/1) - assert {:ok, map} = CoinMarketCap.fetch_latest([:uco]) - assert [:uco] = Map.keys(map) + assert {:ok, map} = CoinMarketCap.fetch_latest([6887]) + assert [6887] = Map.keys(map) assert Enum.all?(Map.values(map), &is_float/1) - assert {:ok, map} = CoinMarketCap.fetch_latest([:eth, :matic, :btc, :bnb, :uco]) + assert {:ok, map} = CoinMarketCap.fetch_latest([1027, 3890, 1, 1839, 6887]) assert 5 = map_size(map) assert Enum.all?(Map.values(map), &is_float/1) end diff --git a/test/archethic_fas/quotes/ucid_test.exs b/test/archethic_fas/quotes/ucid_test.exs new file mode 100644 index 0000000..a38fd88 --- /dev/null +++ b/test/archethic_fas/quotes/ucid_test.exs @@ -0,0 +1,6 @@ +defmodule ArchethicFAS.Quotes.UCIDTest do + alias ArchethicFAS.Quotes.UCID + + use ExUnit.Case + doctest UCID +end