From fae80f7501645ae4ea01235cba62512c43a1485e Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Fri, 27 Sep 2024 13:49:48 +0300 Subject: [PATCH] feat: (celo) include token information in API response for address epoch rewards (#10831) * chore: add missing spec and doc * feat: include token information in API response for address epoch rewards --- .../block_scout_web/views/api/v2/celo_view.ex | 9 ++- .../chain/cache/celo_core_contracts.ex | 45 +++++++++++- .../explorer/chain/celo/election_reward.ex | 73 ++++++++++++++++++- .../lib/explorer/chain/celo/reader.ex | 1 + 4 files changed, 123 insertions(+), 5 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex index ac22f8ae13..99241c6d4e 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex @@ -221,7 +221,7 @@ defmodule BlockScoutWeb.API.V2.CeloView do } end - defp prepare_election_reward(%ElectionReward{} = reward) do + defp prepare_election_reward(%ElectionReward{token: %Token{}} = reward) do %{ amount: reward.amount, block_number: reward.block.number, @@ -237,7 +237,12 @@ defmodule BlockScoutWeb.API.V2.CeloView do reward.associated_account_address, reward.associated_account_address_hash ), - type: reward.type + type: reward.type, + token: + TokenView.render("token.json", %{ + token: reward.token, + contract_address_hash: reward.token.contract_address_hash + }) } end diff --git a/apps/explorer/lib/explorer/chain/cache/celo_core_contracts.ex b/apps/explorer/lib/explorer/chain/cache/celo_core_contracts.ex index 42e86fc203..b0c75b1ccd 100644 --- a/apps/explorer/lib/explorer/chain/cache/celo_core_contracts.ex +++ b/apps/explorer/lib/explorer/chain/cache/celo_core_contracts.ex @@ -161,7 +161,33 @@ defmodule Explorer.Chain.Cache.CeloCoreContracts do end end - defp get_address_updates(contract_atom) do + @doc """ + Retrieves all address updates for a specified core contract. + + ## Parameters + + - `contract_atom` (`atom()`): The atom representing the core contract (e.g., + `:accounts`, `:validators`). + + ## Returns + + - `{:ok, [map()]}`: On success, returns a list of maps containing address + updates for the contract. + - `{:error, reason}`: Returns an error tuple with one of the following + reasons: `:contract_atom_not_found`, `:contract_name_not_found` + + ## Examples + + iex> Explorer.Chain.Cache.CeloCoreContracts.get_address_updates(:validators) + {:ok, [%{"address" => "0x123...", "updated_at_block_number" => 1000000}, ...]} + + iex> Explorer.Chain.Cache.CeloCoreContracts.get_address_updates(:unknown_contract) + {:error, :contract_atom_not_found} + + """ + @spec get_address_updates(atom()) :: + {:ok, [map()]} | {:error, :contract_atom_not_found | :contract_name_not_found} + def get_address_updates(contract_atom) do with {:atom, {:ok, contract_name}} <- {:atom, Map.fetch(@atom_to_contract_name, contract_atom)}, {:addresses, {:ok, contract_name_to_addresses}} <- @@ -223,6 +249,23 @@ defmodule Explorer.Chain.Cache.CeloCoreContracts do end end + @doc """ + Retrieves the block number of the first address update for a given core + contract. + + ## Parameters + + - `contract_atom` (`atom()`): The atom representing the core contract. + + ## Returns + + - `{:ok, Block.block_number()}`: The block number of the first update. + - `{:error, :contract_atom_not_found}`: If the contract atom is not + recognized. + """ + @spec get_first_update_block_number(atom()) :: + {:ok, Block.block_number() | nil} + | {:error, :contract_atom_not_found} def get_first_update_block_number(contract_atom) do with {:ok, address_updates} <- get_address_updates(contract_atom), %{"updated_at_block_number" => updated_at_block_number} <- diff --git a/apps/explorer/lib/explorer/chain/celo/election_reward.ex b/apps/explorer/lib/explorer/chain/celo/election_reward.ex index f31898bf9d..e45d563f2b 100644 --- a/apps/explorer/lib/explorer/chain/celo/election_reward.ex +++ b/apps/explorer/lib/explorer/chain/celo/election_reward.ex @@ -32,8 +32,9 @@ defmodule Explorer.Chain.Celo.ElectionReward do import Ecto.Query, only: [from: 2, where: 3] import Explorer.Helper, only: [safe_parse_non_negative_integer: 1] + alias Explorer.Chain.Cache.CeloCoreContracts alias Explorer.{Chain, PagingOptions} - alias Explorer.Chain.{Address, Block, Hash, Wei} + alias Explorer.Chain.{Address, Block, Hash, Token, Wei} @type type :: :voter | :validator | :group | :delegated_payment @types_enum ~w(voter validator group delegated_payment)a @@ -96,6 +97,8 @@ defmodule Explorer.Chain.Celo.ElectionReward do null: false ) + field(:token, :any, virtual: true) :: Token.t() | nil + timestamps() end @@ -243,10 +246,76 @@ defmodule Explorer.Chain.Celo.ElectionReward do ) end + @doc """ + Joins the token table to the query based on the reward type. + + ## Parameters + - `query` (`Ecto.Query.t()`): The query to join the token table. + + ## Returns + - An Ecto query with the token table joined. + """ + @spec join_token(Ecto.Query.t()) :: Ecto.Query.t() + def join_token(query) do + # This match should never fail + %{ + voter: [voter_token_address_hash], + validator: [validator_token_address_hash], + group: [group_token_address_hash], + delegated_payment: [delegated_payment_token_address_hash] + } = + Map.new( + @reward_type_atom_to_token_atom, + fn {type, token_atom} -> + addresses = + token_atom + |> CeloCoreContracts.get_address_updates() + |> case do + {:ok, addresses} -> addresses + _ -> [] + end + |> Enum.map(fn %{"address" => address_hash_string} -> + {:ok, address_hash} = Hash.Address.cast(address_hash_string) + address_hash + end) + + {type, addresses} + end + ) + + from( + r in query, + join: t in Token, + on: + t.contract_address_hash == + fragment( + """ + CASE ? + WHEN ? THEN ?::bytea + WHEN ? THEN ?::bytea + WHEN ? THEN ?::bytea + WHEN ? THEN ?::bytea + ELSE NULL + END + """, + r.type, + ^"voter", + ^voter_token_address_hash.bytes, + ^"validator", + ^validator_token_address_hash.bytes, + ^"group", + ^group_token_address_hash.bytes, + ^"delegated_payment", + ^delegated_payment_token_address_hash.bytes + ), + select_merge: %{token: t} + ) + end + @doc """ Makes Explorer.PagingOptions map for election rewards. """ - @spec address_paging_options(map()) :: [Chain.paging_options()] + @spec block_paging_options(map()) :: [Chain.paging_options()] def block_paging_options(params) do with %{ "amount" => amount_string, diff --git a/apps/explorer/lib/explorer/chain/celo/reader.ex b/apps/explorer/lib/explorer/chain/celo/reader.ex index 35a534e9e5..ac49b8787b 100644 --- a/apps/explorer/lib/explorer/chain/celo/reader.ex +++ b/apps/explorer/lib/explorer/chain/celo/reader.ex @@ -52,6 +52,7 @@ defmodule Explorer.Chain.Celo.Reader do address_hash |> ElectionReward.address_hash_to_ordered_rewards_query() + |> ElectionReward.join_token() |> ElectionReward.paginate(paging_options) |> limit(^paging_options.page_size) |> join_associations(necessity_by_association)