From b82bc91768257b3ce92f4fc47fa106e0a28be34f Mon Sep 17 00:00:00 2001 From: Samuel <42943690+samuel-uniris@users.noreply.github.com> Date: Wed, 24 Aug 2022 09:47:08 +0200 Subject: [PATCH] Add graphql queries to get oracle data (#532) Provide a live feed with subscription and query for the latest or by time. --- lib/archethic/oracle_chain.ex | 9 +++- lib/archethic/oracle_chain/mem_table.ex | 13 +++--- .../oracle_chain/mem_table_loader.ex | 10 +++++ lib/archethic_web/graphql_schema.ex | 45 +++++++++++++++++++ .../graphql_schema/datetime_type.ex | 17 +++++++ .../graphql_schema/oracle_data.ex | 22 +++++++++ .../oracle_chain/mem_table_loader_test.exs | 4 +- 7 files changed, 111 insertions(+), 9 deletions(-) create mode 100644 lib/archethic_web/graphql_schema/oracle_data.ex diff --git a/lib/archethic/oracle_chain.ex b/lib/archethic/oracle_chain.ex index 7b2fb050d..3bd27a663 100644 --- a/lib/archethic/oracle_chain.ex +++ b/lib/archethic/oracle_chain.ex @@ -103,7 +103,7 @@ defmodule Archethic.OracleChain do @spec get_uco_price(DateTime.t()) :: list({binary(), float()}) def get_uco_price(date = %DateTime{}) do case MemTable.get_oracle_data("uco", date) do - {:ok, prices} -> + {:ok, prices, _} -> Enum.map(prices, fn {pair, price} -> {String.to_existing_atom(pair), price} end) _ -> @@ -111,6 +111,13 @@ defmodule Archethic.OracleChain do end end + @doc """ + Get the oracle data by date for a given service + """ + @spec get_oracle_data(binary(), DateTime.t()) :: + {:ok, map(), DateTime.t()} | {:error, :not_found} + defdelegate get_oracle_data(service, date), to: MemTable + def config_change(changed_conf) do changed_conf |> Keyword.get(Scheduler) diff --git a/lib/archethic/oracle_chain/mem_table.ex b/lib/archethic/oracle_chain/mem_table.ex index e9578cdb6..2bf8b6508 100644 --- a/lib/archethic/oracle_chain/mem_table.ex +++ b/lib/archethic/oracle_chain/mem_table.ex @@ -52,12 +52,13 @@ defmodule Archethic.OracleChain.MemTable do iex> MemTable.add_oracle_data("uco", %{ "eur" => 0.02 }, ~U[2021-06-04 10:00:00Z]) iex> MemTable.add_oracle_data("uco", %{ "eur" => 0.04 }, ~U[2021-06-04 15:00:00Z]) iex> MemTable.get_oracle_data("uco", ~U[2021-06-04 10:10:00Z]) - {:ok, %{ "eur" => 0.02 }} + {:ok, %{ "eur" => 0.02 }, ~U[2021-06-04 10:00:00Z]} iex> MemTable.get_oracle_data("uco", ~U[2021-06-04 20:10:40Z]) - {:ok, %{ "eur" => 0.04 }} + {:ok, %{ "eur" => 0.04 }, ~U[2021-06-04 15:00:00Z]} """ - @spec get_oracle_data(any(), DateTime.t()) :: {:ok, map()} | {:error, :not_found} + @spec get_oracle_data(any(), DateTime.t()) :: + {:ok, data :: map(), oracle_datetime :: DateTime.t()} | {:error, :not_found} def get_oracle_data(type, date = %DateTime{}) do timestamp = date @@ -70,13 +71,13 @@ defmodule Archethic.OracleChain.MemTable do :"$end_of_table" -> {:error, :not_found} - key -> + key = {time, _} -> [{_, data}] = :ets.lookup(:archethic_oracle, key) - {:ok, data} + {:ok, data, DateTime.from_unix!(time)} end [{_, data}] -> - {:ok, data} + {:ok, data, date} end end end diff --git a/lib/archethic/oracle_chain/mem_table_loader.ex b/lib/archethic/oracle_chain/mem_table_loader.ex index 338a87a45..cce5f41b7 100644 --- a/lib/archethic/oracle_chain/mem_table_loader.ex +++ b/lib/archethic/oracle_chain/mem_table_loader.ex @@ -43,6 +43,16 @@ defmodule Archethic.OracleChain.MemTableLoader do content |> Jason.decode!() + |> tap(fn data -> + Absinthe.Subscription.publish( + ArchethicWeb.Endpoint, + %{ + services: data, + timestamp: timestamp + }, + oracle_update: "oracle-topic" + ) + end) |> Enum.each(fn {service, data} -> MemTable.add_oracle_data(service, data, timestamp) end) diff --git a/lib/archethic_web/graphql_schema.ex b/lib/archethic_web/graphql_schema.ex index c6e7db2b4..b9461bab0 100644 --- a/lib/archethic_web/graphql_schema.ex +++ b/lib/archethic_web/graphql_schema.ex @@ -12,6 +12,7 @@ defmodule ArchethicWeb.GraphQLSchema do alias __MODULE__.PageType alias __MODULE__.TransactionAttestation alias __MODULE__.TransactionError + alias __MODULE__.OracleData import_types(HexType) import_types(DateTimeType) @@ -21,6 +22,7 @@ defmodule ArchethicWeb.GraphQLSchema do import_types(TransactionAttestation) import_types(TransactionError) import_types(PageType) + import_types(OracleData) query do @desc """ @@ -128,6 +130,22 @@ defmodule ArchethicWeb.GraphQLSchema do {:ok, Resolver.network_transactions(type, page)} end) end + + field :oracle_data, :oracle_data do + arg(:timestamp, :timestamp) + + resolve(fn args, _ -> + datetime = Map.get(args, :timestamp, DateTime.utc_now()) + + case Archethic.OracleChain.get_oracle_data("uco", datetime) do + {:ok, %{"eur" => eur, "usd" => usd}, datetime} -> + {:ok, %{services: %{uco: %{eur: eur, usd: usd}}, timestamp: datetime}} + + {:error, :not_found} -> + {:error, "Not data found at this date"} + end + end) + end end subscription do @@ -142,6 +160,9 @@ defmodule ArchethicWeb.GraphQLSchema do end) end + @desc """ + Subscribe to be notified when a transaction is on error + """ field :transaction_error, :transaction_error do arg(:address, non_null(:address)) @@ -149,5 +170,29 @@ defmodule ArchethicWeb.GraphQLSchema do {:ok, topic: args.address} end) end + + @desc """ + Subscribe to be notified when a new oracle data is stored + """ + field :oracle_update, :oracle_data do + config(fn _args, _info -> + {:ok, topic: "oracle-topic"} + end) + + resolve(fn %{timestamp: timestamp, services: %{"uco" => %{"eur" => eur, "usd" => usd}}}, + _, + _ -> + {:ok, + %{ + timestamp: timestamp, + services: %{ + uco: %{ + eur: eur, + usd: usd + } + } + }} + end) + end end end diff --git a/lib/archethic_web/graphql_schema/datetime_type.ex b/lib/archethic_web/graphql_schema/datetime_type.ex index 627801008..1b2efa2e5 100644 --- a/lib/archethic_web/graphql_schema/datetime_type.ex +++ b/lib/archethic_web/graphql_schema/datetime_type.ex @@ -8,5 +8,22 @@ defmodule ArchethicWeb.GraphQLSchema.DateTimeType do """ scalar :timestamp do serialize(&DateTime.to_unix/1) + parse(&parse_datetime/1) end + + defp parse_datetime(%Absinthe.Blueprint.Input.String{value: value}) do + with {timestamp, ""} <- Integer.parse(value), + {:ok, datetime} <- DateTime.from_unix(timestamp) do + {:ok, datetime} + else + _ -> + :error + end + end + + defp parse_datetime(%Absinthe.Blueprint.Input.Integer{value: value}) do + DateTime.from_unix(value) + end + + defp parse_datetime(_), do: :error end diff --git a/lib/archethic_web/graphql_schema/oracle_data.ex b/lib/archethic_web/graphql_schema/oracle_data.ex new file mode 100644 index 000000000..0a3743ba7 --- /dev/null +++ b/lib/archethic_web/graphql_schema/oracle_data.ex @@ -0,0 +1,22 @@ +defmodule ArchethicWeb.GraphQLSchema.OracleData do + @moduledoc false + + use Absinthe.Schema.Notation + + @desc """ + [OracleData] represents an oracle data. + """ + object :oracle_data do + field(:services, :oracle_services) + field(:timestamp, :timestamp) + end + + object :oracle_services do + field(:uco, :uco_data) + end + + object :uco_data do + field(:usd, :float) + field(:eur, :float) + end +end diff --git a/test/archethic/oracle_chain/mem_table_loader_test.exs b/test/archethic/oracle_chain/mem_table_loader_test.exs index a030d5210..b8c412eca 100644 --- a/test/archethic/oracle_chain/mem_table_loader_test.exs +++ b/test/archethic/oracle_chain/mem_table_loader_test.exs @@ -21,7 +21,7 @@ defmodule Archethic.OracleChain.MemTableLoaderTest do } }) - assert {:ok, %{"eur" => 0.02}} = MemTable.get_oracle_data("uco", DateTime.utc_now()) + assert {:ok, %{"eur" => 0.02}, _} = MemTable.get_oracle_data("uco", DateTime.utc_now()) end test "should load an oracle summary transaction and the related changes" do @@ -42,7 +42,7 @@ defmodule Archethic.OracleChain.MemTableLoaderTest do } }) - assert {:ok, %{"eur" => 0.07}} = + assert {:ok, %{"eur" => 0.07}, _} = MemTable.get_oracle_data("uco", DateTime.from_unix!(1_614_677_925)) end end