From a47d83f0beceb7b4a0934f690306749b2d78e2cc Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Fri, 26 Jan 2024 14:54:39 +0300 Subject: [PATCH] =?UTF-8?q?Add=20bridged=20tokens=20functionality,=20could?= =?UTF-8?q?=20be=20enabled=20by=20compile=20time=20en=E2=80=A6=20(#9169)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add bridged tokens functionality, could be enabled by compile time env var * Process reviewer's comment * Fix credo * Process review comments * Reset GA cache * Fix warning --- .github/workflows/config.yml | 26 +- CHANGELOG.md | 1 + .../lib/block_scout_web/api_router.ex | 4 + .../lib/block_scout_web/chain.ex | 4 + .../controllers/api/v2/token_controller.ex | 49 +- .../lib/block_scout_web/paging_helper.ex | 16 +- .../views/api/v2/token_view.ex | 28 +- apps/block_scout_web/test/test_helper.exs | 1 + apps/explorer/config/config.exs | 2 + apps/explorer/config/dev.exs | 2 + apps/explorer/config/prod.exs | 4 + apps/explorer/config/test.exs | 3 +- apps/explorer/lib/explorer/application.ex | 4 +- .../lib/explorer/chain/bridged_token.ex | 1049 +++++++++++++++++ .../explorer/chain/import/runner/tokens.ex | 99 +- apps/explorer/lib/explorer/chain/token.ex | 64 +- apps/explorer/lib/explorer/repo.ex | 10 + .../explorer/tags/address_tag_cataloger.ex | 6 +- .../20230919080116_add_bridged_tokens.exs | 29 + apps/explorer/test/support/data_case.ex | 2 + apps/explorer/test/test_helper.exs | 1 + .../calc_lp_tokens_total_liquidity.ex | 52 + .../set_amb_bridged_metadata_for_tokens.ex | 44 + .../set_omni_bridged_metadata_for_tokens.ex | 54 + apps/indexer/lib/indexer/supervisor.ex | 26 +- config/config_helper.exs | 21 +- config/runtime.exs | 7 + config/runtime/dev.exs | 9 + config/runtime/prod.exs | 8 + cspell.json | 3 +- 30 files changed, 1547 insertions(+), 81 deletions(-) create mode 100644 apps/explorer/lib/explorer/chain/bridged_token.ex create mode 100644 apps/explorer/priv/bridged_tokens/migrations/20230919080116_add_bridged_tokens.exs create mode 100644 apps/indexer/lib/indexer/bridged_tokens/calc_lp_tokens_total_liquidity.ex create mode 100644 apps/indexer/lib/indexer/bridged_tokens/set_amb_bridged_metadata_for_tokens.ex create mode 100644 apps/indexer/lib/indexer/bridged_tokens/set_omni_bridged_metadata_for_tokens.ex diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index e9566d63ac..71d16ae8ce 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -75,7 +75,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_37-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_38-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps- @@ -133,7 +133,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_37-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_38-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -157,7 +157,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_37-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_38-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -186,7 +186,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_37-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_38-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -230,7 +230,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_37-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_38-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -256,7 +256,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_37-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_38-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -285,7 +285,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_37-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_38-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -333,7 +333,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_37-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_38-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -379,7 +379,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_37-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_38-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -441,7 +441,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_37-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_38-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -501,7 +501,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_37-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_38-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -572,7 +572,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_37-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_38-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -640,7 +640,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_37-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_38-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" diff --git a/CHANGELOG.md b/CHANGELOG.md index 500518ccd2..7fea8f3a50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- [#9169](https://github.com/blockscout/blockscout/pull/9169) - Add bridged tokens functionality to master branch - [#9158](https://github.com/blockscout/blockscout/pull/9158) - Increase shared memory for PostgreSQL containers - [#9155](https://github.com/blockscout/blockscout/pull/9155) - Allow bypassing avg block time in proxy implementation re-fetch ttl calculation - [#9148](https://github.com/blockscout/blockscout/pull/9148) - Add `/api/v2/utils/decode-calldata` diff --git a/apps/block_scout_web/lib/block_scout_web/api_router.ex b/apps/block_scout_web/lib/block_scout_web/api_router.ex index f9da5834f4..4b32910bc8 100644 --- a/apps/block_scout_web/lib/block_scout_web/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/api_router.ex @@ -247,6 +247,10 @@ defmodule BlockScoutWeb.ApiRouter do end scope "/tokens" do + if Application.compile_env(:explorer, Explorer.Chain.BridgedToken)[:enabled] do + get("/bridged", V2.TokenController, :bridged_tokens_list) + end + get("/", V2.TokenController, :tokens_list) get("/:address_hash_param", V2.TokenController, :token) get("/:address_hash_param/counters", V2.TokenController, :counters) diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index e988dec2bf..a0798ff247 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -532,6 +532,10 @@ defmodule BlockScoutWeb.Chain do } end + defp paging_params({%Token{} = token, _}) do + paging_params(token) + end + defp paging_params(%TagAddress{id: id}) do %{"id" => id} end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex index bc4034ad0f..e29a2127d5 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex @@ -4,7 +4,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do alias BlockScoutWeb.AccessHelper alias BlockScoutWeb.API.V2.{AddressView, TransactionView} alias Explorer.{Chain, Repo} - alias Explorer.Chain.{Address, Token, Token.Instance} + alias Explorer.Chain.{Address, BridgedToken, Token, Token.Instance} alias Indexer.Fetcher.TokenTotalSupplyOnDemand import BlockScoutWeb.Chain, @@ -18,7 +18,12 @@ defmodule BlockScoutWeb.API.V2.TokenController do ] import BlockScoutWeb.PagingHelper, - only: [delete_parameters_from_next_page_params: 1, token_transfers_types_options: 1, tokens_sorting: 1] + only: [ + chain_ids_filter_options: 1, + delete_parameters_from_next_page_params: 1, + token_transfers_types_options: 1, + tokens_sorting: 1 + ] import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens: 1] @@ -32,6 +37,27 @@ defmodule BlockScoutWeb.API.V2.TokenController do {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)} do TokenTotalSupplyOnDemand.trigger_fetch(address_hash) + conn + |> token_response(token, address_hash) + end + end + + if Application.compile_env(:explorer, Explorer.Chain.BridgedToken)[:enabled] do + defp token_response(conn, token, address_hash) do + if token.bridged do + bridged_token = Repo.get_by(BridgedToken, home_token_contract_address_hash: address_hash) + + conn + |> put_status(200) + |> render(:bridged_token, %{token: {token, bridged_token}}) + else + conn + |> put_status(200) + |> render(:token, %{token: token}) + end + end + else + defp token_response(conn, token, _address_hash) do conn |> put_status(200) |> render(:token, %{token: token}) @@ -281,6 +307,25 @@ defmodule BlockScoutWeb.API.V2.TokenController do |> render(:tokens, %{tokens: tokens, next_page_params: next_page_params}) end + def bridged_tokens_list(conn, params) do + filter = params["q"] + + options = + params + |> paging_options() + |> Keyword.merge(chain_ids_filter_options(params)) + |> Keyword.merge(tokens_sorting(params)) + |> Keyword.merge(@api_true) + + {tokens, next_page} = filter |> BridgedToken.list_top_bridged_tokens(options) |> split_list_by_page() + + next_page_params = next_page |> next_page_params(tokens, delete_parameters_from_next_page_params(params)) + + conn + |> put_status(200) + |> render(:bridged_tokens, %{tokens: tokens, next_page_params: next_page_params}) + end + defp put_owner(token_instances, holder_address), do: Enum.map(token_instances, fn token_instance -> %Instance{token_instance | owner: holder_address} end) end diff --git a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex index 406f47b6c7..bb9ecef5ec 100644 --- a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex @@ -4,7 +4,7 @@ defmodule BlockScoutWeb.PagingHelper do """ import Explorer.Chain, only: [string_to_transaction_hash: 1] alias Explorer.Chain.Transaction - alias Explorer.{PagingOptions, SortingHelper} + alias Explorer.{Helper, PagingOptions, SortingHelper} @page_size 50 @default_paging_options %PagingOptions{page_size: @page_size + 1} @@ -12,6 +12,7 @@ defmodule BlockScoutWeb.PagingHelper do @allowed_type_labels ["coin_transfer", "contract_call", "contract_creation", "token_transfer", "token_creation"] @allowed_token_transfer_type_labels ["ERC-20", "ERC-721", "ERC-1155"] @allowed_nft_token_type_labels ["ERC-721", "ERC-1155"] + @allowed_chain_id [1, 56, 99] def paging_options(%{"block_number" => block_number_string, "index" => index_string}, [:validated | _]) do with {block_number, ""} <- Integer.parse(block_number_string), @@ -66,6 +67,19 @@ defmodule BlockScoutWeb.PagingHelper do def filter_options(_params, fallback), do: [fallback] + def chain_ids_filter_options(%{"chain_ids" => chain_id}) do + [ + chain_ids: + chain_id + |> String.split(",") + |> Enum.uniq() + |> Enum.map(&Helper.parse_integer/1) + |> Enum.filter(&Enum.member?(@allowed_chain_id, &1)) + ] + end + + def chain_ids_filter_options(_), do: [chain_id: []] + # sobelow_skip ["DOS.StringToAtom"] def type_filter_options(%{"type" => type}) do [type: type |> parse_filter(@allowed_type_labels) |> Enum.map(&String.to_atom/1)] diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex index 616299fde9..a6b5fc99f7 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex @@ -4,10 +4,10 @@ defmodule BlockScoutWeb.API.V2.TokenView do alias BlockScoutWeb.API.V2.Helper alias BlockScoutWeb.NFTHelper alias Ecto.Association.NotLoaded - alias Explorer.Chain.Address + alias Explorer.Chain.{Address, BridgedToken} alias Explorer.Chain.Token.Instance - def render("token.json", %{token: nil, contract_address_hash: contract_address_hash}) do + def render("token.json", %{token: nil = token, contract_address_hash: contract_address_hash}) do %{ "address" => Address.checksum(contract_address_hash), "symbol" => nil, @@ -20,6 +20,7 @@ defmodule BlockScoutWeb.API.V2.TokenView do "icon_url" => nil, "circulating_market_cap" => nil } + |> maybe_append_bridged_info(token) end def render("token.json", %{token: nil}) do @@ -39,6 +40,7 @@ defmodule BlockScoutWeb.API.V2.TokenView do "icon_url" => token.icon_url, "circulating_market_cap" => token.circulating_market_cap } + |> maybe_append_bridged_info(token) end def render("token_balances.json", %{ @@ -71,6 +73,20 @@ defmodule BlockScoutWeb.API.V2.TokenView do } end + def render("bridged_tokens.json", %{tokens: tokens, next_page_params: next_page_params}) do + %{"items" => Enum.map(tokens, &render("bridged_token.json", %{token: &1})), "next_page_params" => next_page_params} + end + + def render("bridged_token.json", %{token: {token, bridged_token}}) do + "token.json" + |> render(%{token: token}) + |> Map.merge(%{ + foreign_address: Address.checksum(bridged_token.foreign_token_contract_address_hash), + bridge_type: bridged_token.type, + origin_chain_id: bridged_token.foreign_chain_id + }) + end + def exchange_rate(%{fiat_value: fiat_value}) when not is_nil(fiat_value), do: to_string(fiat_value) def exchange_rate(_), do: nil @@ -114,4 +130,12 @@ defmodule BlockScoutWeb.API.V2.TokenView do defp prepare_holders_count(nil), do: nil defp prepare_holders_count(count) when count < 0, do: prepare_holders_count(0) defp prepare_holders_count(count), do: to_string(count) + + defp maybe_append_bridged_info(map, token) do + if BridgedToken.enabled?() do + (token && Map.put(map, "is_bridged", token.bridged || false)) || map + else + map + end + end end diff --git a/apps/block_scout_web/test/test_helper.exs b/apps/block_scout_web/test/test_helper.exs index 8a9fe648fe..e5c91ee2b6 100644 --- a/apps/block_scout_web/test/test_helper.exs +++ b/apps/block_scout_web/test/test_helper.exs @@ -31,6 +31,7 @@ Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.PolygonZkevm, :manual) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.RSK, :manual) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Shibarium, :manual) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Suave, :manual) +Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.BridgedTokens, :manual) Absinthe.Test.prime(BlockScoutWeb.Schema) diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index eafec8dec7..117ca2c0ab 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -138,6 +138,8 @@ config :explorer, config :explorer, :http_adapter, HTTPoison +config :explorer, Explorer.Chain.BridgedToken, enabled: ConfigHelper.parse_bool_env_var("BRIDGED_TOKENS_ENABLED") + config :logger, :explorer, # keep synced with `config/config.exs` format: "$dateT$time $metadata[$level] $message\n", diff --git a/apps/explorer/config/dev.exs b/apps/explorer/config/dev.exs index 8996b7e72c..1a6f4b2384 100644 --- a/apps/explorer/config/dev.exs +++ b/apps/explorer/config/dev.exs @@ -23,6 +23,8 @@ config :explorer, Explorer.Repo.Shibarium, timeout: :timer.seconds(80) config :explorer, Explorer.Repo.Suave, timeout: :timer.seconds(80) +config :explorer, Explorer.Repo.BridgedTokens, timeout: :timer.seconds(80) + config :explorer, Explorer.Tracer, env: "dev", disabled?: true config :logger, :explorer, diff --git a/apps/explorer/config/prod.exs b/apps/explorer/config/prod.exs index e8184837df..8dbd83fd76 100644 --- a/apps/explorer/config/prod.exs +++ b/apps/explorer/config/prod.exs @@ -36,6 +36,10 @@ config :explorer, Explorer.Repo.Suave, prepare: :unnamed, timeout: :timer.seconds(60) +config :explorer, Explorer.Repo.BridgedTokens, + prepare: :unnamed, + timeout: :timer.seconds(60) + config :explorer, Explorer.Tracer, env: "production", disabled?: true config :logger, :explorer, diff --git a/apps/explorer/config/test.exs b/apps/explorer/config/test.exs index 0da1447c6e..0d55c56330 100644 --- a/apps/explorer/config/test.exs +++ b/apps/explorer/config/test.exs @@ -48,7 +48,8 @@ for repo <- [ Explorer.Repo.PolygonZkevm, Explorer.Repo.RSK, Explorer.Repo.Shibarium, - Explorer.Repo.Suave + Explorer.Repo.Suave, + Explorer.Repo.BridgedTokens ] do config :explorer, repo, database: "explorer_test", diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 85a94c6a82..482abd0af7 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -118,7 +118,6 @@ defmodule Explorer.Application do configure(Explorer.Counters.BlockBurntFeeCounter), configure(Explorer.Counters.BlockPriorityFeeCounter), configure(Explorer.Counters.AverageBlockTime), - configure(Explorer.Counters.Bridge), configure(Explorer.Validator.MetadataProcessor), configure(Explorer.Tags.AddressTag.Cataloger), configure(MinMissingBlockNumber), @@ -143,7 +142,8 @@ defmodule Explorer.Application do Explorer.Repo.PolygonZkevm, Explorer.Repo.RSK, Explorer.Repo.Shibarium, - Explorer.Repo.Suave + Explorer.Repo.Suave, + Explorer.Repo.BridgedTokens ] else [] diff --git a/apps/explorer/lib/explorer/chain/bridged_token.ex b/apps/explorer/lib/explorer/chain/bridged_token.ex new file mode 100644 index 0000000000..ca4b007410 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/bridged_token.ex @@ -0,0 +1,1049 @@ +defmodule Explorer.Chain.BridgedToken do + @moduledoc """ + Represents a bridged token. + """ + use Explorer.Schema + + import Ecto.Changeset + import EthereumJSONRPC, only: [json_rpc: 2] + + import Ecto.Query, + only: [ + from: 2, + limit: 2, + where: 2 + ] + + alias ABI.{TypeDecoder, TypeEncoder} + alias Ecto.Changeset + alias EthereumJSONRPC.Contract + alias Explorer.{Chain, PagingOptions, Repo, SortingHelper} + + alias Explorer.Chain.{ + Address, + BridgedToken, + Hash, + InternalTransaction, + Search, + Token, + Transaction + } + + require Logger + + @default_paging_options %PagingOptions{page_size: 50} + + @typedoc """ + * `foreign_chain_id` - chain ID of a foreign token + * `foreign_token_contract_address_hash` - Foreign token's contract hash + * `home_token_contract_address` - The `t:Address.t/0` of the home token's contract + * `home_token_contract_address_hash` - Home token's contract hash foreign key + * `custom_metadata` - Arbitrary string with custom metadata. For instance, tokens/weights for Balance tokens + * `custom_cap` - Custom capitalization for this token + * `lp_token` - Boolean flag: LP token or not + * `type` - omni/amb + """ + @type t :: %BridgedToken{ + foreign_chain_id: Decimal.t(), + foreign_token_contract_address_hash: Hash.Address.t(), + home_token_contract_address: %Ecto.Association.NotLoaded{} | Address.t(), + home_token_contract_address_hash: Hash.Address.t(), + custom_metadata: String.t(), + custom_cap: Decimal.t(), + lp_token: boolean(), + type: String.t(), + exchange_rate: Decimal.t() + } + + @derive {Poison.Encoder, + except: [ + :__meta__, + :home_token_contract_address, + :inserted_at, + :updated_at + ]} + + @derive {Jason.Encoder, + except: [ + :__meta__, + :home_token_contract_address, + :inserted_at, + :updated_at + ]} + + @primary_key false + schema "bridged_tokens" do + field(:foreign_chain_id, :decimal) + field(:foreign_token_contract_address_hash, Hash.Address) + field(:custom_metadata, :string) + field(:custom_cap, :decimal) + field(:lp_token, :boolean) + field(:type, :string) + field(:exchange_rate, :decimal) + + belongs_to( + :home_token_contract_address, + Token, + foreign_key: :home_token_contract_address_hash, + primary_key: true, + references: :contract_address_hash, + type: Hash.Address + ) + + timestamps() + end + + @required_attrs ~w(home_token_contract_address_hash)a + @optional_attrs ~w(foreign_chain_id foreign_token_contract_address_hash custom_metadata custom_cap boolean type exchange_rate)a + + @doc false + def changeset(%BridgedToken{} = bridged_token, params \\ %{}) do + bridged_token + |> cast(params, @required_attrs ++ @optional_attrs) + |> validate_required(@required_attrs) + |> foreign_key_constraint(:home_token_contract_address) + |> unique_constraint(:home_token_contract_address_hash) + end + + def get_unprocessed_mainnet_lp_tokens_list do + query = + from(bt in BridgedToken, + where: bt.foreign_chain_id == ^1, + where: is_nil(bt.lp_token) or bt.lp_token == true, + select: bt + ) + + query + |> Repo.all() + end + + def necessary_envs_passed? do + config = Application.get_env(:explorer, __MODULE__) + eth_omni_bridge_mediator = config[:eth_omni_bridge_mediator] + bsc_omni_bridge_mediator = config[:bsc_omni_bridge_mediator] + poa_omni_bridge_mediator = config[:poa_omni_bridge_mediator] + + (eth_omni_bridge_mediator && eth_omni_bridge_mediator !== "") || + (bsc_omni_bridge_mediator && bsc_omni_bridge_mediator !== "") || + (poa_omni_bridge_mediator && poa_omni_bridge_mediator !== "") + end + + def enabled? do + Application.get_env(:explorer, __MODULE__)[:enabled] + end + + @doc """ + Returns a list of token addresses `t:Address.t/0`s that don't have an + bridged property revealed. + """ + def unprocessed_token_addresses_to_reveal_bridged_tokens do + query = + from(t in Token, + where: is_nil(t.bridged), + select: t.contract_address_hash + ) + + Repo.stream_reduce(query, [], &[&1 | &2]) + end + + @doc """ + Processes AMB tokens from mediators addresses provided + """ + def process_amb_tokens do + amb_bridge_mediators_var = Application.get_env(:explorer, __MODULE__)[:amb_bridge_mediators] + amb_bridge_mediators = (amb_bridge_mediators_var && String.split(amb_bridge_mediators_var, ",")) || [] + + json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + + foreign_json_rpc = Application.get_env(:explorer, __MODULE__)[:foreign_json_rpc] + + eth_call_foreign_json_rpc_named_arguments = + compose_foreign_json_rpc_named_arguments(json_rpc_named_arguments, foreign_json_rpc) + + try do + amb_bridge_mediators + |> Enum.each(fn amb_bridge_mediator_hash -> + with {:ok, bridge_contract_hash_resp} <- + get_bridge_contract_hash(amb_bridge_mediator_hash, json_rpc_named_arguments), + bridge_contract_hash <- decode_contract_address_hash_response(bridge_contract_hash_resp), + {:ok, destination_chain_id_resp} <- + get_destination_chain_id(bridge_contract_hash, json_rpc_named_arguments), + foreign_chain_id <- decode_contract_integer_response(destination_chain_id_resp), + {:ok, home_token_contract_hash_resp} <- + get_erc677_token_hash(amb_bridge_mediator_hash, json_rpc_named_arguments), + home_token_contract_hash_string <- decode_contract_address_hash_response(home_token_contract_hash_resp), + {:ok, home_token_contract_hash} <- Chain.string_to_address_hash(home_token_contract_hash_string), + {:ok, foreign_mediator_contract_hash_resp} <- + get_foreign_mediator_contract_hash(amb_bridge_mediator_hash, json_rpc_named_arguments), + foreign_mediator_contract_hash <- + decode_contract_address_hash_response(foreign_mediator_contract_hash_resp), + {:ok, foreign_token_contract_hash_resp} <- + get_erc677_token_hash(foreign_mediator_contract_hash, eth_call_foreign_json_rpc_named_arguments), + foreign_token_contract_hash_string <- + decode_contract_address_hash_response(foreign_token_contract_hash_resp), + {:ok, foreign_token_contract_hash} <- Chain.string_to_address_hash(foreign_token_contract_hash_string) do + insert_bridged_token_metadata(home_token_contract_hash, %{ + foreign_chain_id: foreign_chain_id, + foreign_token_address_hash: foreign_token_contract_hash, + custom_metadata: nil, + custom_cap: nil, + lp_token: nil, + type: "amb" + }) + + set_token_bridged_status(home_token_contract_hash, true) + else + result -> + Logger.debug([ + "failed to fetch metadata for token bridged with AMB mediator #{amb_bridge_mediator_hash}", + inspect(result) + ]) + end + end) + rescue + _ -> + :ok + end + + :ok + end + + @doc """ + Fetches bridged tokens metadata from OmniBridge. + """ + def fetch_omni_bridged_tokens_metadata(token_addresses) do + Enum.each(token_addresses, fn token_address_hash -> + created_from_int_tx_success_query = + from( + it in InternalTransaction, + inner_join: t in assoc(it, :transaction), + where: it.created_contract_address_hash == ^token_address_hash, + where: t.status == ^1 + ) + + created_from_int_tx_success = + created_from_int_tx_success_query + |> limit(1) + |> Repo.one() + + created_from_tx_query = + from( + t in Transaction, + where: t.created_contract_address_hash == ^token_address_hash + ) + + created_from_tx = + created_from_tx_query + |> Repo.all() + |> Enum.count() > 0 + + created_from_int_tx_query = + from( + it in InternalTransaction, + where: it.created_contract_address_hash == ^token_address_hash + ) + + created_from_int_tx = + created_from_int_tx_query + |> Repo.all() + |> Enum.count() > 0 + + cond do + created_from_tx -> + set_token_bridged_status(token_address_hash, false) + + created_from_int_tx && !created_from_int_tx_success -> + set_token_bridged_status(token_address_hash, false) + + created_from_int_tx && created_from_int_tx_success -> + proceed_with_set_omni_status(token_address_hash, created_from_int_tx_success) + + true -> + :ok + end + end) + + :ok + end + + defp proceed_with_set_omni_status(token_address_hash, created_from_int_tx_success) do + {:ok, eth_omni_status} = + extract_omni_bridged_token_metadata_wrapper( + token_address_hash, + created_from_int_tx_success, + :eth_omni_bridge_mediator + ) + + {:ok, bsc_omni_status} = + if eth_omni_status do + {:ok, false} + else + extract_omni_bridged_token_metadata_wrapper( + token_address_hash, + created_from_int_tx_success, + :bsc_omni_bridge_mediator + ) + end + + {:ok, poa_omni_status} = + if eth_omni_status || bsc_omni_status do + {:ok, false} + else + extract_omni_bridged_token_metadata_wrapper( + token_address_hash, + created_from_int_tx_success, + :poa_omni_bridge_mediator + ) + end + + if !eth_omni_status && !bsc_omni_status && !poa_omni_status do + set_token_bridged_status(token_address_hash, false) + end + end + + defp extract_omni_bridged_token_metadata_wrapper(token_address_hash, created_from_int_tx_success, mediator) do + omni_bridge_mediator = Application.get_env(:explorer, __MODULE__)[mediator] + %{transaction_hash: transaction_hash} = created_from_int_tx_success + + if omni_bridge_mediator && omni_bridge_mediator !== "" do + {:ok, omni_bridge_mediator_hash} = Chain.string_to_address_hash(omni_bridge_mediator) + + created_by_amb_mediator_query = + from( + it in InternalTransaction, + where: it.transaction_hash == ^transaction_hash, + where: it.to_address_hash == ^omni_bridge_mediator_hash + ) + + created_by_amb_mediator = + created_by_amb_mediator_query + |> Repo.all() + + if Enum.count(created_by_amb_mediator) > 0 do + extract_omni_bridged_token_metadata( + token_address_hash, + omni_bridge_mediator, + omni_bridge_mediator_hash + ) + + {:ok, true} + else + {:ok, false} + end + else + {:ok, false} + end + end + + defp extract_omni_bridged_token_metadata(token_address_hash, omni_bridge_mediator, omni_bridge_mediator_hash) do + json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + + with {:ok, _} <- + get_token_interfaces_version_signature(token_address_hash, json_rpc_named_arguments), + {:ok, foreign_token_address_abi_encoded} <- + get_foreign_token_address(omni_bridge_mediator, token_address_hash, json_rpc_named_arguments), + {:ok, bridge_contract_hash_resp} <- + get_bridge_contract_hash(omni_bridge_mediator_hash, json_rpc_named_arguments) do + foreign_token_address_hash_string = decode_contract_address_hash_response(foreign_token_address_abi_encoded) + {:ok, foreign_token_address_hash} = Chain.string_to_address_hash(foreign_token_address_hash_string) + + multi_token_bridge_hash_string = decode_contract_address_hash_response(bridge_contract_hash_resp) + + {:ok, foreign_chain_id_abi_encoded} = + get_destination_chain_id(multi_token_bridge_hash_string, json_rpc_named_arguments) + + foreign_chain_id = decode_contract_integer_response(foreign_chain_id_abi_encoded) + + foreign_json_rpc = Application.get_env(:explorer, __MODULE__)[:foreign_json_rpc] + + custom_metadata = + if foreign_chain_id == 1 do + get_bridged_token_custom_metadata(foreign_token_address_hash, json_rpc_named_arguments, foreign_json_rpc) + else + nil + end + + bridged_token_metadata = %{ + foreign_chain_id: foreign_chain_id, + foreign_token_address_hash: foreign_token_address_hash, + custom_metadata: custom_metadata, + custom_cap: nil, + lp_token: nil, + type: "omni" + } + + insert_bridged_token_metadata(token_address_hash, bridged_token_metadata) + + set_token_bridged_status(token_address_hash, true) + end + end + + defp get_bridge_contract_hash(mediator_hash, json_rpc_named_arguments) do + # keccak 256 from bridgeContract() + bridge_contract_signature = "0xcd596583" + + perform_eth_call_request(bridge_contract_signature, mediator_hash, json_rpc_named_arguments) + end + + defp get_erc677_token_hash(mediator_hash, json_rpc_named_arguments) do + # keccak 256 from erc677token() + erc677_token_signature = "0x18d8f9c9" + + perform_eth_call_request(erc677_token_signature, mediator_hash, json_rpc_named_arguments) + end + + defp get_foreign_mediator_contract_hash(mediator_hash, json_rpc_named_arguments) do + # keccak 256 from mediatorContractOnOtherSide() + mediator_contract_on_other_side_signature = "0x871c0760" + + perform_eth_call_request(mediator_contract_on_other_side_signature, mediator_hash, json_rpc_named_arguments) + end + + defp get_destination_chain_id(bridge_contract_hash, json_rpc_named_arguments) do + # keccak 256 from destinationChainId() + destination_chain_id_signature = "0xb0750611" + + perform_eth_call_request(destination_chain_id_signature, bridge_contract_hash, json_rpc_named_arguments) + end + + defp get_token_interfaces_version_signature(token_address_hash, json_rpc_named_arguments) do + # keccak 256 from getTokenInterfacesVersion() + get_token_interfaces_version_signature = "0x859ba28c" + + perform_eth_call_request(get_token_interfaces_version_signature, token_address_hash, json_rpc_named_arguments) + end + + defp get_foreign_token_address(omni_bridge_mediator, token_address_hash, json_rpc_named_arguments) do + # keccak 256 from foreignTokenAddress(address) + foreign_token_address_signature = "0x47ac7d6a" + + token_address_hash_abi_encoded = + [token_address_hash.bytes] + |> TypeEncoder.encode([:address]) + |> Base.encode16() + + foreign_token_address_method = foreign_token_address_signature <> token_address_hash_abi_encoded + + perform_eth_call_request(foreign_token_address_method, omni_bridge_mediator, json_rpc_named_arguments) + end + + defp perform_eth_call_request(method, destination, json_rpc_named_arguments) + when not is_nil(json_rpc_named_arguments) do + method + |> Contract.eth_call_request(destination, 1, nil, nil) + |> json_rpc(json_rpc_named_arguments) + end + + defp perform_eth_call_request(_method, _destination, json_rpc_named_arguments) + when is_nil(json_rpc_named_arguments) do + :error + end + + def decode_contract_address_hash_response(resp) do + case resp do + "0x000000000000000000000000" <> address -> + "0x" <> address + + _ -> + nil + end + end + + def decode_contract_integer_response(resp) do + case resp do + "0x" <> integer_encoded -> + {integer_value, _} = Integer.parse(integer_encoded, 16) + integer_value + + _ -> + nil + end + end + + defp set_token_bridged_status(token_address_hash, status) do + case Repo.get(Token, token_address_hash) do + %{bridged: bridged} = target_token -> + if !bridged do + token = Changeset.change(target_token, bridged: status) + + Repo.update(token) + end + + _ -> + :ok + end + end + + defp insert_bridged_token_metadata(token_address_hash, %{ + foreign_chain_id: foreign_chain_id, + foreign_token_address_hash: foreign_token_address_hash, + custom_metadata: custom_metadata, + custom_cap: custom_cap, + lp_token: lp_token, + type: type + }) do + target_token = Repo.get(Token, token_address_hash) + + if target_token do + {:ok, _} = + Repo.insert( + %BridgedToken{ + home_token_contract_address_hash: token_address_hash, + foreign_chain_id: foreign_chain_id, + foreign_token_contract_address_hash: foreign_token_address_hash, + custom_metadata: custom_metadata, + custom_cap: custom_cap, + lp_token: lp_token, + type: type + }, + on_conflict: :nothing + ) + end + end + + # Fetches custom metadata for bridged tokens from the node. + # Currently, gets Balancer token composite tokens with their weights + # from foreign chain + defp get_bridged_token_custom_metadata(foreign_token_address_hash, json_rpc_named_arguments, foreign_json_rpc) + when not is_nil(foreign_json_rpc) and foreign_json_rpc !== "" do + eth_call_foreign_json_rpc_named_arguments = + compose_foreign_json_rpc_named_arguments(json_rpc_named_arguments, foreign_json_rpc) + + balancer_custom_metadata(foreign_token_address_hash, eth_call_foreign_json_rpc_named_arguments) || + sushiswap_custom_metadata(foreign_token_address_hash, eth_call_foreign_json_rpc_named_arguments) + end + + defp get_bridged_token_custom_metadata(_foreign_token_address_hash, _json_rpc_named_arguments, foreign_json_rpc) + when is_nil(foreign_json_rpc) do + nil + end + + defp get_bridged_token_custom_metadata(_foreign_token_address_hash, _json_rpc_named_arguments, foreign_json_rpc) + when foreign_json_rpc == "" do + nil + end + + defp balancer_custom_metadata(foreign_token_address_hash, eth_call_foreign_json_rpc_named_arguments) do + # keccak 256 from getCurrentTokens() + get_current_tokens_signature = "0xcc77828d" + + case get_current_tokens_signature + |> Contract.eth_call_request(foreign_token_address_hash, 1, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments) do + {:ok, "0x"} -> + nil + + {:ok, "0x" <> balancer_current_tokens_encoded} -> + [balancer_current_tokens] = + try do + balancer_current_tokens_encoded + |> Base.decode16!(case: :mixed) + |> TypeDecoder.decode_raw([{:array, :address}]) + rescue + _ -> [] + end + + bridged_token_custom_metadata = + parse_bridged_token_custom_metadata( + balancer_current_tokens, + eth_call_foreign_json_rpc_named_arguments, + foreign_token_address_hash + ) + + tokens_and_weights(bridged_token_custom_metadata) + + _ -> + nil + end + end + + defp tokens_and_weights(bridged_token_custom_metadata) do + with true <- is_map(bridged_token_custom_metadata), + tokens = Map.get(bridged_token_custom_metadata, :tokens), + weights = Map.get(bridged_token_custom_metadata, :weights), + false <- tokens == "" do + if weights !== "", do: "#{tokens} #{weights}", else: tokens + else + _ -> nil + end + end + + defp sushiswap_custom_metadata(foreign_token_address_hash, eth_call_foreign_json_rpc_named_arguments) do + # keccak 256 from token0() + token0_signature = "0x0dfe1681" + + # keccak 256 from token1() + token1_signature = "0xd21220a7" + + # keccak 256 from name() + name_signature = "0x06fdde03" + + # keccak 256 from symbol() + symbol_signature = "0x95d89b41" + + with {:ok, "0x" <> token0_encoded} <- + token0_signature + |> Contract.eth_call_request(foreign_token_address_hash, 1, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments), + {:ok, "0x" <> token1_encoded} <- + token1_signature + |> Contract.eth_call_request(foreign_token_address_hash, 2, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments), + token0_hash <- parse_contract_response(token0_encoded, :address), + token1_hash <- parse_contract_response(token1_encoded, :address), + false <- is_nil(token0_hash), + false <- is_nil(token1_hash), + token0_hash_str <- "0x" <> Base.encode16(token0_hash, case: :lower), + token1_hash_str <- "0x" <> Base.encode16(token1_hash, case: :lower), + {:ok, "0x" <> token0_name_encoded} <- + name_signature + |> Contract.eth_call_request(token0_hash_str, 1, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments), + {:ok, "0x" <> token1_name_encoded} <- + name_signature + |> Contract.eth_call_request(token1_hash_str, 2, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments), + {:ok, "0x" <> token0_symbol_encoded} <- + symbol_signature + |> Contract.eth_call_request(token0_hash_str, 1, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments), + {:ok, "0x" <> token1_symbol_encoded} <- + symbol_signature + |> Contract.eth_call_request(token1_hash_str, 2, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments) do + token0_name = parse_contract_response(token0_name_encoded, :string, {:bytes, 32}) + token1_name = parse_contract_response(token1_name_encoded, :string, {:bytes, 32}) + token0_symbol = parse_contract_response(token0_symbol_encoded, :string, {:bytes, 32}) + token1_symbol = parse_contract_response(token1_symbol_encoded, :string, {:bytes, 32}) + + "#{token0_name}/#{token1_name} (#{token0_symbol}/#{token1_symbol})" + else + _ -> + nil + end + end + + def calc_lp_tokens_total_liquidity do + json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + foreign_json_rpc = Application.get_env(:explorer, __MODULE__)[:foreign_json_rpc] + bridged_mainnet_tokens_list = BridgedToken.get_unprocessed_mainnet_lp_tokens_list() + + Enum.each(bridged_mainnet_tokens_list, fn bridged_token -> + case calc_sushiswap_lp_tokens_cap( + bridged_token.home_token_contract_address_hash, + bridged_token.foreign_token_contract_address_hash, + json_rpc_named_arguments, + foreign_json_rpc + ) do + {:ok, new_custom_cap} -> + bridged_token + |> Changeset.change(%{custom_cap: new_custom_cap, lp_token: true}) + |> Repo.update() + + {:error, :not_lp_token} -> + bridged_token + |> Changeset.change(%{lp_token: false}) + |> Repo.update() + end + end) + + Logger.debug(fn -> "Total liquidity fetched for LP tokens" end) + end + + defp calc_sushiswap_lp_tokens_cap( + home_token_contract_address_hash, + foreign_token_address_hash, + json_rpc_named_arguments, + foreign_json_rpc + ) do + eth_call_foreign_json_rpc_named_arguments = + compose_foreign_json_rpc_named_arguments(json_rpc_named_arguments, foreign_json_rpc) + + # keccak 256 from getReserves() + get_reserves_signature = "0x0902f1ac" + + # keccak 256 from token0() + token0_signature = "0x0dfe1681" + + # keccak 256 from token1() + token1_signature = "0xd21220a7" + + # keccak 256 from totalSupply() + total_supply_signature = "0x18160ddd" + + with {:ok, "0x" <> get_reserves_encoded} <- + get_reserves_signature + |> Contract.eth_call_request(foreign_token_address_hash, 1, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments), + {:ok, "0x" <> home_token_total_supply_encoded} <- + total_supply_signature + |> Contract.eth_call_request(home_token_contract_address_hash, 1, nil, nil) + |> json_rpc(json_rpc_named_arguments), + [reserve0, reserve1, _] <- + parse_contract_response(get_reserves_encoded, [{:uint, 112}, {:uint, 112}, {:uint, 32}]), + {:ok, token0_cap_usd} <- + get_lp_token_cap( + home_token_total_supply_encoded, + token0_signature, + reserve0, + foreign_token_address_hash, + eth_call_foreign_json_rpc_named_arguments + ), + {:ok, token1_cap_usd} <- + get_lp_token_cap( + home_token_total_supply_encoded, + token1_signature, + reserve1, + foreign_token_address_hash, + eth_call_foreign_json_rpc_named_arguments + ) do + total_lp_cap = Decimal.add(token0_cap_usd, token1_cap_usd) + {:ok, total_lp_cap} + else + _ -> + {:error, :not_lp_token} + end + end + + defp get_lp_token_cap( + home_token_total_supply_encoded, + token_signature, + reserve, + foreign_token_address_hash, + eth_call_foreign_json_rpc_named_arguments + ) do + # keccak 256 from decimals() + decimals_signature = "0x313ce567" + + # keccak 256 from totalSupply() + total_supply_signature = "0x18160ddd" + + home_token_total_supply = + home_token_total_supply_encoded + |> parse_contract_response({:uint, 256}) + |> Decimal.new() + + case token_signature + |> Contract.eth_call_request(foreign_token_address_hash, 1, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments) do + {:ok, "0x" <> token_encoded} -> + with token_hash <- parse_contract_response(token_encoded, :address), + false <- is_nil(token_hash), + token_hash_str <- "0x" <> Base.encode16(token_hash, case: :lower), + {:ok, "0x" <> token_decimals_encoded} <- + decimals_signature + |> Contract.eth_call_request(token_hash_str, 1, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments), + {:ok, "0x" <> foreign_token_total_supply_encoded} <- + total_supply_signature + |> Contract.eth_call_request(foreign_token_address_hash, 1, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments) do + token_decimals = parse_contract_response(token_decimals_encoded, {:uint, 256}) + + foreign_token_total_supply = + foreign_token_total_supply_encoded + |> parse_contract_response({:uint, 256}) + |> Decimal.new() + + token_decimals_divider = + 10 + |> :math.pow(token_decimals) + |> Decimal.from_float() + + token_cap = + reserve + |> Decimal.div(foreign_token_total_supply) + |> Decimal.mult(home_token_total_supply) + |> Decimal.div(token_decimals_divider) + + token = Token.get_by_contract_address_hash(token_hash_str, []) + + token_cap_usd = + if token && token.fiat_value do + token.fiat_value + |> Decimal.mult(token_cap) + else + 0 + end + + {:ok, token_cap_usd} + else + _ -> :error + end + end + end + + defp parse_contract_response(abi_encoded_value, types) when is_list(types) do + values = + try do + abi_encoded_value + |> Base.decode16!(case: :mixed) + |> TypeDecoder.decode_raw(types) + rescue + _ -> [nil] + end + + values + end + + defp parse_contract_response(abi_encoded_value, type, emergency_type \\ nil) do + [value] = + try do + [res] = decode_contract_response(abi_encoded_value, type) + + [convert_binary_to_string(res, type)] + rescue + _ -> + if emergency_type do + try do + [res] = decode_contract_response(abi_encoded_value, emergency_type) + + [convert_binary_to_string(res, emergency_type)] + rescue + _ -> + [nil] + end + else + [nil] + end + end + + value + end + + defp decode_contract_response(abi_encoded_value, type) do + abi_encoded_value + |> Base.decode16!(case: :mixed) + |> TypeDecoder.decode_raw([type]) + end + + defp convert_binary_to_string(binary, type) do + case type do + {:bytes, _} -> + binary_to_string(binary) + + _ -> + binary + end + end + + defp compose_foreign_json_rpc_named_arguments(json_rpc_named_arguments, foreign_json_rpc) + when foreign_json_rpc != "" do + {_, eth_call_foreign_json_rpc_named_arguments} = + Keyword.get_and_update(json_rpc_named_arguments, :transport_options, fn transport_options -> + {_, updated_transport_options} = + update_transport_options_set_foreign_json_rpc(transport_options, foreign_json_rpc) + + {transport_options, updated_transport_options} + end) + + eth_call_foreign_json_rpc_named_arguments + end + + defp compose_foreign_json_rpc_named_arguments(_json_rpc_named_arguments, foreign_json_rpc) + when foreign_json_rpc == "" do + nil + end + + defp compose_foreign_json_rpc_named_arguments(json_rpc_named_arguments, _foreign_json_rpc) + when is_nil(json_rpc_named_arguments) do + nil + end + + defp update_transport_options_set_foreign_json_rpc(transport_options, foreign_json_rpc) do + Keyword.get_and_update(transport_options, :method_to_url, fn method_to_url -> + {_, updated_method_to_url} = + Keyword.get_and_update(method_to_url, :eth_call, fn eth_call -> + {eth_call, foreign_json_rpc} + end) + + {method_to_url, updated_method_to_url} + end) + end + + defp parse_bridged_token_custom_metadata( + balancer_current_tokens, + eth_call_foreign_json_rpc_named_arguments, + foreign_token_address_hash + ) do + balancer_current_tokens + |> Enum.reduce(%{:tokens => "", :weights => ""}, fn balancer_token_bytes, balancer_tokens_weights -> + balancer_token_hash_without_0x = + balancer_token_bytes + |> Base.encode16(case: :lower) + + balancer_token_hash = "0x" <> balancer_token_hash_without_0x + + # 95d89b41 = keccak256(symbol()) + symbol_signature = "0x95d89b41" + + case symbol_signature + |> Contract.eth_call_request(balancer_token_hash, 1, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments) do + {:ok, "0x" <> symbol_encoded} -> + [symbol] = + symbol_encoded + |> Base.decode16!(case: :mixed) + |> TypeDecoder.decode_raw([:string]) + + # f1b8a9b7 = keccak256(getNormalizedWeight(address)) + get_normalized_weight_signature = "0xf1b8a9b7" + + get_normalized_weight_arg_abi_encoded = + [balancer_token_bytes] + |> TypeEncoder.encode([:address]) + |> Base.encode16(case: :lower) + + get_normalized_weight_abi_encoded = get_normalized_weight_signature <> get_normalized_weight_arg_abi_encoded + + get_normalized_weight_resp = + get_normalized_weight_abi_encoded + |> Contract.eth_call_request(foreign_token_address_hash, 1, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments) + + parse_balancer_weights(get_normalized_weight_resp, balancer_tokens_weights, symbol) + + _ -> + nil + end + end) + end + + defp parse_balancer_weights(get_normalized_weight_resp, balancer_tokens_weights, symbol) do + case get_normalized_weight_resp do + {:ok, "0x" <> normalized_weight_encoded} -> + [normalized_weight] = + try do + normalized_weight_encoded + |> Base.decode16!(case: :mixed) + |> TypeDecoder.decode_raw([{:uint, 256}]) + rescue + _ -> + [] + end + + normalized_weight_to_100_perc = calc_normalized_weight_to_100_perc(normalized_weight) + + normalized_weight_in_perc = + normalized_weight_to_100_perc + |> div(1_000_000_000_000_000_000) + + current_tokens = Map.get(balancer_tokens_weights, :tokens) + current_weights = Map.get(balancer_tokens_weights, :weights) + + tokens_value = combine_tokens_value(current_tokens, symbol) + weights_value = combine_weights_value(current_weights, normalized_weight_in_perc) + + %{:tokens => tokens_value, :weights => weights_value} + + _ -> + nil + end + end + + defp calc_normalized_weight_to_100_perc(normalized_weight) do + if normalized_weight, do: 100 * normalized_weight, else: 0 + end + + defp combine_tokens_value(current_tokens, symbol) do + if current_tokens == "", do: symbol, else: current_tokens <> "/" <> symbol + end + + defp combine_weights_value(current_weights, normalized_weight_in_perc) do + if current_weights == "", + do: "#{normalized_weight_in_perc}", + else: current_weights <> "/" <> "#{normalized_weight_in_perc}" + end + + defp fetch_top_bridged_tokens(chain_ids, paging_options, filter, sorting, options) do + bridged_tokens_query = + __MODULE__ + |> apply_chain_ids_filter(chain_ids) + + base_query = + from(t in Token.base_token_query(nil, sorting), + right_join: bt in subquery(bridged_tokens_query), + on: t.contract_address_hash == bt.home_token_contract_address_hash, + where: t.total_supply > ^0, + where: t.bridged, + select: {t, bt}, + preload: [:contract_address] + ) + + base_query_with_paging = + base_query + |> SortingHelper.page_with_sorting(paging_options, sorting, Token.default_sorting()) + |> limit(^paging_options.page_size) + + query = + if filter && filter !== "" do + case Search.prepare_search_term(filter) do + {:some, filter_term} -> + base_query_with_paging + |> where(fragment("to_tsvector('english', symbol || ' ' || name) @@ to_tsquery(?)", ^filter_term)) + + _ -> + base_query_with_paging + end + else + base_query_with_paging + end + + query + |> Chain.select_repo(options).all() + end + + @spec list_top_bridged_tokens(String.t()) :: [{Token.t(), BridgedToken.t()}] + def list_top_bridged_tokens(filter, options \\ []) do + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + chain_ids = Keyword.get(options, :chain_ids, nil) + sorting = Keyword.get(options, :sorting, []) + + fetch_top_bridged_tokens(chain_ids, paging_options, filter, sorting, options) + end + + defp apply_chain_ids_filter(query, chain_ids) when chain_ids in [[], nil], do: query + + defp apply_chain_ids_filter(query, chain_ids) when is_list(chain_ids), + do: from(bt in query, where: bt.foreign_chain_id in ^chain_ids) + + def binary_to_string(binary) do + binary + |> :binary.bin_to_list() + |> Enum.filter(fn x -> x != 0 end) + |> List.to_string() + end + + def token_display_name_based_on_bridge_destination(name, foreign_chain_id) do + cond do + Decimal.compare(foreign_chain_id, 1) == :eq -> + name + |> String.replace("on xDai", "from Ethereum") + + Decimal.compare(foreign_chain_id, 56) == :eq -> + name + |> String.replace("on xDai", "from BSC") + + true -> + name + end + end + + def token_display_name_based_on_bridge_destination(name, symbol, foreign_chain_id) do + token_name = + cond do + Decimal.compare(foreign_chain_id, 1) == :eq -> + name + |> String.replace("on xDai", "from Ethereum") + + Decimal.compare(foreign_chain_id, 56) == :eq -> + name + |> String.replace("on xDai", "from BSC") + + true -> + name + end + + "#{token_name} (#{symbol})" + end +end diff --git a/apps/explorer/lib/explorer/chain/import/runner/tokens.ex b/apps/explorer/lib/explorer/chain/import/runner/tokens.ex index 791ee9daa2..638b59a710 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/tokens.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/tokens.ex @@ -136,36 +136,73 @@ defmodule Explorer.Chain.Import.Runner.Tokens do ) end - def default_on_conflict do - from( - token in Token, - update: [ - set: [ - name: fragment("COALESCE(EXCLUDED.name, ?)", token.name), - symbol: fragment("COALESCE(EXCLUDED.symbol, ?)", token.symbol), - total_supply: fragment("COALESCE(EXCLUDED.total_supply, ?)", token.total_supply), - decimals: fragment("COALESCE(EXCLUDED.decimals, ?)", token.decimals), - type: fragment("COALESCE(EXCLUDED.type, ?)", token.type), - cataloged: fragment("COALESCE(EXCLUDED.cataloged, ?)", token.cataloged), - skip_metadata: fragment("COALESCE(EXCLUDED.skip_metadata, ?)", token.skip_metadata), - # `holder_count` is not updated as a pre-existing token means the `holder_count` is already initialized OR - # need to be migrated with `priv/repo/migrations/scripts/update_new_tokens_holder_count_in_batches.sql.exs` - # Don't update `contract_address_hash` as it is the primary key and used for the conflict target - inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", token.inserted_at), - updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", token.updated_at) - ] - ], - where: - fragment( - "(EXCLUDED.name, EXCLUDED.symbol, EXCLUDED.total_supply, EXCLUDED.decimals, EXCLUDED.type, EXCLUDED.cataloged, EXCLUDED.skip_metadata) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?)", - token.name, - token.symbol, - token.total_supply, - token.decimals, - token.type, - token.cataloged, - token.skip_metadata - ) - ) + if Application.compile_env(:explorer, Explorer.Chain.BridgedToken)[:enabled] do + def default_on_conflict do + from( + token in Token, + update: [ + set: [ + name: fragment("COALESCE(EXCLUDED.name, ?)", token.name), + symbol: fragment("COALESCE(EXCLUDED.symbol, ?)", token.symbol), + total_supply: fragment("COALESCE(EXCLUDED.total_supply, ?)", token.total_supply), + decimals: fragment("COALESCE(EXCLUDED.decimals, ?)", token.decimals), + type: fragment("COALESCE(EXCLUDED.type, ?)", token.type), + cataloged: fragment("COALESCE(EXCLUDED.cataloged, ?)", token.cataloged), + bridged: fragment("COALESCE(EXCLUDED.bridged, ?)", token.bridged), + skip_metadata: fragment("COALESCE(EXCLUDED.skip_metadata, ?)", token.skip_metadata), + # `holder_count` is not updated as a pre-existing token means the `holder_count` is already initialized OR + # need to be migrated with `priv/repo/migrations/scripts/update_new_tokens_holder_count_in_batches.sql.exs` + # Don't update `contract_address_hash` as it is the primary key and used for the conflict target + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", token.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", token.updated_at) + ] + ], + where: + fragment( + "(EXCLUDED.name, EXCLUDED.symbol, EXCLUDED.total_supply, EXCLUDED.decimals, EXCLUDED.type, EXCLUDED.cataloged, EXCLUDED.bridged, EXCLUDED.skip_metadata) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?)", + token.name, + token.symbol, + token.total_supply, + token.decimals, + token.type, + token.cataloged, + token.bridged, + token.skip_metadata + ) + ) + end + else + def default_on_conflict do + from( + token in Token, + update: [ + set: [ + name: fragment("COALESCE(EXCLUDED.name, ?)", token.name), + symbol: fragment("COALESCE(EXCLUDED.symbol, ?)", token.symbol), + total_supply: fragment("COALESCE(EXCLUDED.total_supply, ?)", token.total_supply), + decimals: fragment("COALESCE(EXCLUDED.decimals, ?)", token.decimals), + type: fragment("COALESCE(EXCLUDED.type, ?)", token.type), + cataloged: fragment("COALESCE(EXCLUDED.cataloged, ?)", token.cataloged), + skip_metadata: fragment("COALESCE(EXCLUDED.skip_metadata, ?)", token.skip_metadata), + # `holder_count` is not updated as a pre-existing token means the `holder_count` is already initialized OR + # need to be migrated with `priv/repo/migrations/scripts/update_new_tokens_holder_count_in_batches.sql.exs` + # Don't update `contract_address_hash` as it is the primary key and used for the conflict target + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", token.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", token.updated_at) + ] + ], + where: + fragment( + "(EXCLUDED.name, EXCLUDED.symbol, EXCLUDED.total_supply, EXCLUDED.decimals, EXCLUDED.type, EXCLUDED.cataloged, EXCLUDED.skip_metadata) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?)", + token.name, + token.symbol, + token.total_supply, + token.decimals, + token.type, + token.cataloged, + token.skip_metadata + ) + ) + end end end diff --git a/apps/explorer/lib/explorer/chain/token.ex b/apps/explorer/lib/explorer/chain/token.ex index 66c3fdd5e2..b360646624 100644 --- a/apps/explorer/lib/explorer/chain/token.ex +++ b/apps/explorer/lib/explorer/chain/token.ex @@ -24,7 +24,7 @@ defmodule Explorer.Chain.Token do alias Ecto.Changeset alias Explorer.{Chain, SortingHelper} - alias Explorer.Chain.{Address, Hash, Search, Token} + alias Explorer.Chain.{Address, BridgedToken, Hash, Search, Token} alias Explorer.SmartContract.Helper @default_sorting [ @@ -35,6 +35,16 @@ defmodule Explorer.Chain.Token do asc: :contract_address_hash ] + if Application.compile_env(:explorer, Explorer.Chain.BridgedToken)[:enabled] do + @bridged_field quote( + do: [ + bridged: boolean() + ] + ) + else + @bridged_field quote(do: []) + end + @typedoc """ * `name` - Name of the token * `symbol` - Trading symbol of the token @@ -51,23 +61,25 @@ defmodule Explorer.Chain.Token do * `icon_url` - URL of the token's icon. * `is_verified_via_admin_panel` - is token verified via admin panel. """ - @type t :: %Token{ - name: String.t(), - symbol: String.t(), - total_supply: Decimal.t() | nil, - decimals: non_neg_integer(), - type: String.t(), - cataloged: boolean(), - contract_address: %Ecto.Association.NotLoaded{} | Address.t(), - contract_address_hash: Hash.Address.t(), - holder_count: non_neg_integer() | nil, - skip_metadata: boolean(), - total_supply_updated_at_block: non_neg_integer() | nil, - fiat_value: Decimal.t() | nil, - circulating_market_cap: Decimal.t() | nil, - icon_url: String.t(), - is_verified_via_admin_panel: boolean() - } + @type t :: + %Token{ + unquote_splicing(@bridged_field), + name: String.t(), + symbol: String.t(), + total_supply: Decimal.t() | nil, + decimals: non_neg_integer(), + type: String.t(), + cataloged: boolean(), + contract_address: %Ecto.Association.NotLoaded{} | Address.t(), + contract_address_hash: Hash.Address.t(), + holder_count: non_neg_integer() | nil, + skip_metadata: boolean(), + total_supply_updated_at_block: non_neg_integer() | nil, + fiat_value: Decimal.t() | nil, + circulating_market_cap: Decimal.t() | nil, + icon_url: String.t(), + is_verified_via_admin_panel: boolean() + } @derive {Poison.Encoder, except: [ @@ -110,6 +122,10 @@ defmodule Explorer.Chain.Token do type: Hash.Address ) + if Application.compile_env(:explorer, BridgedToken)[:enabled] do + field(:bridged, :boolean) + end + timestamps() end @@ -118,8 +134,10 @@ defmodule Explorer.Chain.Token do @doc false def changeset(%Token{} = token, params \\ %{}) do + additional_attrs = if BridgedToken.enabled?(), do: [:bridged], else: [] + token - |> cast(params, @required_attrs ++ @optional_attrs) + |> cast(params, @required_attrs ++ @optional_attrs ++ additional_attrs) |> validate_required(@required_attrs) |> trim_name() |> sanitize_token_input(:name) @@ -168,6 +186,14 @@ defmodule Explorer.Chain.Token do from(token in __MODULE__, where: token.contract_address_hash in ^contract_address_hashes) end + def base_token_query(type, sorting) do + query = from(t in Token, preload: [:contract_address]) + + query |> apply_filter(type) |> SortingHelper.apply_sorting(sorting, @default_sorting) + end + + def default_sorting, do: @default_sorting + @doc """ Lists the top `t:__MODULE__.t/0`'s'. """ diff --git a/apps/explorer/lib/explorer/repo.ex b/apps/explorer/lib/explorer/repo.ex index fd0ad4778b..e299150cdb 100644 --- a/apps/explorer/lib/explorer/repo.ex +++ b/apps/explorer/lib/explorer/repo.ex @@ -200,4 +200,14 @@ defmodule Explorer.Repo do ConfigHelper.init_repo_module(__MODULE__, opts) end end + + defmodule BridgedTokens do + use Ecto.Repo, + otp_app: :explorer, + adapter: Ecto.Adapters.Postgres + + def init(_, opts) do + ConfigHelper.init_repo_module(__MODULE__, opts) + end + end end diff --git a/apps/explorer/lib/explorer/tags/address_tag_cataloger.ex b/apps/explorer/lib/explorer/tags/address_tag_cataloger.ex index a8586313a6..da54d81e2d 100644 --- a/apps/explorer/lib/explorer/tags/address_tag_cataloger.ex +++ b/apps/explorer/lib/explorer/tags/address_tag_cataloger.ex @@ -165,7 +165,11 @@ defmodule Explorer.Tags.AddressTag.Cataloger do defp set_omni_tag do set_tag_for_multiple_env_var_addresses( - ["ETH_OMNI_BRIDGE_MEDIATOR", "BSC_OMNI_BRIDGE_MEDIATOR", "POA_OMNI_BRIDGE_MEDIATOR"], + [ + "BRIDGED_TOKENS_ETH_OMNI_BRIDGE_MEDIATOR", + "BRIDGED_TOKENS_BSC_OMNI_BRIDGE_MEDIATOR", + "BRIDGED_TOKENS_POA_OMNI_BRIDGE_MEDIATOR" + ], "omni bridge" ) end diff --git a/apps/explorer/priv/bridged_tokens/migrations/20230919080116_add_bridged_tokens.exs b/apps/explorer/priv/bridged_tokens/migrations/20230919080116_add_bridged_tokens.exs new file mode 100644 index 0000000000..2622358c1d --- /dev/null +++ b/apps/explorer/priv/bridged_tokens/migrations/20230919080116_add_bridged_tokens.exs @@ -0,0 +1,29 @@ +defmodule Explorer.Repo.BridgedTokens.Migrations.AddBridgedTokens do + use Ecto.Migration + + def change do + alter table(:tokens) do + add(:bridged, :boolean, null: true) + end + + create table(:bridged_tokens, primary_key: false) do + add(:foreign_chain_id, :numeric, null: false) + add(:foreign_token_contract_address_hash, :bytea, null: false) + add(:exchange_rate, :decimal) + add(:custom_metadata, :string, null: true) + add(:lp_token, :boolean, null: true) + add(:custom_cap, :decimal, null: true) + add(:type, :string, null: true) + + add( + :home_token_contract_address_hash, + references(:tokens, column: :contract_address_hash, on_delete: :delete_all, type: :bytea), + null: false + ) + + timestamps() + end + + create(unique_index(:bridged_tokens, :home_token_contract_address_hash)) + end +end diff --git a/apps/explorer/test/support/data_case.ex b/apps/explorer/test/support/data_case.ex index 579ea23a09..d0be3409fb 100644 --- a/apps/explorer/test/support/data_case.ex +++ b/apps/explorer/test/support/data_case.ex @@ -40,6 +40,7 @@ defmodule Explorer.DataCase do :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo.RSK) :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo.Shibarium) :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo.Suave) + :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo.BridgedTokens) unless tags[:async] do Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, {:shared, self()}) @@ -49,6 +50,7 @@ defmodule Explorer.DataCase do Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.RSK, {:shared, self()}) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Shibarium, {:shared, self()}) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Suave, {:shared, self()}) + Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.BridgedTokens, {:shared, self()}) end Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.BlockNumber.child_id()) diff --git a/apps/explorer/test/test_helper.exs b/apps/explorer/test/test_helper.exs index fd2de12998..3459a12781 100644 --- a/apps/explorer/test/test_helper.exs +++ b/apps/explorer/test/test_helper.exs @@ -18,6 +18,7 @@ Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.PolygonZkevm, :auto) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.RSK, :auto) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Shibarium, :auto) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Suave, :auto) +Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.BridgedTokens, :auto) Mox.defmock(Explorer.ExchangeRates.Source.TestSource, for: Explorer.ExchangeRates.Source) Mox.defmock(Explorer.Market.History.Source.Price.TestSource, for: Explorer.Market.History.Source.Price) diff --git a/apps/indexer/lib/indexer/bridged_tokens/calc_lp_tokens_total_liquidity.ex b/apps/indexer/lib/indexer/bridged_tokens/calc_lp_tokens_total_liquidity.ex new file mode 100644 index 0000000000..fabf22e75d --- /dev/null +++ b/apps/indexer/lib/indexer/bridged_tokens/calc_lp_tokens_total_liquidity.ex @@ -0,0 +1,52 @@ +defmodule Indexer.BridgedTokens.CalcLpTokensTotalLiquidity do + @moduledoc """ + Periodically updates LP tokens total liquidity + """ + + use GenServer + + require Logger + + alias Explorer.Chain.BridgedToken + + @interval :timer.minutes(20) + + def start_link([init_opts, gen_server_opts]) do + start_link(init_opts, gen_server_opts) + end + + def start_link(init_opts, gen_server_opts) do + GenServer.start_link(__MODULE__, init_opts, gen_server_opts) + end + + @impl GenServer + def init(opts) do + interval = opts[:interval] || @interval + + Process.send_after(self(), :calc_total_liquidity, interval) + + {:ok, %{interval: interval}} + end + + @impl GenServer + def handle_info(:calc_total_liquidity, %{interval: interval} = state) do + Logger.debug(fn -> "Calc LP tokens total liquidity" end) + + calc_total_liquidity() + + Process.send_after(self(), :calc_total_liquidity, interval) + + {:noreply, state} + end + + # don't handle other messages (e.g. :ssl_closed) + def handle_info(_, state) do + {:noreply, state} + end + + defp calc_total_liquidity do + BridgedToken.calc_lp_tokens_total_liquidity() + + Logger.debug(fn -> "Total liquidity fetched for LP tokens" end) + end +end diff --git a/apps/indexer/lib/indexer/bridged_tokens/set_amb_bridged_metadata_for_tokens.ex b/apps/indexer/lib/indexer/bridged_tokens/set_amb_bridged_metadata_for_tokens.ex new file mode 100644 index 0000000000..02fcaa9705 --- /dev/null +++ b/apps/indexer/lib/indexer/bridged_tokens/set_amb_bridged_metadata_for_tokens.ex @@ -0,0 +1,44 @@ +defmodule Indexer.BridgedTokens.SetAmbBridgedMetadataForTokens do + @moduledoc """ + Sets token metadata for bridged tokens from AMB extensions. + """ + + use GenServer + + require Logger + + alias Explorer.Chain.BridgedToken + + def start_link([init_opts, gen_server_opts]) do + start_link(init_opts, gen_server_opts) + end + + def start_link(init_opts, gen_server_opts) do + GenServer.start_link(__MODULE__, init_opts, gen_server_opts) + end + + @impl GenServer + def init(_opts) do + send(self(), :process_amb_tokens) + + {:ok, %{}} + end + + @impl GenServer + def handle_info(:process_amb_tokens, state) do + fetch_amb_bridged_tokens_metadata() + + {:noreply, state} + end + + # don't handle other messages (e.g. :ssl_closed) + def handle_info(_, state) do + {:noreply, state} + end + + defp fetch_amb_bridged_tokens_metadata do + :ok = BridgedToken.process_amb_tokens() + + Logger.debug(fn -> "Bridged status fetched for AMB tokens" end) + end +end diff --git a/apps/indexer/lib/indexer/bridged_tokens/set_omni_bridged_metadata_for_tokens.ex b/apps/indexer/lib/indexer/bridged_tokens/set_omni_bridged_metadata_for_tokens.ex new file mode 100644 index 0000000000..e6ba7afc3c --- /dev/null +++ b/apps/indexer/lib/indexer/bridged_tokens/set_omni_bridged_metadata_for_tokens.ex @@ -0,0 +1,54 @@ +defmodule Indexer.BridgedTokens.SetOmniBridgedMetadataForTokens do + @moduledoc """ + Periodically checks unprocessed tokens and sets bridged status. + """ + + use GenServer + + require Logger + + alias Explorer.Chain.BridgedToken + + @interval :timer.minutes(20) + + def start_link([init_opts, gen_server_opts]) do + start_link(init_opts, gen_server_opts) + end + + def start_link(init_opts, gen_server_opts) do + GenServer.start_link(__MODULE__, init_opts, gen_server_opts) + end + + @impl GenServer + def init(opts) do + interval = opts[:interval] || @interval + + send(self(), :reveal_unprocessed_tokens) + + {:ok, %{interval: interval}} + end + + @impl GenServer + def handle_info(:reveal_unprocessed_tokens, %{interval: interval} = state) do + Logger.debug(fn -> "Reveal unprocessed tokens" end) + + {:ok, token_addresses} = BridgedToken.unprocessed_token_addresses_to_reveal_bridged_tokens() + + fetch_omni_bridged_tokens_metadata(token_addresses) + + Process.send_after(self(), :reveal_unprocessed_tokens, interval) + + {:noreply, state} + end + + # don't handle other messages (e.g. :ssl_closed) + def handle_info(_, state) do + {:noreply, state} + end + + defp fetch_omni_bridged_tokens_metadata(token_addresses) do + :ok = BridgedToken.fetch_omni_bridged_tokens_metadata(token_addresses) + + Logger.debug(fn -> "Bridged status fetched for tokens" end) + end +end diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index 7dd38efb31..3ee2f6ad35 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -5,8 +5,13 @@ defmodule Indexer.Supervisor do use Supervisor + alias Explorer.Chain.BridgedToken + alias Indexer.{ Block, + BridgedTokens.CalcLpTokensTotalLiquidity, + BridgedTokens.SetAmbBridgedMetadataForTokens, + BridgedTokens.SetOmniBridgedMetadataForTokens, PendingOpsCleaner, PendingTransactionsSanitizer } @@ -178,12 +183,31 @@ defmodule Indexer.Supervisor do ] |> List.flatten() + all_fetchers = maybe_add_bridged_tokens_fetchers(basic_fetchers) + Supervisor.init( - basic_fetchers, + all_fetchers, strategy: :one_for_one ) end + defp maybe_add_bridged_tokens_fetchers(basic_fetchers) do + extended_fetchers = + if BridgedToken.enabled?() && BridgedToken.necessary_envs_passed?() do + [{CalcLpTokensTotalLiquidity, [[], []]}, {SetOmniBridgedMetadataForTokens, [[], []]}] ++ basic_fetchers + else + basic_fetchers + end + + amb_bridge_mediators = Application.get_env(:explorer, Explorer.Chain.BridgedToken)[:amb_bridge_mediators] + + if BridgedToken.enabled?() && amb_bridge_mediators && amb_bridge_mediators !== "" do + [{SetAmbBridgedMetadataForTokens, [[], []]} | extended_fetchers] + else + extended_fetchers + end + end + defp configure(process, opts) do if Application.get_env(:indexer, process)[:enabled] do [{process, opts}] diff --git a/config/config_helper.exs b/config/config_helper.exs index 645b20286c..985654e286 100644 --- a/config/config_helper.exs +++ b/config/config_helper.exs @@ -7,13 +7,20 @@ defmodule ConfigHelper do def repos do base_repos = [Explorer.Repo, Explorer.Repo.Account] - case System.get_env("CHAIN_TYPE") do - "polygon_edge" -> base_repos ++ [Explorer.Repo.PolygonEdge] - "polygon_zkevm" -> base_repos ++ [Explorer.Repo.PolygonZkevm] - "rsk" -> base_repos ++ [Explorer.Repo.RSK] - "shibarium" -> base_repos ++ [Explorer.Repo.Shibarium] - "suave" -> base_repos ++ [Explorer.Repo.Suave] - _ -> base_repos + repos = + case System.get_env("CHAIN_TYPE") do + "polygon_edge" -> base_repos ++ [Explorer.Repo.PolygonEdge] + "polygon_zkevm" -> base_repos ++ [Explorer.Repo.PolygonZkevm] + "rsk" -> base_repos ++ [Explorer.Repo.RSK] + "shibarium" -> base_repos ++ [Explorer.Repo.Shibarium] + "suave" -> base_repos ++ [Explorer.Repo.Suave] + _ -> base_repos + end + + if System.get_env("BRIDGED_TOKENS_ENABLED") do + repos ++ [Explorer.Repo.BridgedTokens] + else + repos end end diff --git a/config/runtime.exs b/config/runtime.exs index 3d3d6f103d..9a3f329acb 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -485,6 +485,13 @@ config :explorer, Explorer.Migrator.TransactionsDenormalization, batch_size: ConfigHelper.parse_integer_env_var("DENORMALIZATION_MIGRATION_BATCH_SIZE", 500), concurrency: ConfigHelper.parse_integer_env_var("DENORMALIZATION_MIGRATION_CONCURRENCY", 10) +config :explorer, Explorer.Chain.BridgedToken, + eth_omni_bridge_mediator: System.get_env("BRIDGED_TOKENS_ETH_OMNI_BRIDGE_MEDIATOR"), + bsc_omni_bridge_mediator: System.get_env("BRIDGED_TOKENS_BSC_OMNI_BRIDGE_MEDIATOR"), + poa_omni_bridge_mediator: System.get_env("BRIDGED_TOKENS_POA_OMNI_BRIDGE_MEDIATOR"), + amb_bridge_mediators: System.get_env("BRIDGED_TOKENS_AMB_BRIDGE_MEDIATORS"), + foreign_json_rpc: System.get_env("BRIDGED_TOKENS_FOREIGN_JSON_RPC", "") + ############### ### Indexer ### ############### diff --git a/config/runtime/dev.exs b/config/runtime/dev.exs index d0e69f6937..c8c56981d9 100644 --- a/config/runtime/dev.exs +++ b/config/runtime/dev.exs @@ -115,6 +115,15 @@ config :explorer, Explorer.Repo.Shibarium, url: System.get_env("DATABASE_URL"), pool_size: 1 +# Configures BridgedTokens database +config :explorer, Explorer.Repo.BridgedTokens, + database: database, + hostname: hostname, + url: System.get_env("DATABASE_URL"), + # actually this repo is not started, and its pool size remains unused. + # separating repos for different CHAIN_TYPE is implemented only for the sake of keeping DB schema update relevant to the current chain type + pool_size: 1 + variant = Variant.get() Code.require_file("#{variant}.exs", "apps/explorer/config/dev") diff --git a/config/runtime/prod.exs b/config/runtime/prod.exs index 7abd887681..c22e140c9e 100644 --- a/config/runtime/prod.exs +++ b/config/runtime/prod.exs @@ -87,6 +87,14 @@ config :explorer, Explorer.Repo.Shibarium, pool_size: 1, ssl: ExplorerConfigHelper.ssl_enabled?() +# Configures BridgedTokens database +config :explorer, Explorer.Repo.BridgedTokens, + url: System.get_env("DATABASE_URL"), + # actually this repo is not started, and its pool size remains unused. + # separating repos for different CHAIN_TYPE is implemented only for the sake of keeping DB schema update relevant to the current chain type + pool_size: 1, + ssl: ExplorerConfigHelper.ssl_enabled?() + variant = Variant.get() Code.require_file("#{variant}.exs", "apps/explorer/config/prod") diff --git a/cspell.json b/cspell.json index e2289d76f9..503b81b999 100644 --- a/cspell.json +++ b/cspell.json @@ -568,7 +568,8 @@ "evmversion", "verifyproxycontract", "checkproxyverification", - "NOTOK" + "NOTOK", + "sushiswap" ], "enableFiletypes": [ "dotenv",