Add bridged tokens functionality, could be enabled by compile time en… (#9169)

* 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
pull/9189/head
nikitosing 10 months ago committed by GitHub
parent 464054e6ad
commit a47d83f0be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 26
      .github/workflows/config.yml
  2. 1
      CHANGELOG.md
  3. 4
      apps/block_scout_web/lib/block_scout_web/api_router.ex
  4. 4
      apps/block_scout_web/lib/block_scout_web/chain.ex
  5. 49
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex
  6. 16
      apps/block_scout_web/lib/block_scout_web/paging_helper.ex
  7. 28
      apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex
  8. 1
      apps/block_scout_web/test/test_helper.exs
  9. 2
      apps/explorer/config/config.exs
  10. 2
      apps/explorer/config/dev.exs
  11. 4
      apps/explorer/config/prod.exs
  12. 3
      apps/explorer/config/test.exs
  13. 4
      apps/explorer/lib/explorer/application.ex
  14. 1049
      apps/explorer/lib/explorer/chain/bridged_token.ex
  15. 99
      apps/explorer/lib/explorer/chain/import/runner/tokens.ex
  16. 64
      apps/explorer/lib/explorer/chain/token.ex
  17. 10
      apps/explorer/lib/explorer/repo.ex
  18. 6
      apps/explorer/lib/explorer/tags/address_tag_cataloger.ex
  19. 29
      apps/explorer/priv/bridged_tokens/migrations/20230919080116_add_bridged_tokens.exs
  20. 2
      apps/explorer/test/support/data_case.ex
  21. 1
      apps/explorer/test/test_helper.exs
  22. 52
      apps/indexer/lib/indexer/bridged_tokens/calc_lp_tokens_total_liquidity.ex
  23. 44
      apps/indexer/lib/indexer/bridged_tokens/set_amb_bridged_metadata_for_tokens.ex
  24. 54
      apps/indexer/lib/indexer/bridged_tokens/set_omni_bridged_metadata_for_tokens.ex
  25. 26
      apps/indexer/lib/indexer/supervisor.ex
  26. 21
      config/config_helper.exs
  27. 7
      config/runtime.exs
  28. 9
      config/runtime/dev.exs
  29. 8
      config/runtime/prod.exs
  30. 3
      cspell.json

@ -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-"

@ -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`

@ -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)

@ -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

@ -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

@ -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)]

@ -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

@ -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)

@ -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",

@ -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,

@ -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,

@ -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",

@ -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
[]

File diff suppressed because it is too large Load Diff

@ -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

@ -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'.
"""

@ -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

@ -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

@ -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

@ -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())

@ -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)

@ -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

@ -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

@ -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

@ -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}]

@ -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

@ -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 ###
###############

@ -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")

@ -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")

@ -568,7 +568,8 @@
"evmversion",
"verifyproxycontract",
"checkproxyverification",
"NOTOK"
"NOTOK",
"sushiswap"
],
"enableFiletypes": [
"dotenv",

Loading…
Cancel
Save