An ability to separate Ecto Repo DB endpoint for the API endpoints

pull/5004/head
Viktor Baranov 3 years ago
parent da57a29b59
commit 29aa23e213
  1. 3
      .dialyzer-ignore
  2. 24
      .github/workflows/config.yml
  3. 1
      CHANGELOG.md
  4. 5
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex
  5. 4
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/block_controller.ex
  6. 21
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex
  7. 4
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/stats_controller.ex
  8. 6
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/token_controller.ex
  9. 3
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/transaction_controller.ex
  10. 3
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/bridged_tokens_controller.ex
  11. 4
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/holder_controller.ex
  12. 3
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_log_controller.ex
  13. 16
      apps/explorer/config/dev.exs
  14. 13
      apps/explorer/config/prod.exs
  15. 10
      apps/explorer/config/test.exs
  16. 1
      apps/explorer/lib/explorer/application.ex
  17. 400
      apps/explorer/lib/explorer/chain.ex
  18. 4
      apps/explorer/lib/explorer/chain/cache/address_sum.ex
  19. 4
      apps/explorer/lib/explorer/chain/cache/address_sum_minus_burnt.ex
  20. 2
      apps/explorer/lib/explorer/chain/transaction.ex
  21. 6
      apps/explorer/lib/explorer/chain/transaction/history/historian.ex
  22. 10
      apps/explorer/lib/explorer/eth_rpc.ex
  23. 72
      apps/explorer/lib/explorer/etherscan.ex
  24. 28
      apps/explorer/lib/explorer/etherscan/addresses.ex
  25. 95
      apps/explorer/lib/explorer/etherscan/blocks.ex
  26. 167
      apps/explorer/lib/explorer/etherscan/contracts.ex
  27. 4
      apps/explorer/lib/explorer/etherscan/logs.ex
  28. 40
      apps/explorer/lib/explorer/etherscan/rpc.ex
  29. 4
      apps/explorer/lib/explorer/graphql.ex
  30. 13
      apps/explorer/lib/explorer/repo.ex
  31. 0
      apps/explorer/priv/repo_api/migrations/.gitkeep
  32. 22
      apps/explorer/test/explorer/chain_test.exs
  33. 1
      apps/explorer/test/explorer/etherscan_test.exs
  34. 24
      apps/explorer/test/explorer/graphql_test.exs

@ -17,8 +17,7 @@ lib/phoenix/router.ex:324
lib/phoenix/router.ex:402
lib/block_scout_web/views/layout_view.ex:145: The call 'Elixir.Poison.Parser':'parse!'
lib/block_scout_web/views/layout_view.ex:237: The call 'Elixir.Poison.Parser':'parse!'
lib/block_scout_web/controllers/api/rpc/transaction_controller.ex:21
lib/block_scout_web/controllers/api/rpc/transaction_controller.ex:22
lib/block_scout_web/controllers/api/rpc/transaction_controller.ex:23
lib/explorer/smart_contract/reader.ex:461
lib/indexer/fetcher/token_total_supply_on_demand.ex:16
lib/explorer/exchange_rates/source.ex:110

@ -38,7 +38,7 @@ jobs:
path: |
deps
_build
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_5-${{ hashFiles('mix.lock') }}
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_6-${{ hashFiles('mix.lock') }}
restore-keys: |
${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-
@ -98,7 +98,7 @@ jobs:
path: |
deps
_build
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_5-${{ hashFiles('mix.lock') }}
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_6-${{ hashFiles('mix.lock') }}
restore-keys: |
${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-"
@ -122,7 +122,7 @@ jobs:
path: |
deps
_build
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_5-${{ hashFiles('mix.lock') }}
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_6-${{ hashFiles('mix.lock') }}
restore-keys: |
${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-"
@ -145,7 +145,7 @@ jobs:
path: |
deps
_build
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_5-${{ hashFiles('mix.lock') }}
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_6-${{ hashFiles('mix.lock') }}
restore-keys: |
${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-"
@ -185,7 +185,7 @@ jobs:
path: |
deps
_build
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_5-${{ hashFiles('mix.lock') }}
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_6-${{ hashFiles('mix.lock') }}
restore-keys: |
${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-"
@ -211,7 +211,7 @@ jobs:
path: |
deps
_build
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_5-${{ hashFiles('mix.lock') }}
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_6-${{ hashFiles('mix.lock') }}
restore-keys: |
${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-"
@ -239,7 +239,7 @@ jobs:
path: |
deps
_build
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_5-${{ hashFiles('mix.lock') }}
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_6-${{ hashFiles('mix.lock') }}
restore-keys: |
${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-"
@ -288,7 +288,7 @@ jobs:
path: |
deps
_build
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_5-${{ hashFiles('mix.lock') }}
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_6-${{ hashFiles('mix.lock') }}
restore-keys: |
${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-"
@ -350,7 +350,7 @@ jobs:
path: |
deps
_build
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_5-${{ hashFiles('mix.lock') }}
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_6-${{ hashFiles('mix.lock') }}
restore-keys: |
${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-"
@ -406,7 +406,7 @@ jobs:
path: |
deps
_build
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_5-${{ hashFiles('mix.lock') }}
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_6-${{ hashFiles('mix.lock') }}
restore-keys: |
${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-"
@ -473,7 +473,7 @@ jobs:
path: |
deps
_build
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_5-${{ hashFiles('mix.lock') }}
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_6-${{ hashFiles('mix.lock') }}
restore-keys: |
${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-"
@ -534,7 +534,7 @@ jobs:
path: |
deps
_build
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_5-${{ hashFiles('mix.lock') }}
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_6-${{ hashFiles('mix.lock') }}
restore-keys: |
${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-"

@ -1,6 +1,7 @@
## Current
### Features
- [#5004](https://github.com/blockscout/blockscout/pull/5004) - Add ability to set up a separate DB endpoint for the API endpoints
- [#4989](https://github.com/blockscout/blockscout/pull/4989), [#4991](https://github.com/blockscout/blockscout/pull/4991) - Bridged tokens list API endpoint
- [#4931](https://github.com/blockscout/blockscout/pull/4931) - Web3 modal with Wallet Connect for Write contract page and Staking Dapp

@ -4,6 +4,7 @@ defmodule BlockScoutWeb.API.RPC.AddressController do
alias BlockScoutWeb.API.RPC.Helpers
alias Explorer.{Chain, Etherscan}
alias Explorer.Chain.{Address, Wei}
alias Explorer.Etherscan.{Addresses, Blocks}
alias Indexer.Fetcher.CoinBalanceOnDemand
def listaccounts(conn, params) do
@ -24,7 +25,7 @@ defmodule BlockScoutWeb.API.RPC.AddressController do
with {:address_param, {:ok, address_param}} <- fetch_address(params),
{:block_param, {:ok, block}} <- {:block_param, fetch_block_param(params)},
{:format, {:ok, address_hash}} <- to_address_hash(address_param),
{:balance, {:ok, balance}} <- {:balance, Chain.get_balance_as_of_block(address_hash, block)} do
{:balance, {:ok, balance}} <- {:balance, Blocks.get_balance_as_of_block(address_hash, block)} do
render(conn, :eth_get_balance, %{balance: Wei.hex_format(balance)})
else
{:address_param, :error} ->
@ -347,7 +348,7 @@ defmodule BlockScoutWeb.API.RPC.AddressController do
# limit is just page_size
offset
|> Chain.list_ordered_addresses(page_size)
|> Addresses.list_ordered_addresses(page_size)
|> trigger_balances_and_add_status()
end

@ -25,11 +25,13 @@ defmodule BlockScoutWeb.API.RPC.BlockController do
end
def getblocknobytime(conn, params) do
from_api = true
with {:timestamp_param, {:ok, unsafe_timestamp}} <- {:timestamp_param, Map.fetch(params, "timestamp")},
{:closest_param, {:ok, unsafe_closest}} <- {:closest_param, Map.fetch(params, "closest")},
{:ok, timestamp} <- ChainWeb.param_to_block_timestamp(unsafe_timestamp),
{:ok, closest} <- ChainWeb.param_to_block_closest(unsafe_closest),
{:ok, block_number} <- Chain.timestamp_to_block_number(timestamp, closest) do
{:ok, block_number} <- Chain.timestamp_to_block_number(timestamp, closest, from_api) do
render(conn, block_number: block_number)
else
{:timestamp_param, :error} ->

@ -6,6 +6,7 @@ defmodule BlockScoutWeb.API.RPC.ContractController do
alias Explorer.Chain
alias Explorer.Chain.Events.Publisher, as: EventsPublisher
alias Explorer.Chain.{Hash, SmartContract}
alias Explorer.Etherscan.Contracts
alias Explorer.SmartContract.Solidity.Publisher
alias Explorer.SmartContract.Vyper.Publisher, as: VyperPublisher
alias Explorer.ThirdPartyIntegrations.Sourcify
@ -17,7 +18,7 @@ defmodule BlockScoutWeb.API.RPC.ContractController do
{:params, fetch_external_libraries(params)},
{:publish, {:ok, _}} <-
{:publish, Publisher.publish(address_hash, fetched_params, external_libraries)} do
address = Chain.address_hash_to_address_with_source_code(casted_address_hash)
address = Contracts.address_hash_to_address_with_source_code(casted_address_hash)
render(conn, :verify, %{contract: address})
else
@ -163,7 +164,7 @@ defmodule BlockScoutWeb.API.RPC.ContractController do
}) do
{:ok, _contract} ->
{:format, {:ok, address_hash}} = to_address_hash(address_hash_string)
address = Chain.address_hash_to_address_with_source_code(address_hash)
address = Contracts.address_hash_to_address_with_source_code(address_hash)
render(conn, :verify, %{contract: address})
{:error, changeset} ->
@ -202,7 +203,7 @@ defmodule BlockScoutWeb.API.RPC.ContractController do
{:format, {:ok, casted_address_hash}} <- to_address_hash(address_hash),
{:publish, {:ok, _}} <-
{:publish, VyperPublisher.publish(address_hash, fetched_params)} do
address = Chain.address_hash_to_address_with_source_code(casted_address_hash)
address = Contracts.address_hash_to_address_with_source_code(casted_address_hash)
render(conn, :verify, %{contract: address})
else
@ -320,7 +321,7 @@ defmodule BlockScoutWeb.API.RPC.ContractController do
with {:address_param, {:ok, address_param}} <- fetch_address(params),
{:format, {:ok, address_hash}} <- to_address_hash(address_param) do
_ = VerificationController.check_and_verify(address_param)
address = Chain.address_hash_to_address_with_source_code(address_hash)
address = Contracts.address_hash_to_address_with_source_code(address_hash)
render(conn, :getsourcecode, %{
contract: address
@ -342,23 +343,23 @@ defmodule BlockScoutWeb.API.RPC.ContractController do
case Map.get(opts, :filter) do
:verified ->
Chain.list_verified_contracts(page_size, offset)
Contracts.list_verified_contracts(page_size, offset)
:decompiled ->
not_decompiled_with_version = Map.get(opts, :not_decompiled_with_version)
Chain.list_decompiled_contracts(page_size, offset, not_decompiled_with_version)
Contracts.list_decompiled_contracts(page_size, offset, not_decompiled_with_version)
:unverified ->
Chain.list_unordered_unverified_contracts(page_size, offset)
Contracts.list_unordered_unverified_contracts(page_size, offset)
:not_decompiled ->
Chain.list_unordered_not_decompiled_contracts(page_size, offset)
Contracts.list_unordered_not_decompiled_contracts(page_size, offset)
:empty ->
Chain.list_empty_contracts(page_size, offset)
Contracts.list_empty_contracts(page_size, offset)
_ ->
Chain.list_contracts(page_size, offset)
Contracts.list_contracts(page_size, offset)
end
end

@ -3,7 +3,7 @@ defmodule BlockScoutWeb.API.RPC.StatsController do
use Explorer.Schema
alias Explorer.{Chain, ExchangeRates}
alias Explorer.{Chain, Etherscan, ExchangeRates}
alias Explorer.Chain.Cache.{AddressSum, AddressSumMinusBurnt}
alias Explorer.Chain.Wei
@ -77,7 +77,7 @@ defmodule BlockScoutWeb.API.RPC.StatsController do
def totalfees(conn, params) do
case Map.fetch(params, "date") do
{:ok, date} ->
case Chain.get_total_fees_per_day(date) do
case Etherscan.get_total_fees_per_day(date) do
{:ok, total_fees} -> render(conn, "totalfees.json", total_fees: total_fees)
{:error, error} -> render(conn, :error, error: error)
end

@ -40,7 +40,8 @@ defmodule BlockScoutWeb.API.RPC.TokenController do
}
]
token_holders = Chain.fetch_token_holders_from_token_hash(address_hash, options)
from_api = true
token_holders = Chain.fetch_token_holders_from_token_hash(address_hash, from_api, options)
render(conn, "gettokenholders.json", %{token_holders: token_holders})
else
{:contractaddress_param, :error} ->
@ -73,7 +74,8 @@ defmodule BlockScoutWeb.API.RPC.TokenController do
}
]
bridged_tokens = Chain.list_top_bridged_tokens(destination, nil, options)
from_api = true
bridged_tokens = Chain.list_top_bridged_tokens(destination, nil, from_api, options)
render(conn, "bridgedtokenlist.json", %{bridged_tokens: bridged_tokens})
end

@ -13,7 +13,8 @@ defmodule BlockScoutWeb.API.RPC.TransactionController do
{:transaction, {:ok, %Transaction{revert_reason: revert_reason, error: error} = transaction}} <-
transaction_from_hash(transaction_hash),
paging_options <- paging_options(params) do
logs = Chain.transaction_to_logs(transaction_hash, paging_options)
from_api = true
logs = Chain.transaction_to_logs(transaction_hash, from_api, paging_options)
{logs, next_page} = split_list_by_page(logs)
transaction_updated =

@ -62,7 +62,8 @@ defmodule BlockScoutWeb.BridgedTokensController do
params
|> paging_options()
tokens = Chain.list_top_bridged_tokens(destination, filter, paging_params)
from_api = false
tokens = Chain.list_top_bridged_tokens(destination, filter, from_api, paging_params)
{tokens_page, next_page} = split_list_by_page(tokens)

@ -15,9 +15,11 @@ defmodule BlockScoutWeb.Tokens.HolderController do
]
def index(conn, %{"token_id" => address_hash_string, "type" => "JSON"} = params) do
from_api = false
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, token} <- Chain.token_from_address_hash(address_hash),
token_balances <- Chain.fetch_token_holders_from_token_hash(address_hash, paging_options(params)),
token_balances <- Chain.fetch_token_holders_from_token_hash(address_hash, from_api, paging_options(params)),
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params) do
{token_balances_paginated, next_page} = split_list_by_page(token_balances)

@ -26,7 +26,8 @@ defmodule BlockScoutWeb.TransactionLogController do
paging_options(params)
)
logs_plus_one = Chain.transaction_to_logs(transaction_hash, full_options)
from_api = false
logs_plus_one = Chain.transaction_to_logs(transaction_hash, from_api, full_options)
{logs, next_page} = split_list_by_page(logs_plus_one)

@ -3,6 +3,11 @@ use Mix.Config
database = if System.get_env("DATABASE_URL"), do: nil, else: "explorer_dev"
hostname = if System.get_env("DATABASE_URL"), do: nil, else: "localhost"
database_api_url =
if System.get_env("DATABASE_READ_ONLY_API_URL"),
do: System.get_env("DATABASE_READ_ONLY_API_URL"),
else: System.get_env("DATABASE_URL")
# Configure your database
config :explorer, Explorer.Repo,
database: database,
@ -11,6 +16,17 @@ config :explorer, Explorer.Repo,
pool_size: String.to_integer(System.get_env("POOL_SIZE", "50")),
timeout: :timer.seconds(80)
database_api = if System.get_env("DATABASE_READ_ONLY_API_URL"), do: nil, else: database
hostname_api = if System.get_env("DATABASE_READ_ONLY_API_URL"), do: nil, else: hostname
# Configure API database
config :explorer, Explorer.Repo.Replica1,
database: database_api,
hostname: hostname_api,
url: database_api_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE_API", "50")),
timeout: :timer.seconds(80)
config :explorer, Explorer.Tracer, env: "dev", disabled?: true
config :logger, :explorer,

@ -8,6 +8,19 @@ config :explorer, Explorer.Repo,
prepare: :unnamed,
timeout: :timer.seconds(60)
database_api_url =
if System.get_env("DATABASE_READ_ONLY_API_URL"),
do: System.get_env("DATABASE_READ_ONLY_API_URL"),
else: System.get_env("DATABASE_URL")
# Configures API the database
config :explorer, Explorer.Repo.Replica1,
url: database_api_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE_API", "50")),
ssl: String.equivalent?(System.get_env("ECTO_USE_SSL") || "true", "true"),
prepare: :unnamed,
timeout: :timer.seconds(60)
config :explorer, Explorer.Tracer, env: "production", disabled?: true
config :logger, :explorer,

@ -13,6 +13,16 @@ config :explorer, Explorer.Repo,
timeout: :timer.seconds(60),
queue_target: 1000
# Configure API database
config :explorer, Explorer.Repo.Replica1,
database: "explorer_test",
hostname: "localhost",
pool: Ecto.Adapters.SQL.Sandbox,
# Default of `5_000` was too low for `BlockFetcher` test
ownership_timeout: :timer.minutes(1),
timeout: :timer.seconds(60),
queue_target: 1000
config :explorer, Explorer.ExchangeRates, enabled: false, store: :ets
config :explorer, Explorer.Chain.Cache.BlockNumber, enabled: false

@ -41,6 +41,7 @@ defmodule Explorer.Application do
# Children to start in all environments
base_children = [
Explorer.Repo,
Explorer.Repo.Replica1,
Supervisor.child_spec({SpandexDatadog.ApiServer, datadog_opts()}, id: SpandexDatadog.ApiServer),
Supervisor.child_spec({Task.Supervisor, name: Explorer.HistoryTaskSupervisor}, id: Explorer.HistoryTaskSupervisor),
Supervisor.child_spec({Task.Supervisor, name: Explorer.MarketTaskSupervisor}, id: Explorer.MarketTaskSupervisor),

@ -80,7 +80,6 @@ defmodule Explorer.Chain do
alias Explorer.Chain.Import.Runner
alias Explorer.Chain.InternalTransaction.{CallType, Type}
alias Explorer.Chain.Transaction.History.TransactionStats
alias Explorer.Counters.{AddressesCounter, AddressesWithBalanceCounter}
alias Explorer.Market.MarketHistoryCache
alias Explorer.{PagingOptions, Repo}
@ -1659,99 +1658,6 @@ defmodule Explorer.Chain do
Repo.all(query)
end
@doc """
Returns the balance of the given address and block combination.
Returns `{:error, :not_found}` if there is no address by that hash present.
Returns `{:error, :no_balance}` if there is no balance for that address at that block.
"""
@spec get_balance_as_of_block(Hash.Address.t(), Block.block_number() | :earliest | :latest | :pending) ::
{:ok, Wei.t()} | {:error, :no_balance} | {:error, :not_found}
def get_balance_as_of_block(address, block) when is_integer(block) do
coin_balance_query =
from(coin_balance in CoinBalance,
where: coin_balance.address_hash == ^address,
where: not is_nil(coin_balance.value),
where: coin_balance.block_number <= ^block,
order_by: [desc: coin_balance.block_number],
limit: 1,
select: coin_balance.value
)
case Repo.one(coin_balance_query) do
nil -> {:error, :not_found}
coin_balance -> {:ok, coin_balance}
end
end
def get_balance_as_of_block(address, :latest) do
case max_consensus_block_number() do
{:ok, latest_block_number} ->
get_balance_as_of_block(address, latest_block_number)
{:error, :not_found} ->
{:error, :not_found}
end
end
def get_balance_as_of_block(address, :earliest) do
query =
from(coin_balance in CoinBalance,
where: coin_balance.address_hash == ^address,
where: not is_nil(coin_balance.value),
where: coin_balance.block_number == 0,
limit: 1,
select: coin_balance.value
)
case Repo.one(query) do
nil -> {:error, :not_found}
coin_balance -> {:ok, coin_balance}
end
end
def get_balance_as_of_block(address, :pending) do
query =
case max_consensus_block_number() do
{:ok, latest_block_number} ->
from(coin_balance in CoinBalance,
where: coin_balance.address_hash == ^address,
where: not is_nil(coin_balance.value),
where: coin_balance.block_number > ^latest_block_number,
order_by: [desc: coin_balance.block_number],
limit: 1,
select: coin_balance.value
)
{:error, :not_found} ->
from(coin_balance in CoinBalance,
where: coin_balance.address_hash == ^address,
where: not is_nil(coin_balance.value),
order_by: [desc: coin_balance.block_number],
limit: 1,
select: coin_balance.value
)
end
case Repo.one(query) do
nil -> {:error, :not_found}
coin_balance -> {:ok, coin_balance}
end
end
@spec list_ordered_addresses(non_neg_integer(), non_neg_integer()) :: [Address.t()]
def list_ordered_addresses(offset, limit) do
query =
from(
address in Address,
order_by: [asc: address.inserted_at],
offset: ^offset,
limit: ^limit
)
Repo.all(query)
end
@doc """
Finds an `t:Explorer.Chain.Address.t/0` that has the provided `t:Explorer.Chain.Address.t/0` `hash` and a contract.
@ -2094,33 +2000,6 @@ defmodule Explorer.Chain do
Repo.get(Block, block_hash)
end
@spec fetch_sum_coin_total_supply_minus_burnt() :: non_neg_integer
def fetch_sum_coin_total_supply_minus_burnt do
{:ok, burn_address_hash} = Chain.string_to_address_hash(@burn_address_hash_str)
query =
from(
a0 in Address,
select: fragment("SUM(a0.fetched_coin_balance)"),
where: a0.hash != ^burn_address_hash,
where: a0.fetched_coin_balance > ^0
)
Repo.one!(query, timeout: :infinity) || 0
end
@spec fetch_sum_coin_total_supply() :: non_neg_integer
def fetch_sum_coin_total_supply do
query =
from(
a0 in Address,
select: fragment("SUM(a0.fetched_coin_balance)"),
where: a0.fetched_coin_balance > ^0
)
Repo.one!(query, timeout: :infinity) || 0
end
@spec fetch_sum_gas_used() :: non_neg_integer
def fetch_sum_gas_used do
query =
@ -2306,13 +2185,14 @@ defmodule Explorer.Chain do
fetch_top_tokens(filter, paging_options)
end
@spec list_top_bridged_tokens(atom(), String.t() | nil, [paging_options | necessity_by_association_option]) :: [
{Token.t(), non_neg_integer()}
]
def list_top_bridged_tokens(destination, filter, options \\ []) do
@spec list_top_bridged_tokens(atom(), String.t() | nil, boolean(), [paging_options | necessity_by_association_option]) ::
[
{Token.t(), BridgedToken.t()}
]
def list_top_bridged_tokens(destination, filter, from_api, options \\ []) do
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
fetch_top_bridged_tokens(destination, paging_options, filter)
fetch_top_bridged_tokens(destination, paging_options, filter, from_api)
end
defp fetch_top_tokens(filter, paging_options) do
@ -2340,7 +2220,7 @@ defmodule Explorer.Chain do
|> Repo.all()
end
defp fetch_top_bridged_tokens(destination, paging_options, filter) do
defp fetch_top_bridged_tokens(destination, paging_options, filter, from_api) do
offset = (max(paging_options.page_number, 1) - 1) * paging_options.page_size
chain_id = translate_destination_to_chain_id(destination)
@ -2384,8 +2264,13 @@ defmodule Explorer.Chain do
base_query_with_paging
end
query
|> Repo.all()
if from_api do
query
|> Repo.replica().all()
else
query
|> Repo.all()
end
end
end
@ -2885,32 +2770,6 @@ defmodule Explorer.Chain do
end
end
@spec max_non_consensus_block_number(integer | nil) :: {:ok, Block.block_number()} | {:error, :not_found}
def max_non_consensus_block_number(max_consensus_block_number \\ nil) do
max =
if max_consensus_block_number do
{:ok, max_consensus_block_number}
else
max_consensus_block_number()
end
case max do
{:ok, number} ->
query =
from(block in Block,
where: block.consensus == false,
where: block.number > ^number
)
query
|> Repo.aggregate(:max, :number)
|> case do
nil -> {:error, :not_found}
number -> {:ok, number}
end
end
end
@spec block_height() :: block_height()
def block_height do
query = from(block in Block, select: coalesce(max(block.number), 0), where: block.consensus == true)
@ -3170,8 +3029,9 @@ defmodule Explorer.Chain do
end
end
@spec timestamp_to_block_number(DateTime.t(), :before | :after) :: {:ok, Block.block_number()} | {:error, :not_found}
def timestamp_to_block_number(given_timestamp, closest) do
@spec timestamp_to_block_number(DateTime.t(), :before | :after, boolean()) ::
{:ok, Block.block_number()} | {:error, :not_found}
def timestamp_to_block_number(given_timestamp, closest, from_api) do
{:ok, t} = Timex.format(given_timestamp, "%Y-%m-%d %H:%M:%S", :strftime)
inner_query =
@ -3193,8 +3053,16 @@ defmodule Explorer.Chain do
limit: 1
)
query
|> Repo.one()
response =
if from_api do
query
|> Repo.replica().one()
else
query
|> Repo.one()
end
response
|> case do
nil ->
{:error, :not_found}
@ -3564,8 +3432,8 @@ defmodule Explorer.Chain do
the `index` that are passed.
"""
@spec transaction_to_logs(Hash.Full.t(), [paging_options | necessity_by_association_option]) :: [Log.t()]
def transaction_to_logs(transaction_hash, options \\ []) when is_list(options) do
@spec transaction_to_logs(Hash.Full.t(), boolean(), [paging_options | necessity_by_association_option]) :: [Log.t()]
def transaction_to_logs(transaction_hash, from_api, options \\ []) when is_list(options) do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
@ -3577,13 +3445,21 @@ defmodule Explorer.Chain do
transaction.hash == log.transaction_hash
)
log_with_transactions
|> where([_, transaction], transaction.hash == ^transaction_hash)
|> page_logs(paging_options)
|> limit(^paging_options.page_size)
|> order_by([log], asc: log.index)
|> join_associations(necessity_by_association)
|> Repo.all()
query =
log_with_transactions
|> where([_, transaction], transaction.hash == ^transaction_hash)
|> page_logs(paging_options)
|> limit(^paging_options.page_size)
|> order_by([log], asc: log.index)
|> join_associations(necessity_by_association)
if from_api do
query
|> Repo.replica().all()
else
query
|> Repo.all()
end
end
@doc """
@ -4112,44 +3988,6 @@ defmodule Explorer.Chain do
|> repo.insert(on_conflict: :nothing, conflict_target: [:address_hash, :name])
end
@spec address_hash_to_address_with_source_code(Hash.Address.t()) :: Address.t() | nil
def address_hash_to_address_with_source_code(address_hash) do
case Repo.get(Address, address_hash) do
nil ->
nil
address ->
address_with_smart_contract =
Repo.preload(address, [:smart_contract, :decompiled_smart_contracts, :smart_contract_additional_sources])
if address_with_smart_contract.smart_contract do
formatted_code = format_source_code_output(address_with_smart_contract.smart_contract)
%{
address_with_smart_contract
| smart_contract: %{address_with_smart_contract.smart_contract | contract_source_code: formatted_code}
}
else
address_verified_twin_contract =
Chain.get_minimal_proxy_template(address_hash) ||
Chain.get_address_verified_twin_contract(address_hash).verified_contract
if address_verified_twin_contract do
formatted_code = format_source_code_output(address_verified_twin_contract)
%{
address_with_smart_contract
| smart_contract: %{address_verified_twin_contract | contract_source_code: formatted_code}
}
else
address_with_smart_contract
end
end
end
end
defp format_source_code_output(smart_contract), do: smart_contract.contract_source_code
@doc """
Finds metadata for verification of a contract from verified twins: contracts with the same bytecode
which were verified previously, returns a single t:SmartContract.t/0
@ -5884,11 +5722,19 @@ defmodule Explorer.Chain do
end
end
@spec fetch_token_holders_from_token_hash(Hash.Address.t(), [paging_options]) :: [TokenBalance.t()]
def fetch_token_holders_from_token_hash(contract_address_hash, options \\ []) do
contract_address_hash
|> CurrentTokenBalance.token_holders_ordered_by_value(options)
|> Repo.all()
@spec fetch_token_holders_from_token_hash(Hash.Address.t(), boolean(), [paging_options]) :: [TokenBalance.t()]
def fetch_token_holders_from_token_hash(contract_address_hash, from_api, options \\ []) do
query =
contract_address_hash
|> CurrentTokenBalance.token_holders_ordered_by_value(options)
if from_api do
query
|> Repo.replica().all()
else
query
|> Repo.all()
end
end
def fetch_token_holders_from_token_hash_and_token_id(contract_address_hash, token_id, options \\ []) do
@ -5939,24 +5785,6 @@ defmodule Explorer.Chain do
@spec data() :: Dataloader.Ecto.t()
def data, do: DataloaderEcto.new(Repo)
def list_decompiled_contracts(limit, offset, not_decompiled_with_version \\ nil) do
query =
from(
address in Address,
where: address.contract_code != <<>>,
where: not is_nil(address.contract_code),
where: address.decompiled == true,
limit: ^limit,
offset: ^offset,
order_by: [asc: address.inserted_at],
preload: [:smart_contract]
)
query
|> reject_decompiled_with_version(not_decompiled_with_version)
|> Repo.all()
end
@spec transaction_token_transfer_type(Transaction.t()) ::
:erc20 | :erc721 | :erc1155 | :token_transfer | nil
def transaction_token_transfer_type(
@ -6073,98 +5901,6 @@ defmodule Explorer.Chain do
end
end
defp reject_decompiled_with_version(query, nil), do: query
defp reject_decompiled_with_version(query, reject_version) do
from(
address in query,
left_join: decompiled_smart_contract in assoc(address, :decompiled_smart_contracts),
on: decompiled_smart_contract.decompiler_version == ^reject_version,
where: is_nil(decompiled_smart_contract.address_hash)
)
end
def list_verified_contracts(limit, offset) do
query =
from(
smart_contract in SmartContract,
order_by: [asc: smart_contract.inserted_at],
limit: ^limit,
offset: ^offset,
preload: [:address]
)
query
|> Repo.all()
|> Enum.map(fn smart_contract ->
Map.put(smart_contract.address, :smart_contract, smart_contract)
end)
end
def list_contracts(limit, offset) do
query =
from(
address in Address,
where: not is_nil(address.contract_code),
preload: [:smart_contract],
order_by: [asc: address.inserted_at],
limit: ^limit,
offset: ^offset
)
Repo.all(query)
end
def list_unordered_unverified_contracts(limit, offset) do
query =
from(
address in Address,
where: address.contract_code != <<>>,
where: not is_nil(address.contract_code),
where: fragment("? IS NOT TRUE", address.verified),
limit: ^limit,
offset: ^offset
)
query
|> Repo.all()
|> Enum.map(fn address ->
%{address | smart_contract: nil}
end)
end
def list_empty_contracts(limit, offset) do
query =
from(address in Address,
where: address.contract_code == <<>>,
preload: [:smart_contract, :decompiled_smart_contracts],
order_by: [asc: address.inserted_at],
limit: ^limit,
offset: ^offset
)
Repo.all(query)
end
def list_unordered_not_decompiled_contracts(limit, offset) do
query =
from(
address in Address,
where: fragment("? IS NOT TRUE", address.verified),
where: fragment("? IS NOT TRUE", address.decompiled),
where: address.contract_code != <<>>,
where: not is_nil(address.contract_code),
limit: ^limit,
offset: ^offset
)
query
|> Repo.all()
|> Enum.map(fn address ->
%{address | smart_contract: nil}
end)
end
@doc """
Combined block reward from all the fees.
"""
@ -7186,28 +6922,6 @@ defmodule Explorer.Chain do
end
end
@doc """
It is used by `totalfees` API endpoint of `stats` module for retrieving of total fee per day
"""
@spec get_total_fees_per_day(String.t()) :: {:ok, non_neg_integer() | nil} | {:error, String.t()}
def get_total_fees_per_day(date_string) do
case Date.from_iso8601(date_string) do
{:ok, date} ->
query =
from(
tx_stats in TransactionStats,
where: tx_stats.date == ^date,
select: tx_stats.total_fee
)
total_fees = Repo.one(query)
{:ok, total_fees}
_ ->
{:error, "An incorrect input date provided. It should be in ISO 8601 format (yyyy-mm-dd)."}
end
end
@spec get_token_transfer_type(TokenTransfer.t()) ::
:token_burning | :token_minting | :token_spawning | :token_transfer
def get_token_transfer_type(transfer) do

@ -13,7 +13,7 @@ defmodule Explorer.Chain.Cache.AddressSum do
global_ttl: Application.get_env(:explorer, __MODULE__)[:global_ttl],
callback: &async_task_on_deletion(&1)
alias Explorer.Chain
alias Explorer.Etherscan
defp handle_fallback(:sum) do
# This will get the task PID if one exists and launch a new task if not
@ -29,7 +29,7 @@ defmodule Explorer.Chain.Cache.AddressSum do
{:ok, task} =
Task.start(fn ->
try do
result = Chain.fetch_sum_coin_total_supply()
result = Etherscan.fetch_sum_coin_total_supply()
set_sum(result)
rescue

@ -13,7 +13,7 @@ defmodule Explorer.Chain.Cache.AddressSumMinusBurnt do
global_ttl: Application.get_env(:explorer, __MODULE__)[:global_ttl],
callback: &async_task_on_deletion(&1)
alias Explorer.Chain
alias Explorer.{Chain, Etherscan}
defp handle_fallback(:sum_minus_burnt) do
# This will get the task PID if one exists and launch a new task if not
@ -29,7 +29,7 @@ defmodule Explorer.Chain.Cache.AddressSumMinusBurnt do
{:ok, task} =
Task.start(fn ->
try do
result = Chain.fetch_sum_coin_total_supply_minus_burnt()
result = Etherscan.fetch_sum_coin_total_supply_minus_burnt()
params = %{
counter_type: "sum_coin_total_supply_minus_burnt",

@ -168,7 +168,7 @@ defmodule Explorer.Chain.Transaction do
uncles: %Ecto.Association.NotLoaded{} | [Block.t()],
v: v(),
value: Wei.t(),
revert_reason: String.t(),
revert_reason: String.t() | nil,
max_priority_fee_per_gas: wei_per_gas | nil,
max_fee_per_gas: wei_per_gas | nil,
type: non_neg_integer() | nil

@ -35,8 +35,10 @@ defmodule Explorer.Chain.Transaction.History.Historian do
Logger.info("tx/per day chart: latest date #{DateTime.to_string(latest)}")
with {:ok, min_block} <- Chain.timestamp_to_block_number(earliest, :after),
{:ok, max_block} <- Chain.timestamp_to_block_number(latest, :after) do
from_api = false
with {:ok, min_block} <- Chain.timestamp_to_block_number(earliest, :after, from_api),
{:ok, max_block} <- Chain.timestamp_to_block_number(latest, :after, from_api) do
record =
min_block
|> compile_records_in_range(max_block)

@ -1,13 +1,13 @@
defmodule Explorer.EthRPC do
@moduledoc """
Ethreum JSON RPC methods logic implementation.
Ethereum JSON RPC methods logic implementation.
"""
alias Ecto.Type, as: EctoType
alias Explorer.{Chain, Repo}
alias Explorer.Chain.{Block, Data, Hash, Hash.Address, Wei}
alias Explorer.Chain.Cache.BlockNumber
alias Explorer.Etherscan.Logs
alias Explorer.Etherscan.{Blocks, Logs, RPC}
@methods %{
"eth_blockNumber" => %{
@ -119,7 +119,7 @@ defmodule Explorer.EthRPC do
def eth_get_balance(address_param, block_param \\ nil) do
with {:address, {:ok, address}} <- {:address, Chain.string_to_address_hash(address_param)},
{:block, {:ok, block}} <- {:block, block_param(block_param)},
{:balance, {:ok, balance}} <- {:balance, Chain.get_balance_as_of_block(address, block)} do
{:balance, {:ok, balance}} <- {:balance, Blocks.get_balance_as_of_block(address, block)} do
{:ok, Wei.hex_format(balance)}
else
{:address, :error} ->
@ -262,7 +262,7 @@ defmodule Explorer.EthRPC do
defp logs_blocks_filter(filter_options) do
with {:filter, %{"blockHash" => block_hash_param}} <- {:filter, filter_options},
{:block_hash, {:ok, block_hash}} <- {:block_hash, Hash.Full.cast(block_hash_param)},
{:block, %{number: number}} <- {:block, Repo.get(Block, block_hash)} do
{:block, %{number: number}} <- {:block, Repo.replica().get(Block, block_hash)} do
{:ok, number, number}
else
{:filter, filters} ->
@ -356,7 +356,7 @@ defmodule Explorer.EthRPC do
defp to_number(_, error_message), do: {:error, error_message}
defp max_non_consensus_block_number(max) do
case Chain.max_non_consensus_block_number(max) do
case RPC.max_non_consensus_block_number(max) do
{:ok, number} -> number
_ -> nil
end

@ -8,7 +8,8 @@ defmodule Explorer.Etherscan do
alias Explorer.Etherscan.Logs
alias Explorer.{Chain, Repo}
alias Explorer.Chain.Address.{CurrentTokenBalance, TokenBalance}
alias Explorer.Chain.{Block, Hash, InternalTransaction, TokenTransfer, Transaction}
alias Explorer.Chain.{Address, Block, Hash, InternalTransaction, TokenTransfer, Transaction}
alias Explorer.Chain.Transaction.History.TransactionStats
@default_options %{
order_by_direction: :desc,
@ -20,6 +21,8 @@ defmodule Explorer.Etherscan do
end_timestamp: nil
}
@burn_address_hash_str "0x0000000000000000000000000000000000000000"
@doc """
Returns the maximum allowed page size number.
@ -115,7 +118,7 @@ defmodule Explorer.Etherscan do
|> Chain.where_transaction_has_multiple_internal_transactions()
|> InternalTransaction.where_is_different_from_parent_transaction()
|> InternalTransaction.where_nonpending_block()
|> Repo.all()
|> Repo.replica().all()
end
@doc """
@ -198,7 +201,7 @@ defmodule Explorer.Etherscan do
query_to_address_hash_wrapped
|> union(^query_from_address_hash_wrapped)
|> union(^query_created_contract_address_hash_wrapped)
|> Repo.all()
|> Repo.replica().all()
else
query =
from(
@ -222,7 +225,7 @@ defmodule Explorer.Etherscan do
|> where_start_block_match(options)
|> where_end_block_match(options)
|> InternalTransaction.where_nonpending_block()
|> Repo.all()
|> Repo.replica().all()
end
end
@ -279,7 +282,7 @@ defmodule Explorer.Etherscan do
}
)
Repo.all(query)
Repo.replica().all(query)
end
@doc """
@ -300,7 +303,7 @@ defmodule Explorer.Etherscan do
select: ctb
)
Repo.one(query)
Repo.replica().one(query)
end
@doc """
@ -326,7 +329,7 @@ defmodule Explorer.Etherscan do
}
)
Repo.all(query)
Repo.replica().all(query)
end
@transaction_fields ~w(
@ -377,7 +380,7 @@ defmodule Explorer.Etherscan do
|> where_address_match(address_hash, options)
|> Chain.pending_transactions_query()
|> order_by([transaction], desc: transaction.inserted_at, desc: transaction.hash)
|> Repo.all()
|> Repo.replica().all()
end
defp list_transactions(address_hash, max_block_number, options) do
@ -401,7 +404,7 @@ defmodule Explorer.Etherscan do
|> where_end_block_match(options)
|> where_start_timestamp_match(options)
|> where_end_timestamp_match(options)
|> Repo.all()
|> Repo.replica().all()
end
defp where_address_match(query, address_hash, %{filter_by: "to"}) do
@ -490,7 +493,7 @@ defmodule Explorer.Etherscan do
wrapped_query
|> where_start_block_match(options)
|> where_end_block_match(options)
|> Repo.all()
|> Repo.replica().all()
end
defp where_start_block_match(query, %{start_block: nil}), do: query
@ -556,4 +559,53 @@ defmodule Explorer.Etherscan do
"""
@spec list_logs(map()) :: [map()]
def list_logs(filter), do: Logs.list_logs(filter)
@spec fetch_sum_coin_total_supply() :: non_neg_integer
def fetch_sum_coin_total_supply do
query =
from(
a0 in Address,
select: fragment("SUM(a0.fetched_coin_balance)"),
where: a0.fetched_coin_balance > ^0
)
Repo.replica().one!(query, timeout: :infinity) || 0
end
@spec fetch_sum_coin_total_supply_minus_burnt() :: non_neg_integer
def fetch_sum_coin_total_supply_minus_burnt do
{:ok, burn_address_hash} = Chain.string_to_address_hash(@burn_address_hash_str)
query =
from(
a0 in Address,
select: fragment("SUM(a0.fetched_coin_balance)"),
where: a0.hash != ^burn_address_hash,
where: a0.fetched_coin_balance > ^0
)
Repo.replica().one!(query, timeout: :infinity) || 0
end
@doc """
It is used by `totalfees` API endpoint of `stats` module for retrieving of total fee per day
"""
@spec get_total_fees_per_day(String.t()) :: {:ok, non_neg_integer() | nil} | {:error, String.t()}
def get_total_fees_per_day(date_string) do
case Date.from_iso8601(date_string) do
{:ok, date} ->
query =
from(
tx_stats in TransactionStats,
where: tx_stats.date == ^date,
select: tx_stats.total_fee
)
total_fees = Repo.replica().one(query)
{:ok, total_fees}
_ ->
{:error, "An incorrect input date provided. It should be in ISO 8601 format (yyyy-mm-dd)."}
end
end
end

@ -0,0 +1,28 @@
defmodule Explorer.Etherscan.Addresses do
@moduledoc """
This module contains functions for working with addresses, as they pertain to the
`Explorer.Etherscan` context.
"""
import Ecto.Query,
only: [
from: 2
]
alias Explorer.Chain.Address
alias Explorer.Repo
@spec list_ordered_addresses(non_neg_integer(), non_neg_integer()) :: [Address.t()]
def list_ordered_addresses(offset, limit) do
query =
from(
address in Address,
order_by: [asc: address.inserted_at],
offset: ^offset,
limit: ^limit
)
Repo.replica().all(query)
end
end

@ -0,0 +1,95 @@
defmodule Explorer.Etherscan.Blocks do
@moduledoc """
This module contains functions for working with blocks, as they pertain to the
`Explorer.Etherscan` context.
"""
import Ecto.Query,
only: [
from: 2
]
alias Explorer.{Chain, Repo}
alias Explorer.Chain.{Address.CoinBalance, Block, Hash, Wei}
@doc """
Returns the balance of the given address and block combination.
Returns `{:error, :not_found}` if there is no address by that hash present.
Returns `{:error, :no_balance}` if there is no balance for that address at that block.
"""
@spec get_balance_as_of_block(Hash.Address.t(), Block.block_number() | :earliest | :latest | :pending) ::
{:ok, Wei.t()} | {:error, :no_balance} | {:error, :not_found}
def get_balance_as_of_block(address, block) when is_integer(block) do
coin_balance_query =
from(coin_balance in CoinBalance,
where: coin_balance.address_hash == ^address,
where: not is_nil(coin_balance.value),
where: coin_balance.block_number <= ^block,
order_by: [desc: coin_balance.block_number],
limit: 1,
select: coin_balance.value
)
case Repo.replica().one(coin_balance_query) do
nil -> {:error, :not_found}
coin_balance -> {:ok, coin_balance}
end
end
def get_balance_as_of_block(address, :latest) do
case Chain.max_consensus_block_number() do
{:ok, latest_block_number} ->
get_balance_as_of_block(address, latest_block_number)
{:error, :not_found} ->
{:error, :not_found}
end
end
def get_balance_as_of_block(address, :earliest) do
query =
from(coin_balance in CoinBalance,
where: coin_balance.address_hash == ^address,
where: not is_nil(coin_balance.value),
where: coin_balance.block_number == 0,
limit: 1,
select: coin_balance.value
)
case Repo.replica().one(query) do
nil -> {:error, :not_found}
coin_balance -> {:ok, coin_balance}
end
end
def get_balance_as_of_block(address, :pending) do
query =
case Chain.max_consensus_block_number() do
{:ok, latest_block_number} ->
from(coin_balance in CoinBalance,
where: coin_balance.address_hash == ^address,
where: not is_nil(coin_balance.value),
where: coin_balance.block_number > ^latest_block_number,
order_by: [desc: coin_balance.block_number],
limit: 1,
select: coin_balance.value
)
{:error, :not_found} ->
from(coin_balance in CoinBalance,
where: coin_balance.address_hash == ^address,
where: not is_nil(coin_balance.value),
order_by: [desc: coin_balance.block_number],
limit: 1,
select: coin_balance.value
)
end
case Repo.replica().one(query) do
nil -> {:error, :not_found}
coin_balance -> {:ok, coin_balance}
end
end
end

@ -0,0 +1,167 @@
defmodule Explorer.Etherscan.Contracts do
@moduledoc """
This module contains functions for working with contracts, as they pertain to the
`Explorer.Etherscan` context.
"""
import Ecto.Query,
only: [
from: 2
]
alias Explorer.{Chain, Repo}
alias Explorer.Chain.{Address, Hash, SmartContract}
@spec address_hash_to_address_with_source_code(Hash.Address.t()) :: Address.t() | nil
def address_hash_to_address_with_source_code(address_hash) do
case Repo.replica().get(Address, address_hash) do
nil ->
nil
address ->
address_with_smart_contract =
Repo.replica().preload(address, [
:smart_contract,
:decompiled_smart_contracts,
:smart_contract_additional_sources
])
if address_with_smart_contract.smart_contract do
formatted_code = format_source_code_output(address_with_smart_contract.smart_contract)
%{
address_with_smart_contract
| smart_contract: %{address_with_smart_contract.smart_contract | contract_source_code: formatted_code}
}
else
address_verified_twin_contract =
Chain.get_minimal_proxy_template(address_hash) ||
Chain.get_address_verified_twin_contract(address_hash).verified_contract
if address_verified_twin_contract do
formatted_code = format_source_code_output(address_verified_twin_contract)
%{
address_with_smart_contract
| smart_contract: %{address_verified_twin_contract | contract_source_code: formatted_code}
}
else
address_with_smart_contract
end
end
end
end
def list_verified_contracts(limit, offset) do
query =
from(
smart_contract in SmartContract,
order_by: [asc: smart_contract.inserted_at],
limit: ^limit,
offset: ^offset,
preload: [:address]
)
query
|> Repo.replica().all()
|> Enum.map(fn smart_contract ->
Map.put(smart_contract.address, :smart_contract, smart_contract)
end)
end
def list_decompiled_contracts(limit, offset, not_decompiled_with_version \\ nil) do
query =
from(
address in Address,
where: address.contract_code != <<>>,
where: not is_nil(address.contract_code),
where: address.decompiled == true,
limit: ^limit,
offset: ^offset,
order_by: [asc: address.inserted_at],
preload: [:smart_contract]
)
query
|> reject_decompiled_with_version(not_decompiled_with_version)
|> Repo.replica().all()
end
def list_unordered_unverified_contracts(limit, offset) do
query =
from(
address in Address,
where: address.contract_code != <<>>,
where: not is_nil(address.contract_code),
where: fragment("? IS NOT TRUE", address.verified),
limit: ^limit,
offset: ^offset
)
query
|> Repo.replica().all()
|> Enum.map(fn address ->
%{address | smart_contract: nil}
end)
end
def list_unordered_not_decompiled_contracts(limit, offset) do
query =
from(
address in Address,
where: fragment("? IS NOT TRUE", address.verified),
where: fragment("? IS NOT TRUE", address.decompiled),
where: address.contract_code != <<>>,
where: not is_nil(address.contract_code),
limit: ^limit,
offset: ^offset
)
query
|> Repo.replica().all()
|> Enum.map(fn address ->
%{address | smart_contract: nil}
end)
end
def list_empty_contracts(limit, offset) do
query =
from(address in Address,
where: address.contract_code == <<>>,
preload: [:smart_contract, :decompiled_smart_contracts],
order_by: [asc: address.inserted_at],
limit: ^limit,
offset: ^offset
)
Repo.replica().all(query)
end
def list_contracts(limit, offset) do
query =
from(
address in Address,
where: not is_nil(address.contract_code),
preload: [:smart_contract],
order_by: [asc: address.inserted_at],
limit: ^limit,
offset: ^offset
)
Repo.replica().all(query)
end
defp format_source_code_output(smart_contract), do: smart_contract.contract_source_code
defp reject_decompiled_with_version(query, nil), do: query
defp reject_decompiled_with_version(query, reject_version) do
from(
address in query,
left_join: decompiled_smart_contract in assoc(address, :decompiled_smart_contracts),
on: decompiled_smart_contract.decompiler_version == ^reject_version,
where: is_nil(decompiled_smart_contract.address_hash)
)
end
end

@ -146,7 +146,7 @@ defmodule Explorer.Etherscan.Logs do
query_with_consensus
|> order_by([log], asc: log.index)
|> page_logs(paging_options)
|> Repo.all()
|> Repo.replica().all()
end
# Since address_hash was not present, we know that a
@ -198,7 +198,7 @@ defmodule Explorer.Etherscan.Logs do
query_with_block_transaction_data
|> order_by([log], asc: log.index)
|> page_logs(paging_options)
|> Repo.all()
|> Repo.replica().all()
end
@topics [

@ -0,0 +1,40 @@
defmodule Explorer.Etherscan.RPC do
@moduledoc """
This module contains functions for working with mimicking of ETH RPC.
"""
import Ecto.Query,
only: [
from: 2
]
alias Explorer.{Chain, Repo}
alias Explorer.Chain.Block
@spec max_non_consensus_block_number(integer | nil) :: {:ok, Block.block_number()} | {:error, :not_found}
def max_non_consensus_block_number(max_consensus_block_number \\ nil) do
max =
if max_consensus_block_number do
{:ok, max_consensus_block_number}
else
Chain.max_consensus_block_number()
end
case max do
{:ok, number} ->
query =
from(block in Block,
where: block.consensus == false,
where: block.number > ^number
)
query
|> Repo.replica().aggregate(:max, :number)
|> case do
nil -> {:error, :not_found}
number -> {:ok, number}
end
end
end
end

@ -40,7 +40,7 @@ defmodule Explorer.GraphQL do
"""
@spec get_internal_transaction(map()) :: {:ok, InternalTransaction.t()} | {:error, String.t()}
def get_internal_transaction(%{transaction_hash: _, index: _} = clauses) do
if internal_transaction = Repo.get_by(InternalTransaction.where_nonpending_block(), clauses) do
if internal_transaction = Repo.replica().get_by(InternalTransaction.where_nonpending_block(), clauses) do
{:ok, internal_transaction}
else
{:error, "Internal transaction not found."}
@ -75,7 +75,7 @@ defmodule Explorer.GraphQL do
"""
@spec get_token_transfer(map()) :: {:ok, TokenTransfer.t()} | {:error, String.t()}
def get_token_transfer(%{transaction_hash: _, log_index: _} = clauses) do
if token_transfer = Repo.get_by(TokenTransfer, clauses) do
if token_transfer = Repo.replica().get_by(TokenTransfer, clauses) do
{:ok, token_transfer}
else
{:error, "Token transfer not found."}

@ -105,4 +105,17 @@ defmodule Explorer.Repo do
def stream_reduce(query, initial, reducer) when is_function(reducer, 2) do
stream_in_transaction(query, &Enum.reduce(&1, initial, reducer))
end
if Mix.env() == :test do
def replica, do: __MODULE__
else
def replica, do: Explorer.Repo.Replica1
end
defmodule Replica1 do
use Ecto.Repo,
otp_app: :explorer,
adapter: Ecto.Adapters.Postgres,
read_only: true
end
end

@ -27,7 +27,7 @@ defmodule Explorer.ChainTest do
Wei
}
alias Explorer.Chain
alias Explorer.{Chain, Etherscan}
alias Explorer.Chain.InternalTransaction.Type
alias Explorer.Chain.Supply.ProofOfAuthority
@ -1413,11 +1413,11 @@ defmodule Explorer.ChainTest do
insert(:address, fetched_coin_balance: index)
end
assert "10" = Decimal.to_string(Chain.fetch_sum_coin_total_supply())
assert "10" = Decimal.to_string(Etherscan.fetch_sum_coin_total_supply())
end
test "fetches coin total supply when there are no blocks" do
assert 0 = Chain.fetch_sum_coin_total_supply()
assert 0 = Etherscan.fetch_sum_coin_total_supply()
end
end
@ -3049,11 +3049,11 @@ defmodule Explorer.ChainTest do
end
end
describe "transaction_to_logs/2" do
describe "transaction_to_logs/3" do
test "without logs" do
transaction = insert(:transaction)
assert [] = Chain.transaction_to_logs(transaction.hash)
assert [] = Chain.transaction_to_logs(transaction.hash, false)
end
test "with logs" do
@ -3065,7 +3065,8 @@ defmodule Explorer.ChainTest do
%Log{transaction_hash: transaction_hash, index: index} =
insert(:log, transaction: transaction, block: transaction.block, block_number: transaction.block_number)
assert [%Log{transaction_hash: ^transaction_hash, index: ^index}] = Chain.transaction_to_logs(transaction.hash)
assert [%Log{transaction_hash: ^transaction_hash, index: ^index}] =
Chain.transaction_to_logs(transaction.hash, false)
end
test "with logs can be paginated" do
@ -3096,7 +3097,7 @@ defmodule Explorer.ChainTest do
assert second_page_indexes ==
transaction.hash
|> Chain.transaction_to_logs(paging_options: %PagingOptions{key: {log.index}, page_size: 50})
|> Chain.transaction_to_logs(false, paging_options: %PagingOptions{key: {log.index}, page_size: 50})
|> Enum.map(& &1.index)
end
@ -3111,6 +3112,7 @@ defmodule Explorer.ChainTest do
assert [%Log{address: %Address{}, transaction: %Transaction{}}] =
Chain.transaction_to_logs(
transaction.hash,
false,
necessity_by_association: %{
address: :optional,
transaction: :optional
@ -3122,7 +3124,7 @@ defmodule Explorer.ChainTest do
address: %Ecto.Association.NotLoaded{},
transaction: %Ecto.Association.NotLoaded{}
}
] = Chain.transaction_to_logs(transaction.hash)
] = Chain.transaction_to_logs(transaction.hash, false)
end
end
@ -4672,7 +4674,7 @@ defmodule Explorer.ChainTest do
end
end
describe "fetch_token_holders_from_token_hash/2" do
describe "fetch_token_holders_from_token_hash/3" do
test "returns the token holders" do
%Token{contract_address_hash: contract_address_hash} = insert(:token)
address_a = insert(:address)
@ -4695,7 +4697,7 @@ defmodule Explorer.ChainTest do
token_holders_count =
contract_address_hash
|> Chain.fetch_token_holders_from_token_hash([])
|> Chain.fetch_token_holders_from_token_hash(false, [])
|> Enum.count()
assert token_holders_count == 2

@ -1248,6 +1248,7 @@ defmodule Explorer.EtherscanTest do
options = %{order_by_direction: :desc}
found_token_transfers = Etherscan.list_token_transfers(address.hash, nil, options)
IO.inspect("Gimme found_token_transfers #{inspect(found_token_transfers)}")
block_numbers_order = Enum.map(found_token_transfers, & &1.block_number)

@ -13,7 +13,7 @@ defmodule Explorer.GraphQLTest do
|> insert()
|> Map.get(:hash)
|> GraphQL.address_to_transactions_query()
|> Repo.all()
|> Repo.replica().all()
assert result == []
end
@ -26,7 +26,7 @@ defmodule Explorer.GraphQLTest do
[found_transaction] =
address_hash
|> GraphQL.address_to_transactions_query()
|> Repo.all()
|> Repo.replica().all()
assert found_transaction.hash == transaction.hash
end
@ -39,7 +39,7 @@ defmodule Explorer.GraphQLTest do
[found_transaction] =
address_hash
|> GraphQL.address_to_transactions_query()
|> Repo.all()
|> Repo.replica().all()
assert found_transaction.hash == transaction.hash
end
@ -52,7 +52,7 @@ defmodule Explorer.GraphQLTest do
[found_transaction] =
address_hash
|> GraphQL.address_to_transactions_query()
|> Repo.all()
|> Repo.replica().all()
assert found_transaction.hash == transaction.hash
end
@ -79,7 +79,7 @@ defmodule Explorer.GraphQLTest do
found_transactions =
address_hash
|> GraphQL.address_to_transactions_query()
|> Repo.all()
|> Repo.replica().all()
block_number_and_index_order =
Enum.map(found_transactions, fn transaction ->
@ -144,7 +144,7 @@ defmodule Explorer.GraphQLTest do
[found_internal_transaction] =
transaction1
|> GraphQL.transaction_to_internal_transactions_query()
|> Repo.all()
|> Repo.replica().all()
assert found_internal_transaction.transaction_hash == transaction1.hash
assert found_internal_transaction.index == internal_transaction.index
@ -173,7 +173,7 @@ defmodule Explorer.GraphQLTest do
found_internal_transactions =
transaction1
|> GraphQL.transaction_to_internal_transactions_query()
|> Repo.all()
|> Repo.replica().all()
assert length(found_internal_transactions) == 3
@ -209,7 +209,7 @@ defmodule Explorer.GraphQLTest do
found_internal_transactions =
transaction
|> GraphQL.transaction_to_internal_transactions_query()
|> Repo.all()
|> Repo.replica().all()
index_order = Enum.map(found_internal_transactions, & &1.index)
@ -259,7 +259,7 @@ defmodule Explorer.GraphQLTest do
|> insert()
|> Map.get(:hash)
|> GraphQL.list_token_transfers_query()
|> Repo.all()
|> Repo.replica().all()
assert result == []
end
@ -271,7 +271,7 @@ defmodule Explorer.GraphQLTest do
[found_token_transfer] =
token_transfer.token_contract_address_hash
|> GraphQL.list_token_transfers_query()
|> Repo.all()
|> Repo.replica().all()
expected_fields = ~w(
amount
@ -336,8 +336,8 @@ defmodule Explorer.GraphQLTest do
found_token_transfers =
token_address.hash
|> GraphQL.list_token_transfers_query()
|> Repo.all()
|> Repo.preload(:transaction)
|> Repo.replica().all()
|> Repo.replica().preload(:transaction)
block_number_order = Enum.map(found_token_transfers, & &1.block_number)

Loading…
Cancel
Save