feat: support for `:celo` chain type (#10564)
* feat: indexing of Celo transactions (#10275) * feat(json-rpc): support celo-specific fields in transactions * chore: add new chain type to matrix builder * feat: add celo-specific fields to transactions schema * chore: add "celo" to `cspell.json` * chore: fix formatting * refactor: improve naming for celo fields and ignore `ethCompatible` field * chore: add block fetcher test for celo fields * feat: add celo core contract abis * feat: add a cache for core celo contracts * feat: treat celo native token transfers as token transfers * fix: update `cspell.json` with celo words * fix(dialyzer): pattern can never match the type * fix: update `cspell.json` with celo words * feat: move `CELO_NETWORK` env to config * feat: parse native Celo token transfers from internal transactions * feat: add token balances fetching * refactor: remove `dbg` and organize code better * fix: start TokenBalance fetcher * fix: start token balance fetcher * fix: typo in log message * fix: missing fields in celo transaction token transfer * refactor: remove `AbiHandler` in favour of hardcoded pieces of abi * refactor: native token transfers from internal transactions fether * feat: implement parsing of token transfers for traceable rpc node * feat: fetch address coin balances after erc20 celo token transfers * feat: add `gas_token` to transaction response in api v2 * feat: index tokens specified as `feeCurrency` * chore: fix credo warnings * chore: decrease log level to debug * chore: fix cspell warnings * refactor: apply suggestions * fix: handle case when `gas_token` is not loaded * dbg * chore: expect contract calls in celo fetcher test * fix: compile errors for celo view on default chain type * chore: remove unused import * refactor: move `celo_gas_tokens` to chain type imports * refactor: apply suggestions * fix: swap arguments in `put_if_present` * fix: indexer tests * refactor: always include `gas_token` to the response * feat: add `gas_token` to all transaction-related endpoints * feat: celo core contracts with historical data * fix: `uncles` field is expected but not present in RPC node response * fix: credo & format * fix: define `CELO_CORE_CONTRACTS` in tests * fix: put `gas_token` under `celo` section * refactor: move token transfers filtering from `TokenTotalSupplyUpdater` to `Transform.TokenTransfers` * feat: add workflow to publish docker image * refactor: apply suggestions by @nikitosing * chore: add docs and specs * fix: malformed import * fix: publish image for celo * fix: ci build for celo * feat: index celo epoch transactions (#9944) * feat(json-rpc): support celo-specific fields in transactions * chore: add new chain type to matrix builder * feat: add celo-specific fields to transactions schema * chore: add "celo" to `cspell.json` * chore: fix formatting * refactor: improve naming for celo fields and ignore `ethCompatible` field * chore: add block fetcher test for celo fields * feat: add celo core contract abis * feat: add a cache for core celo contracts * feat: treat celo native token transfers as token transfers * fix: update `cspell.json` with celo words * fix(dialyzer): pattern can never match the type * fix: update `cspell.json` with celo words * feat: move `CELO_NETWORK` env to config * feat: parse native Celo token transfers from internal transactions * feat: add token balances fetching * refactor: remove `dbg` and organize code better * fix: start TokenBalance fetcher * fix: start token balance fetcher * fix: typo in log message * fix: missing fields in celo transaction token transfer * refactor: remove `AbiHandler` in favour of hardcoded pieces of abi * refactor: native token transfers from internal transactions fether * feat: implement parsing of token transfers for traceable rpc node * feat: fetch address coin balances after erc20 celo token transfers * feat: add `gas_token` to transaction response in api v2 * feat: index tokens specified as `feeCurrency` * chore: fix credo warnings * chore: decrease log level to debug * chore: fix cspell warnings * refactor: apply suggestions * fix: handle case when `gas_token` is not loaded * dbg * chore: expect contract calls in celo fetcher test * fix: compile errors for celo view on default chain type * chore: remove unused import * refactor: move `celo_gas_tokens` to chain type imports * refactor: apply suggestions * fix: swap arguments in `put_if_present` * fix: indexer tests * refactor: always include `gas_token` to the response * feat: add `gas_token` to all transaction-related endpoints * feat: celo core contracts with historical data * fix: `uncles` field is expected but not present in RPC node response * fix: credo & format * fix: define `CELO_CORE_CONTRACTS` in tests * fix: put `gas_token` under `celo` section * refactor: move token transfers filtering from `TokenTotalSupplyUpdater` to `Transform.TokenTransfers` * feat: add workflow to publish docker image * refactor: apply suggestions by @nikitosing * chore: add docs and specs * fix: malformed import * feat: add pending epoch operations table * refactor: fix format * feat: add transformers for epoch events * feat: add query to stream pending epoch block operations * fix: call to renamed function * fix: add factory method for pending epoch block operation * feat: add utils for logs parsing * feat: add schemas for epoch rewards and election rewards * fix: rename transformer according to event name * feat: implement epoch rewards fetcher * feat: fetch and import epoch logs * feat: improve epoch rewards fetcher 1. Do not fetch logs -- use the ones stored in DB 2. Import epochs to the database 3. Configure fetcher in runtime.exs * feat: add epoch helper functions * activated_validator_group_vote.ex * feat: add task to fetch core contracts * feat: fetch core contract events in the task * fix: merge artifacts * refactor: logs requests and reduce scope of epoch logs request * chore: fix formatting, credo warning, etc. * feat: fetch epoch rewards in one sql query * feat: fetch validator group votes (historical data and on demand) * refactor: rename fields and replace all entries on conflict * feat: validator group votes fetcher * fix: put each topic in a separate request * feat: fetch voter rewards * refactor: split epoch fetcher to multiple modules * feat: send epoch blocks for async fetching from block fetcher * chore: fix credo and formatting issues * fix: dialyzer warnings * fix: add on demand fetch of group votes * fix: failed explorer and indexer tests * fix: block fetcher tests * fix: match error in epoch logs * feat: add env to manage logs batch size when fetching validator group votes * fix: Add `ssl_opts` for Celo repo * fix: add `disabled?` predicate to supervisor config * fix: return empty list when `getPaymentDelegation` is not available * fix: validate the case when there is no voter rewards for an epoch * fix: formatting * fix: import `put_if_present/3` * fix: do not treat genesis block as an epoch block * fix: some of the fetcher tests * dbg * chore: remove commented code * chore: canonical disable flag env name * fix: run test only for celo chain type * fix: add explicit `wait_ for_tasks` in token instance realtime test * fix: set missing `CELO_CORE_CONTRACTS` env * fix: rollback token instance realtime test * chore: remove debug artifact * chore: add docs for new tables * perf: remove unused fields from `celo_validator_group_votes` table * fix: add on_exit clause * refactor: use `remove/3` function for migration rollback possibility * refactor: extract celo core contracts environment variable setup into a separate function * chore: add new vars to `common-blockscout.env` file * chore: add specs and docs for new modules and functions * refactor: eliminate unused import warnings * fix: credo warning * feat: API for celo epoch rewards and fees breakdown (#10308) * feat(json-rpc): support celo-specific fields in transactions * chore: add new chain type to matrix builder * feat: add celo-specific fields to transactions schema * chore: add "celo" to `cspell.json` * chore: fix formatting * refactor: improve naming for celo fields and ignore `ethCompatible` field * chore: add block fetcher test for celo fields * feat: add celo core contract abis * feat: add a cache for core celo contracts * feat: treat celo native token transfers as token transfers * fix: update `cspell.json` with celo words * fix(dialyzer): pattern can never match the type * fix: update `cspell.json` with celo words * feat: move `CELO_NETWORK` env to config * feat: parse native Celo token transfers from internal transactions * feat: add token balances fetching * refactor: remove `dbg` and organize code better * fix: start TokenBalance fetcher * fix: start token balance fetcher * fix: typo in log message * fix: missing fields in celo transaction token transfer * refactor: remove `AbiHandler` in favour of hardcoded pieces of abi * refactor: native token transfers from internal transactions fether * feat: implement parsing of token transfers for traceable rpc node * feat: fetch address coin balances after erc20 celo token transfers * feat: add `gas_token` to transaction response in api v2 * feat: index tokens specified as `feeCurrency` * chore: fix credo warnings * chore: decrease log level to debug * chore: fix cspell warnings * refactor: apply suggestions * fix: handle case when `gas_token` is not loaded * dbg * chore: expect contract calls in celo fetcher test * fix: compile errors for celo view on default chain type * chore: remove unused import * refactor: move `celo_gas_tokens` to chain type imports * refactor: apply suggestions * fix: swap arguments in `put_if_present` * fix: indexer tests * feat: add pending epoch operations table * feat: add several celo core contracts * refactor: fix format * feat: add new core contract default addresses * feat: add transformers for epoch events * feat: add query to stream pending epoch block operations * fix: call to renamed function * fix: add factory method for pending epoch block operation * feat: add utils for logs parsing * feat: add schemas for epoch rewards and election rewards * fix: rename transformer according to event name * feat: implement epoch rewards fetcher * feat: fetch and import epoch logs * feat: improve epoch rewards fetcher 1. Do not fetch logs -- use the ones stored in DB 2. Import epochs to the database 3. Configure fetcher in runtime.exs * fix: remove duplicated module attribute * feat: add epoch helper functions * activated_validator_group_vote.ex * feat: tmp * refactor: always include `gas_token` to the response * feat: add `gas_token` to all transaction-related endpoints * feat: celo core contracts with historical data * fix: `uncles` field is expected but not present in RPC node response * fix: credo & format * fix: define `CELO_CORE_CONTRACTS` in tests * fix: put `gas_token` under `celo` section * refactor: move token transfers filtering from `TokenTotalSupplyUpdater` to `Transform.TokenTransfers` * feat: add workflow to publish docker image * refactor: apply suggestions by @nikitosing * chore: add docs and specs * fix: malformed import * feat: add pending epoch operations table * refactor: fix format * feat: add transformers for epoch events * feat: add query to stream pending epoch block operations * fix: call to renamed function * fix: add factory method for pending epoch block operation * feat: add utils for logs parsing * feat: add schemas for epoch rewards and election rewards * fix: rename transformer according to event name * feat: implement epoch rewards fetcher * feat: fetch and import epoch logs * feat: improve epoch rewards fetcher 1. Do not fetch logs -- use the ones stored in DB 2. Import epochs to the database 3. Configure fetcher in runtime.exs * feat: add epoch helper functions * activated_validator_group_vote.ex * feat: add task to fetch core contracts * feat: fetch core contract events in the task * fix: merge artifacts * refactor: logs requests and reduce scope of epoch logs request * chore: fix formatting, credo warning, etc. * feat: fetch epoch rewards in one sql query * feat: fetch validator group votes (historical data and on demand) * refactor: rename fields and replace all entries on conflict * feat: validator group votes fetcher * fix: put each topic in a separate request * feat: fetch voter rewards * fix: merge artifacts * feat: add epoch rewards in block api response * fix: merge artifacts * feat: add base fee breakdown in block api response * feat: add aggregated election rewards to block api response * perf: use replica when querying epoch rewards * feat: endpoint for paginated election rewards * feat: endpoint for paginated election rewards for address * chore: rename `rewards` to `distributions` * fix: add missing reward type to API error message * refactor: split epoch fetcher to multiple modules * feat: send epoch blocks for async fetching from block fetcher * chore: fix credo and formatting issues * fix: dialyzer warnings * fix: add on demand fetch of group votes * fix: failed explorer and indexer tests * fix: block fetcher tests * chore: remove unused module * fix: sort election rewards by block number * fix: match error in epoch logs * feat: add env to manage logs batch size when fetching validator group votes * fix: credo and formatting warnings * fix: cspell errors * fix: dialyzer errors * fix: Add `ssl_opts` for Celo repo * fix: add missing preloads and make more explicit api response for block * fix: add `disabled?` predicate to supervisor config * fix: return empty list when `getPaymentDelegation` is not available * fix: validate the case when there is no voter rewards for an epoch * fix: formatting * fix: add missing preload * refactor: more robust fees breakdown logic for the case of fee handler * fix: formatting * refactor: move epoch information to the separate endpoint * fix: import `put_if_present/3` * fix: formatting * fix: do not treat genesis block as an epoch block * fix: some of the fetcher tests * dbg * chore: remove commented code * chore: canonical disable flag env name * fix: run test only for celo chain type * fix: add explicit `wait_ for_tasks` in token instance realtime test * fix: set missing `CELO_CORE_CONTRACTS` env * fix: rollback token instance realtime test * chore: remove debug artifact * chore: add `@docs` and `@specs` * fix: missing core contracts var in tests * feat: extend `tabs-counters` endpoint with election rewards count * refactor: move dead address to `Explorer.Chain.SmartContract` * perf: clause with simplified queries in the case of `amount == 0` and/or `block_number == 0` * fix: it comes that `NotLoaded` clause is not redundant actually... * fix: add missing preload in `BlockScoutWeb.AddressChannel` * chore: unset `CELO_CORE_CONTRACTS` in tests * chore: add missing specs * fix: `paginate` clause when `amount == 0` * refactor: move `paging_options/2` to `Explorer.Chain.Celo.ElectionReward` * refactor: avoid using virtual field for block number. * fix: remove redundant condition in the query * chore: clarify `@spec` * chore: fix credo warning * chore: remove `fi-celo` from branches that trigger ci * fix: merge artifacts * fix: remove todo commentpull/10568/head
parent
e876e03763
commit
6c07246446
@ -0,0 +1,48 @@ |
||||
name: Celo Publish Docker image |
||||
|
||||
on: |
||||
workflow_dispatch: |
||||
push: |
||||
branches: |
||||
- production-celo |
||||
jobs: |
||||
push_to_registry: |
||||
name: Push Docker image to Docker Hub |
||||
runs-on: ubuntu-latest |
||||
env: |
||||
RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} |
||||
DOCKER_CHAIN_NAME: celo |
||||
steps: |
||||
- uses: actions/checkout@v4 |
||||
- name: Setup repo |
||||
uses: ./.github/actions/setup-repo |
||||
id: setup |
||||
with: |
||||
docker-username: ${{ secrets.DOCKER_USERNAME }} |
||||
docker-password: ${{ secrets.DOCKER_PASSWORD }} |
||||
docker-remote-multi-platform: true |
||||
docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} |
||||
docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} |
||||
|
||||
- name: Build and push Docker image for Celo (indexer + API) |
||||
uses: docker/build-push-action@v5 |
||||
with: |
||||
context: . |
||||
file: ./docker/Dockerfile |
||||
push: true |
||||
tags: blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} |
||||
labels: ${{ steps.setup.outputs.docker-labels }} |
||||
platforms: | |
||||
linux/amd64 |
||||
linux/arm64/v8 |
||||
build-args: | |
||||
CACHE_EXCHANGE_RATES_PERIOD= |
||||
API_V1_READ_METHODS_DISABLED=false |
||||
DISABLE_WEBAPP=false |
||||
API_V1_WRITE_METHODS_DISABLED=false |
||||
CACHE_TOTAL_GAS_USAGE_COUNTER_ENABLED= |
||||
ADMIN_PANEL_ENABLED=false |
||||
CACHE_ADDRESS_WITH_BALANCES_UPDATE_INTERVAL= |
||||
BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-beta.+commit.${{ env.SHORT_SHA }} |
||||
RELEASE_VERSION=${{ env.RELEASE_VERSION }} |
||||
CHAIN_TYPE=${{ env.DOCKER_CHAIN_NAME }} |
@ -0,0 +1,391 @@ |
||||
defmodule BlockScoutWeb.API.V2.CeloView do |
||||
@moduledoc """ |
||||
View functions for rendering Celo-related data in JSON format. |
||||
""" |
||||
|
||||
require Logger |
||||
|
||||
import Explorer.Chain.SmartContract, only: [dead_address_hash_string: 0] |
||||
|
||||
alias BlockScoutWeb.API.V2.{Helper, TokenView, TransactionView} |
||||
alias Ecto.Association.NotLoaded |
||||
alias Explorer.Chain |
||||
alias Explorer.Chain.Cache.CeloCoreContracts |
||||
alias Explorer.Chain.Celo.Helper, as: CeloHelper |
||||
alias Explorer.Chain.Celo.{ElectionReward, EpochReward} |
||||
alias Explorer.Chain.Hash |
||||
alias Explorer.Chain.{Block, Transaction} |
||||
|
||||
@address_params [ |
||||
necessity_by_association: %{ |
||||
:names => :optional, |
||||
:smart_contract => :optional, |
||||
:proxy_implementations => :optional |
||||
}, |
||||
api?: true |
||||
] |
||||
|
||||
def render("celo_epoch_distribution.json", %EpochReward{ |
||||
reserve_bolster_transfer: reserve_bolster_transfer, |
||||
community_transfer: community_transfer, |
||||
carbon_offsetting_transfer: carbon_offsetting_transfer |
||||
}) do |
||||
Map.new( |
||||
[ |
||||
reserve_bolster_transfer: reserve_bolster_transfer, |
||||
community_transfer: community_transfer, |
||||
carbon_offsetting_transfer: carbon_offsetting_transfer |
||||
], |
||||
fn {field, token_transfer} -> |
||||
token_transfer_json = |
||||
token_transfer && |
||||
TransactionView.render( |
||||
"token_transfer.json", |
||||
%{token_transfer: token_transfer, conn: nil} |
||||
) |
||||
|
||||
{field, token_transfer_json} |
||||
end |
||||
) |
||||
end |
||||
|
||||
def render("celo_epoch_distribution.json", _distribution), |
||||
do: nil |
||||
|
||||
def render("celo_aggregated_election_rewards.json", aggregated_election_rewards) do |
||||
Map.new( |
||||
aggregated_election_rewards, |
||||
fn {type, %{total: total, count: count, token: token}} -> |
||||
{type, |
||||
%{ |
||||
total: total, |
||||
count: count, |
||||
token: |
||||
TokenView.render("token.json", %{ |
||||
token: token, |
||||
contract_address_hash: token.contract_address_hash |
||||
}) |
||||
}} |
||||
end |
||||
) |
||||
end |
||||
|
||||
def render("celo_epoch.json", %{ |
||||
epoch_number: epoch_number, |
||||
epoch_distribution: epoch_distribution, |
||||
aggregated_election_rewards: aggregated_election_rewards |
||||
}) do |
||||
distribution_json = render("celo_epoch_distribution.json", epoch_distribution) |
||||
|
||||
# Workaround: we assume that if epoch rewards are not fetched for a block, |
||||
# we should not display aggregated election rewards for it. |
||||
# |
||||
# todo: consider checking pending block epoch operations to determine if |
||||
# epoch is fetched or not |
||||
aggregated_election_rewards_json = |
||||
if distribution_json do |
||||
render("celo_aggregated_election_rewards.json", aggregated_election_rewards) |
||||
else |
||||
nil |
||||
end |
||||
|
||||
%{ |
||||
number: epoch_number, |
||||
distribution: distribution_json, |
||||
aggregated_election_rewards: aggregated_election_rewards_json |
||||
} |
||||
end |
||||
|
||||
def render("celo_base_fee.json", %Block{} = block) do |
||||
# For the blocks, where both FeeHandler and Governance contracts aren't |
||||
# deployed, the base fee is not burnt, but refunded to transaction sender, |
||||
# so we return nil in this case. |
||||
|
||||
base_fee = Block.burnt_fees(block.transactions, block.base_fee_per_gas) |
||||
|
||||
fee_handler_base_fee_breakdown( |
||||
base_fee, |
||||
block.number |
||||
) || |
||||
governance_base_fee_breakdown( |
||||
base_fee, |
||||
block.number |
||||
) |
||||
end |
||||
|
||||
def render("celo_election_rewards.json", %{ |
||||
rewards: rewards, |
||||
next_page_params: next_page_params |
||||
}) do |
||||
%{ |
||||
"items" => Enum.map(rewards, &prepare_election_reward/1), |
||||
"next_page_params" => next_page_params |
||||
} |
||||
end |
||||
|
||||
@doc """ |
||||
Extends the JSON output with a sub-map containing information related to Celo, |
||||
such as the epoch number, whether the block is an epoch block, and the routing |
||||
of the base fee. |
||||
|
||||
## Parameters |
||||
- `out_json`: A map defining the output JSON which will be extended. |
||||
- `block`: The block structure containing Celo-related data. |
||||
- `single_block?`: A boolean indicating if it is a single block. |
||||
|
||||
## Returns |
||||
- A map extended with data related to Celo. |
||||
""" |
||||
def extend_block_json_response(out_json, %Block{} = block, single_block?) do |
||||
celo_json = |
||||
%{ |
||||
"is_epoch_block" => CeloHelper.epoch_block_number?(block.number), |
||||
"epoch_number" => CeloHelper.block_number_to_epoch_number(block.number) |
||||
} |
||||
|> maybe_add_base_fee_info(block, single_block?) |
||||
|
||||
Map.put(out_json, "celo", celo_json) |
||||
end |
||||
|
||||
@doc """ |
||||
Extends the JSON output with a sub-map containing information about the gas |
||||
token used to pay for the transaction fees. |
||||
|
||||
## Parameters |
||||
- `out_json`: A map defining the output JSON which will be extended. |
||||
- `transaction`: The transaction structure containing Celo-related data. |
||||
|
||||
## Returns |
||||
- A map extended with data related to the gas token. |
||||
""" |
||||
def extend_transaction_json_response(out_json, %Transaction{} = transaction) do |
||||
token_json = |
||||
case { |
||||
Map.get(transaction, :gas_token_contract_address), |
||||
Map.get(transaction, :gas_token) |
||||
} do |
||||
# {_, %NotLoaded{}} -> |
||||
# nil |
||||
|
||||
{nil, _} -> |
||||
nil |
||||
|
||||
{gas_token_contract_address, gas_token} -> |
||||
if is_nil(gas_token) do |
||||
Logger.error(fn -> |
||||
[ |
||||
"Transaction #{transaction.hash} has a ", |
||||
"gas token contract address #{gas_token_contract_address} ", |
||||
"but no associated token found in the database" |
||||
] |
||||
end) |
||||
end |
||||
|
||||
TokenView.render("token.json", %{ |
||||
token: gas_token, |
||||
contract_address_hash: gas_token_contract_address |
||||
}) |
||||
end |
||||
|
||||
Map.put(out_json, "celo", %{"gas_token" => token_json}) |
||||
end |
||||
|
||||
@spec prepare_election_reward(Explorer.Chain.Celo.ElectionReward.t()) :: %{ |
||||
:account => nil | %{optional(String.t()) => any()}, |
||||
:amount => Decimal.t(), |
||||
:associated_account => nil | %{optional(String.t()) => any()}, |
||||
optional(:block_hash) => Hash.Full.t(), |
||||
optional(:block_number) => Block.block_number(), |
||||
optional(:epoch_number) => non_neg_integer(), |
||||
optional(:type) => ElectionReward.type() |
||||
} |
||||
defp prepare_election_reward(%ElectionReward{block: %NotLoaded{}} = reward) do |
||||
%{ |
||||
amount: reward.amount, |
||||
account: |
||||
Helper.address_with_info( |
||||
reward.account_address, |
||||
reward.account_address_hash |
||||
), |
||||
associated_account: |
||||
Helper.address_with_info( |
||||
reward.associated_account_address, |
||||
reward.associated_account_address_hash |
||||
) |
||||
} |
||||
end |
||||
|
||||
defp prepare_election_reward(%ElectionReward{} = reward) do |
||||
%{ |
||||
amount: reward.amount, |
||||
block_number: reward.block.number, |
||||
block_hash: reward.block_hash, |
||||
epoch_number: reward.block.number |> CeloHelper.block_number_to_epoch_number(), |
||||
account: |
||||
Helper.address_with_info( |
||||
reward.account_address, |
||||
reward.account_address_hash |
||||
), |
||||
associated_account: |
||||
Helper.address_with_info( |
||||
reward.associated_account_address, |
||||
reward.associated_account_address_hash |
||||
), |
||||
type: reward.type |
||||
} |
||||
end |
||||
|
||||
# Convert the burn fraction from FixidityLib value to decimal. |
||||
@spec burn_fraction_decimal(integer()) :: Decimal.t() |
||||
defp burn_fraction_decimal(burn_fraction_fixidity_lib) |
||||
when is_integer(burn_fraction_fixidity_lib) do |
||||
base = Decimal.new(1, 10, 24) |
||||
fraction = Decimal.new(1, burn_fraction_fixidity_lib, 0) |
||||
Decimal.div(fraction, base) |
||||
end |
||||
|
||||
# Get the breakdown of the base fee for the case when FeeHandler is a contract |
||||
# that receives the base fee. |
||||
@spec fee_handler_base_fee_breakdown(Decimal.t(), Block.block_number()) :: |
||||
%{ |
||||
:recipient => %{optional(String.t()) => any()}, |
||||
:amount => float(), |
||||
:breakdown => [ |
||||
%{ |
||||
:address => %{optional(String.t()) => any()}, |
||||
:amount => float(), |
||||
:percentage => float() |
||||
} |
||||
] |
||||
} |
||||
| nil |
||||
defp fee_handler_base_fee_breakdown(base_fee, block_number) do |
||||
with {:ok, fee_handler_contract_address_hash} <- |
||||
CeloCoreContracts.get_address(:fee_handler, block_number), |
||||
{:ok, %{"address" => fee_beneficiary_address_hash}} <- |
||||
CeloCoreContracts.get_event(:fee_handler, :fee_beneficiary_set, block_number), |
||||
{:ok, %{"value" => burn_fraction_fixidity_lib}} <- |
||||
CeloCoreContracts.get_event(:fee_handler, :burn_fraction_set, block_number) do |
||||
burn_fraction = burn_fraction_decimal(burn_fraction_fixidity_lib) |
||||
|
||||
burnt_amount = Decimal.mult(base_fee, burn_fraction) |
||||
burnt_percentage = Decimal.mult(burn_fraction, 100) |
||||
|
||||
carbon_offsetting_amount = Decimal.sub(base_fee, burnt_amount) |
||||
carbon_offsetting_percentage = Decimal.sub(100, burnt_percentage) |
||||
|
||||
celo_burn_address_hash_string = dead_address_hash_string() |
||||
|
||||
address_hashes_to_fetch_from_db = [ |
||||
fee_handler_contract_address_hash, |
||||
fee_beneficiary_address_hash, |
||||
celo_burn_address_hash_string |
||||
] |
||||
|
||||
address_hash_string_to_address = |
||||
address_hashes_to_fetch_from_db |
||||
|> Enum.map(&(&1 |> Chain.string_to_address_hash() |> elem(1))) |
||||
# todo: Querying database in the view is not a good practice. Consider |
||||
# refactoring. |
||||
|> Chain.hashes_to_addresses(@address_params) |
||||
|> Map.new(fn address -> |
||||
{ |
||||
to_string(address.hash), |
||||
address |
||||
} |
||||
end) |
||||
|
||||
%{ |
||||
^fee_handler_contract_address_hash => fee_handler_contract_address_info, |
||||
^fee_beneficiary_address_hash => fee_beneficiary_address_info, |
||||
^celo_burn_address_hash_string => burn_address_info |
||||
} = |
||||
Map.new( |
||||
address_hashes_to_fetch_from_db, |
||||
&{ |
||||
&1, |
||||
Helper.address_with_info( |
||||
Map.get(address_hash_string_to_address, &1), |
||||
&1 |
||||
) |
||||
} |
||||
) |
||||
|
||||
%{ |
||||
recipient: fee_handler_contract_address_info, |
||||
amount: base_fee, |
||||
breakdown: [ |
||||
%{ |
||||
address: burn_address_info, |
||||
amount: Decimal.to_float(burnt_amount), |
||||
percentage: Decimal.to_float(burnt_percentage) |
||||
}, |
||||
%{ |
||||
address: fee_beneficiary_address_info, |
||||
amount: Decimal.to_float(carbon_offsetting_amount), |
||||
percentage: Decimal.to_float(carbon_offsetting_percentage) |
||||
} |
||||
] |
||||
} |
||||
else |
||||
_ -> nil |
||||
end |
||||
end |
||||
|
||||
# Get the breakdown of the base fee for the case when Governance is a contract |
||||
# that receives the base fee. |
||||
# |
||||
# Note that the base fee is not burnt in this case, but simply kept on the |
||||
# contract balance. |
||||
@spec governance_base_fee_breakdown(Decimal.t(), Block.block_number()) :: |
||||
%{ |
||||
:recipient => %{optional(String.t()) => any()}, |
||||
:amount => float(), |
||||
:breakdown => [ |
||||
%{ |
||||
:address => %{optional(String.t()) => any()}, |
||||
:amount => float(), |
||||
:percentage => float() |
||||
} |
||||
] |
||||
} |
||||
| nil |
||||
defp governance_base_fee_breakdown(base_fee, block_number) do |
||||
with {:ok, address_hash_string} when not is_nil(address_hash_string) <- |
||||
CeloCoreContracts.get_address(:governance, block_number), |
||||
{:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string) do |
||||
address = |
||||
address_hash |
||||
# todo: Querying database in the view is not a good practice. Consider |
||||
# refactoring. |
||||
|> Chain.hash_to_address(@address_params) |
||||
|> case do |
||||
{:ok, address} -> address |
||||
{:error, :not_found} -> nil |
||||
end |
||||
|
||||
address_with_info = |
||||
Helper.address_with_info( |
||||
address, |
||||
address_hash |
||||
) |
||||
|
||||
%{ |
||||
recipient: address_with_info, |
||||
amount: base_fee, |
||||
breakdown: [] |
||||
} |
||||
else |
||||
_ -> |
||||
nil |
||||
end |
||||
end |
||||
|
||||
defp maybe_add_base_fee_info(celo_json, block_or_transaction, true) do |
||||
base_fee_breakdown_json = render("celo_base_fee.json", block_or_transaction) |
||||
Map.put(celo_json, "base_fee", base_fee_breakdown_json) |
||||
end |
||||
|
||||
defp maybe_add_base_fee_info(celo_json, _block_or_transaction, false), |
||||
do: celo_json |
||||
end |
@ -0,0 +1,235 @@ |
||||
defmodule Explorer.Chain.Cache.CeloCoreContracts do |
||||
@moduledoc """ |
||||
Cache for Celo core contract addresses. |
||||
|
||||
This module operates with a `CELO_CORE_CONTRACTS` environment variable, which |
||||
contains the JSON map of core contract addresses on the Celo network. The |
||||
module provides functions to fetch the addresses of core contracts at a given |
||||
block. Additionally, it provides a function to obtain the state of a specific |
||||
contract by fetching the latest event for the contract at a given block. |
||||
|
||||
For details on the structure of the `CELO_CORE_CONTRACTS` environment |
||||
variable, see `app/explorer/lib/fetch_celo_core_contracts.ex`. |
||||
""" |
||||
@dialyzer :no_match |
||||
|
||||
require Logger |
||||
|
||||
alias EthereumJSONRPC |
||||
alias Explorer.Chain.Block |
||||
|
||||
@type contract_name :: String.t() |
||||
|
||||
@atom_to_contract_name %{ |
||||
accounts: "Accounts", |
||||
celo_token: "GoldToken", |
||||
election: "Election", |
||||
epoch_rewards: "EpochRewards", |
||||
locked_gold: "LockedGold", |
||||
reserve: "Reserve", |
||||
usd_token: "StableToken", |
||||
validators: "Validators", |
||||
governance: "Governance", |
||||
fee_handler: "FeeHandler", |
||||
gas_price_minimum: "GasPriceMinimum" |
||||
} |
||||
|
||||
@atom_to_contract_event_names %{ |
||||
fee_handler: %{ |
||||
fee_beneficiary_set: "FeeBeneficiarySet", |
||||
burn_fraction_set: "BurnFractionSet" |
||||
}, |
||||
epoch_rewards: %{ |
||||
carbon_offsetting_fund_set: "CarbonOffsettingFundSet" |
||||
} |
||||
} |
||||
|
||||
@doc """ |
||||
A map where keys are atoms representing contract types, and values are strings |
||||
representing the names of the contracts. |
||||
""" |
||||
@spec atom_to_contract_name() :: %{atom() => contract_name} |
||||
def atom_to_contract_name, do: @atom_to_contract_name |
||||
|
||||
@doc """ |
||||
A nested map where keys are atoms representing contract types, and values are |
||||
maps of event atoms to event names. |
||||
""" |
||||
@spec atom_to_contract_event_names() :: %{atom() => %{atom() => contract_name}} |
||||
def atom_to_contract_event_names, do: @atom_to_contract_event_names |
||||
|
||||
defp core_contracts, do: Application.get_env(:explorer, __MODULE__)[:contracts] |
||||
|
||||
@doc """ |
||||
Gets the specified event for a core contract at a given block number. |
||||
|
||||
## Parameters |
||||
- `contract_atom`: The atom representing the contract. |
||||
- `event_atom`: The atom representing the event. |
||||
- `block_number`: The block number at which to fetch the event. |
||||
|
||||
## Returns (one of the following) |
||||
- `{:ok, map() | nil}`: The event data if found, or `nil` if no event is found. |
||||
- `{:error, reason}`: An error tuple with the reason for the failure. |
||||
""" |
||||
@spec get_event(atom(), atom(), Block.block_number()) :: |
||||
{:ok, map() | nil} |
||||
| {:error, |
||||
:contract_atom_not_found |
||||
| :event_atom_not_found |
||||
| :contract_name_not_found |
||||
| :event_name_not_found |
||||
| :contract_address_not_found |
||||
| :event_does_not_exist} |
||||
def get_event(contract_atom, event_atom, block_number) do |
||||
with {:ok, address} <- get_address(contract_atom, block_number), |
||||
{:contract_atom, {:ok, contract_name}} <- |
||||
{:contract_atom, Map.fetch(@atom_to_contract_name, contract_atom)}, |
||||
{:event_atom, {:ok, event_name}} <- |
||||
{ |
||||
:event_atom, |
||||
@atom_to_contract_event_names |
||||
|> Map.get(contract_atom, %{}) |
||||
|> Map.fetch(event_atom) |
||||
}, |
||||
{:events, {:ok, contract_name_to_addresses}} <- |
||||
{:events, Map.fetch(core_contracts(), "events")}, |
||||
{:contract_name, {:ok, contract_addresses}} <- |
||||
{:contract_name, Map.fetch(contract_name_to_addresses, contract_name)}, |
||||
{:contract_address, {:ok, contract_events}} <- |
||||
{:contract_address, Map.fetch(contract_addresses, address)}, |
||||
{:event_name, {:ok, event_updates}} <- |
||||
{:event_name, Map.fetch(contract_events, event_name)}, |
||||
current_event when not is_nil(current_event) <- |
||||
event_updates |
||||
|> Enum.take_while(&(&1["updated_at_block_number"] <= block_number)) |
||||
|> List.last() do |
||||
{:ok, current_event} |
||||
else |
||||
nil -> |
||||
{:ok, nil} |
||||
|
||||
{:contract_atom, :error} -> |
||||
Logger.error("Unknown contract atom: #{inspect(contract_atom)}") |
||||
{:error, :contract_atom_not_found} |
||||
|
||||
{:event_atom, :error} -> |
||||
Logger.error("Unknown event atom: #{inspect(event_atom)}") |
||||
{:error, :event_atom_not_found} |
||||
|
||||
{:events, :error} -> |
||||
raise "Missing `events` key in CELO core contracts JSON" |
||||
|
||||
{:contract_name, :error} -> |
||||
Logger.error(fn -> |
||||
[ |
||||
"Unknown name for contract atom: #{contract_atom}, ", |
||||
"ensure `CELO_CORE_CONTRACTS` env var is set ", |
||||
"and the provided JSON contains required key" |
||||
] |
||||
end) |
||||
|
||||
{:error, :contract_name_not_found} |
||||
|
||||
{:event_name, :error} -> |
||||
Logger.error(fn -> |
||||
[ |
||||
"Unknown name for event atom: #{event_atom}, ", |
||||
"ensure `CELO_CORE_CONTRACTS` env var is set ", |
||||
"and the provided JSON contains required key" |
||||
] |
||||
end) |
||||
|
||||
{:error, :event_name_not_found} |
||||
|
||||
nil -> |
||||
{:error, :event_does_not_exist} |
||||
|
||||
{:contract_address, :error} -> |
||||
Logger.error(fn -> |
||||
[ |
||||
"Unknown address for contract atom: #{contract_atom}, ", |
||||
"ensure `CELO_CORE_CONTRACTS` env var is set ", |
||||
"and the provided JSON contains required key" |
||||
] |
||||
end) |
||||
|
||||
{:error, :contract_address_not_found} |
||||
|
||||
error -> |
||||
error |
||||
end |
||||
end |
||||
|
||||
defp get_address_updates(contract_atom) do |
||||
with {:atom, {:ok, contract_name}} <- |
||||
{:atom, Map.fetch(@atom_to_contract_name, contract_atom)}, |
||||
{:addresses, {:ok, contract_name_to_addresses}} <- |
||||
{:addresses, Map.fetch(core_contracts(), "addresses")}, |
||||
{:name, {:ok, address_updates}} <- |
||||
{:name, Map.fetch(contract_name_to_addresses, contract_name)} do |
||||
{:ok, address_updates} |
||||
else |
||||
{:atom, :error} -> |
||||
Logger.error("Unknown contract atom: #{inspect(contract_atom)}") |
||||
{:error, :contract_atom_not_found} |
||||
|
||||
{:addresses, :error} -> |
||||
raise "Missing `addresses` key in CELO core contracts JSON" |
||||
|
||||
{:name, :error} -> |
||||
Logger.error(fn -> |
||||
[ |
||||
"Unknown name for contract atom: #{contract_atom}, ", |
||||
"ensure `CELO_CORE_CONTRACTS` env var is set ", |
||||
"and the provided JSON contains required key" |
||||
] |
||||
end) |
||||
|
||||
{:error, :contract_name_not_found} |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Gets the address of a core contract at a given block number. |
||||
|
||||
## Parameters |
||||
- `contract_atom`: The atom representing the contract. |
||||
- `block_number`: The block number at which to fetch the address. |
||||
|
||||
## Returns (one of the following) |
||||
- `{:ok, EthereumJSONRPC.address() | nil}`: The address of the contract, or `nil` if not found. |
||||
- `{:error, reason}`: An error tuple with the reason for the failure. |
||||
""" |
||||
@spec get_address(atom(), Block.block_number()) :: |
||||
{:ok, EthereumJSONRPC.address() | nil} |
||||
| {:error, |
||||
:contract_atom_not_found |
||||
| :contract_name_not_found |
||||
| :address_does_not_exist} |
||||
def get_address(contract_atom, block_number) do |
||||
with {:ok, address_updates} <- get_address_updates(contract_atom), |
||||
%{"address" => current_address} <- |
||||
address_updates |
||||
|> Enum.take_while(&(&1["updated_at_block_number"] <= block_number)) |
||||
|> List.last() do |
||||
{:ok, current_address} |
||||
else |
||||
nil -> |
||||
{:error, :address_does_not_exist} |
||||
|
||||
error -> |
||||
error |
||||
end |
||||
end |
||||
|
||||
def get_first_update_block_number(contract_atom) do |
||||
with {:ok, address_updates} <- get_address_updates(contract_atom), |
||||
%{"updated_at_block_number" => updated_at_block_number} <- |
||||
address_updates |
||||
|> Enum.sort_by(& &1["updated_at_block_number"]) |
||||
|> List.first() do |
||||
{:ok, updated_at_block_number} |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,455 @@ |
||||
defmodule Explorer.Chain.Celo.ElectionReward do |
||||
@moduledoc """ |
||||
Represents the rewards distributed in an epoch election. Each reward has a |
||||
type, and each type of reward is paid in a specific token. The rewards are |
||||
paid to an account address and are also associated with another account |
||||
address. |
||||
|
||||
## Reward Types and Addresses |
||||
|
||||
Here is the breakdown of what each address means for each type of reward: |
||||
|
||||
- `voter`: |
||||
- Account address: The voter address. |
||||
- Associated account address: The group address. |
||||
- `validator`: |
||||
- Account address: The validator address. |
||||
- Associated account address: The validator group address. |
||||
- `group`: |
||||
- Account address: The validator group address. |
||||
- Associated account address: The validator address that the reward was paid |
||||
on behalf of. |
||||
- `delegated_payment`: |
||||
- Account address: The beneficiary receiving the part of the reward on |
||||
behalf of the validator. |
||||
- Associated account address: The validator that set the delegation of a |
||||
part of their reward to some external address. |
||||
""" |
||||
|
||||
use Explorer.Schema |
||||
|
||||
import Explorer.PagingOptions, only: [default_paging_options: 0] |
||||
import Ecto.Query, only: [from: 2, where: 3] |
||||
import Explorer.Helper, only: [safe_parse_non_negative_integer: 1] |
||||
|
||||
alias Explorer.{Chain, PagingOptions} |
||||
alias Explorer.Chain.{Address, Block, Hash, Wei} |
||||
|
||||
@type type :: :voter | :validator | :group | :delegated_payment |
||||
@types_enum ~w(voter validator group delegated_payment)a |
||||
|
||||
@reward_type_string_to_atom %{ |
||||
"voter" => :voter, |
||||
"validator" => :validator, |
||||
"group" => :group, |
||||
"delegated-payment" => :delegated_payment |
||||
} |
||||
|
||||
@reward_type_atom_to_token_atom %{ |
||||
:voter => :celo_token, |
||||
:validator => :usd_token, |
||||
:group => :usd_token, |
||||
:delegated_payment => :usd_token |
||||
} |
||||
|
||||
@required_attrs ~w(amount type block_hash account_address_hash associated_account_address_hash)a |
||||
|
||||
@primary_key false |
||||
typed_schema "celo_election_rewards" do |
||||
field(:amount, Wei, null: false) |
||||
|
||||
field( |
||||
:type, |
||||
Ecto.Enum, |
||||
values: @types_enum, |
||||
null: false, |
||||
primary_key: true |
||||
) |
||||
|
||||
belongs_to( |
||||
:block, |
||||
Block, |
||||
primary_key: true, |
||||
foreign_key: :block_hash, |
||||
references: :hash, |
||||
type: Hash.Full, |
||||
null: false |
||||
) |
||||
|
||||
belongs_to( |
||||
:account_address, |
||||
Address, |
||||
primary_key: true, |
||||
foreign_key: :account_address_hash, |
||||
references: :hash, |
||||
type: Hash.Address, |
||||
null: false |
||||
) |
||||
|
||||
belongs_to( |
||||
:associated_account_address, |
||||
Address, |
||||
primary_key: true, |
||||
foreign_key: :associated_account_address_hash, |
||||
references: :hash, |
||||
type: Hash.Address, |
||||
null: false |
||||
) |
||||
|
||||
timestamps() |
||||
end |
||||
|
||||
@spec changeset( |
||||
Explorer.Chain.Celo.ElectionReward.t(), |
||||
map() |
||||
) :: Ecto.Changeset.t() |
||||
def changeset(%__MODULE__{} = rewards, attrs) do |
||||
rewards |
||||
|> cast(attrs, @required_attrs) |
||||
|> validate_required(@required_attrs) |
||||
|> foreign_key_constraint(:block_hash) |
||||
|> foreign_key_constraint(:account_address_hash) |
||||
|> foreign_key_constraint(:associated_account_address_hash) |
||||
|
||||
# todo: do I need to set this unique constraint here? or it is redundant? |
||||
# |> unique_constraint( |
||||
# [:block_hash, :type, :account_address_hash, :associated_account_address_hash], |
||||
# name: :celo_election_rewards_pkey |
||||
# ) |
||||
end |
||||
|
||||
@doc """ |
||||
Returns the list of election reward types. |
||||
""" |
||||
@spec types() :: [type] |
||||
def types, do: @types_enum |
||||
|
||||
@doc """ |
||||
Converts a reward type string to its corresponding atom. |
||||
|
||||
## Parameters |
||||
- `type_string` (`String.t()`): The string representation of the reward type. |
||||
|
||||
## Returns |
||||
- `{:ok, type}` if the string is valid, `:error` otherwise. |
||||
|
||||
## Examples |
||||
|
||||
iex> ElectionReward.type_from_string("voter") |
||||
{:ok, :voter} |
||||
|
||||
iex> ElectionReward.type_from_string("invalid") |
||||
:error |
||||
""" |
||||
@spec type_from_string(String.t()) :: {:ok, type} | :error |
||||
def type_from_string(type_string) do |
||||
Map.fetch(@reward_type_string_to_atom, type_string) |
||||
end |
||||
|
||||
@doc """ |
||||
Returns a map of reward type atoms to their corresponding token atoms. |
||||
|
||||
## Returns |
||||
- A map where the keys are reward type atoms and the values are token atoms. |
||||
|
||||
## Examples |
||||
|
||||
iex> ElectionReward.reward_type_atom_to_token_atom() |
||||
%{voter: :celo_token, validator: :usd_token, group: :usd_token, delegated_payment: :usd_token} |
||||
""" |
||||
@spec reward_type_atom_to_token_atom() :: %{type => atom()} |
||||
def reward_type_atom_to_token_atom, do: @reward_type_atom_to_token_atom |
||||
|
||||
@doc """ |
||||
Builds a query to aggregate rewards by type for a given block hash. |
||||
|
||||
## Parameters |
||||
- `block_hash` (`Hash.Full.t()`): The block hash to filter rewards. |
||||
|
||||
## Returns |
||||
- An Ecto query. |
||||
""" |
||||
@spec block_hash_to_aggregated_rewards_by_type_query(Hash.Full.t()) :: Ecto.Query.t() |
||||
def block_hash_to_aggregated_rewards_by_type_query(block_hash) do |
||||
from( |
||||
r in __MODULE__, |
||||
where: r.block_hash == ^block_hash, |
||||
select: {r.type, sum(r.amount), count(r)}, |
||||
group_by: r.type |
||||
) |
||||
end |
||||
|
||||
@doc """ |
||||
Builds a query to get rewards by type for a given block hash. |
||||
|
||||
## Parameters |
||||
- `block_hash` (`Hash.Full.t()`): The block hash to filter rewards. |
||||
- `reward_type` (`type`): The type of reward to filter. |
||||
|
||||
## Returns |
||||
- An Ecto query. |
||||
""" |
||||
@spec block_hash_to_rewards_by_type_query(Hash.Full.t(), type) :: Ecto.Query.t() |
||||
def block_hash_to_rewards_by_type_query(block_hash, reward_type) do |
||||
from( |
||||
r in __MODULE__, |
||||
where: r.block_hash == ^block_hash and r.type == ^reward_type, |
||||
select: r, |
||||
order_by: [ |
||||
desc: :amount, |
||||
asc: :account_address_hash, |
||||
asc: :associated_account_address_hash |
||||
] |
||||
) |
||||
end |
||||
|
||||
@doc """ |
||||
Builds a query to get rewards by account address hash. |
||||
""" |
||||
@spec address_hash_to_rewards_query(Hash.Address.t()) :: Ecto.Query.t() |
||||
def address_hash_to_rewards_query(address_hash) do |
||||
from( |
||||
r in __MODULE__, |
||||
where: r.account_address_hash == ^address_hash, |
||||
select: r |
||||
) |
||||
end |
||||
|
||||
@doc """ |
||||
Builds a query to get ordered rewards by account address hash. |
||||
|
||||
## Parameters |
||||
- `address_hash` (`Hash.Address.t()`): The account address hash to filter |
||||
rewards. |
||||
|
||||
## Returns |
||||
- An Ecto query. |
||||
""" |
||||
@spec address_hash_to_ordered_rewards_query(Hash.Address.t()) :: Ecto.Query.t() |
||||
def address_hash_to_ordered_rewards_query(address_hash) do |
||||
from( |
||||
r in __MODULE__, |
||||
join: b in assoc(r, :block), |
||||
as: :block, |
||||
preload: [block: b], |
||||
where: r.account_address_hash == ^address_hash, |
||||
select: r, |
||||
order_by: [ |
||||
desc: b.number, |
||||
desc: r.amount, |
||||
asc: r.associated_account_address_hash, |
||||
asc: r.type |
||||
] |
||||
) |
||||
end |
||||
|
||||
@doc """ |
||||
Makes Explorer.PagingOptions map for election rewards. |
||||
""" |
||||
@spec address_paging_options(map()) :: [Chain.paging_options()] |
||||
def block_paging_options(params) do |
||||
with %{ |
||||
"amount" => amount_string, |
||||
"account_address_hash" => account_address_hash_string, |
||||
"associated_account_address_hash" => associated_account_address_hash_string |
||||
} |
||||
when is_binary(amount_string) and |
||||
is_binary(account_address_hash_string) and |
||||
is_binary(associated_account_address_hash_string) <- params, |
||||
{amount, ""} <- Decimal.parse(amount_string), |
||||
{:ok, account_address_hash} <- Hash.Address.cast(account_address_hash_string), |
||||
{:ok, associated_account_address_hash} <- |
||||
Hash.Address.cast(associated_account_address_hash_string) do |
||||
[ |
||||
paging_options: %{ |
||||
default_paging_options() |
||||
| key: {amount, account_address_hash, associated_account_address_hash} |
||||
} |
||||
] |
||||
else |
||||
_ -> |
||||
[paging_options: default_paging_options()] |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Makes Explorer.PagingOptions map for election rewards. |
||||
""" |
||||
@spec address_paging_options(map()) :: [Chain.paging_options()] |
||||
def address_paging_options(params) do |
||||
with %{ |
||||
"block_number" => block_number_string, |
||||
"amount" => amount_string, |
||||
"associated_account_address_hash" => associated_account_address_hash_string, |
||||
"type" => type_string |
||||
} |
||||
when is_binary(block_number_string) and |
||||
is_binary(amount_string) and |
||||
is_binary(associated_account_address_hash_string) and |
||||
is_binary(type_string) <- params, |
||||
{:ok, block_number} <- safe_parse_non_negative_integer(block_number_string), |
||||
{amount, ""} <- Decimal.parse(amount_string), |
||||
{:ok, associated_account_address_hash} <- |
||||
Hash.Address.cast(associated_account_address_hash_string), |
||||
{:ok, type} <- type_from_string(type_string) do |
||||
[ |
||||
paging_options: %{ |
||||
default_paging_options() |
||||
| key: {block_number, amount, associated_account_address_hash, type} |
||||
} |
||||
] |
||||
else |
||||
_ -> |
||||
[paging_options: default_paging_options()] |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Paginates the given query based on the provided `PagingOptions`. |
||||
|
||||
## Parameters |
||||
- `query` (`Ecto.Query.t()`): The query to paginate. |
||||
- `paging_options` (`PagingOptions.t()`): The pagination options. |
||||
|
||||
## Returns |
||||
- An Ecto query with pagination applied. |
||||
""" |
||||
def paginate(query, %PagingOptions{key: nil}), do: query |
||||
|
||||
def paginate(query, %PagingOptions{key: {0 = _amount, account_address_hash, associated_account_address_hash}}) do |
||||
where( |
||||
query, |
||||
[reward], |
||||
reward.amount == 0 and |
||||
(reward.account_address_hash > ^account_address_hash or |
||||
(reward.account_address_hash == ^account_address_hash and |
||||
reward.associated_account_address_hash > ^associated_account_address_hash)) |
||||
) |
||||
end |
||||
|
||||
def paginate(query, %PagingOptions{key: {amount, account_address_hash, associated_account_address_hash}}) do |
||||
where( |
||||
query, |
||||
[reward], |
||||
reward.amount < ^amount or |
||||
(reward.amount == ^amount and |
||||
reward.account_address_hash > ^account_address_hash) or |
||||
(reward.amount == ^amount and |
||||
reward.account_address_hash == ^account_address_hash and |
||||
reward.associated_account_address_hash > ^associated_account_address_hash) |
||||
) |
||||
end |
||||
|
||||
def paginate(query, %PagingOptions{key: {0 = _block_number, 0 = _amount, associated_account_address_hash, type}}) do |
||||
where( |
||||
query, |
||||
[reward, block], |
||||
block.number == 0 and reward.amount == 0 and |
||||
(reward.associated_account_address_hash > ^associated_account_address_hash or |
||||
(reward.associated_account_address_hash == ^associated_account_address_hash and |
||||
reward.type > ^type)) |
||||
) |
||||
end |
||||
|
||||
def paginate(query, %PagingOptions{key: {0 = _block_number, amount, associated_account_address_hash, type}}) do |
||||
where( |
||||
query, |
||||
[reward, block], |
||||
block.number == 0 and |
||||
(reward.amount < ^amount or |
||||
(reward.amount == ^amount and |
||||
reward.associated_account_address_hash > ^associated_account_address_hash) or |
||||
(reward.amount == ^amount and |
||||
reward.associated_account_address_hash == ^associated_account_address_hash and |
||||
reward.type > ^type)) |
||||
) |
||||
end |
||||
|
||||
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity |
||||
def paginate(query, %PagingOptions{key: {block_number, 0 = _amount, associated_account_address_hash, type}}) do |
||||
where( |
||||
query, |
||||
[reward, block], |
||||
block.number < ^block_number or |
||||
(block.number == ^block_number and |
||||
reward.amount == 0 and |
||||
reward.associated_account_address_hash > ^associated_account_address_hash) or |
||||
(block.number == ^block_number and |
||||
reward.amount == 0 and |
||||
reward.associated_account_address_hash == ^associated_account_address_hash and |
||||
reward.type > ^type) |
||||
) |
||||
end |
||||
|
||||
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity |
||||
def paginate(query, %PagingOptions{key: {block_number, amount, associated_account_address_hash, type}}) do |
||||
where( |
||||
query, |
||||
[reward, block], |
||||
block.number < ^block_number or |
||||
(block.number == ^block_number and |
||||
reward.amount < ^amount) or |
||||
(block.number == ^block_number and |
||||
reward.amount == ^amount and |
||||
reward.associated_account_address_hash > ^associated_account_address_hash) or |
||||
(block.number == ^block_number and |
||||
reward.amount == ^amount and |
||||
reward.associated_account_address_hash == ^associated_account_address_hash and |
||||
reward.type > ^type) |
||||
) |
||||
end |
||||
|
||||
@doc """ |
||||
Converts an `ElectionReward` struct to paging parameters on the block view. |
||||
|
||||
## Parameters |
||||
- `reward` (`%__MODULE__{}`): The election reward struct. |
||||
|
||||
## Returns |
||||
- A map representing the block paging parameters. |
||||
|
||||
## Examples |
||||
|
||||
iex> ElectionReward.to_block_paging_params(%ElectionReward{amount: 1000, account_address_hash: "0x123", associated_account_address_hash: "0x456"}) |
||||
%{"amount" => 1000, "account_address_hash" => "0x123", "associated_account_address_hash" => "0x456"} |
||||
""" |
||||
def to_block_paging_params(%__MODULE__{ |
||||
amount: amount, |
||||
account_address_hash: account_address_hash, |
||||
associated_account_address_hash: associated_account_address_hash |
||||
}) do |
||||
%{ |
||||
"amount" => amount, |
||||
"account_address_hash" => account_address_hash, |
||||
"associated_account_address_hash" => associated_account_address_hash |
||||
} |
||||
end |
||||
|
||||
@doc """ |
||||
Converts an `ElectionReward` struct to paging parameters on the address view. |
||||
|
||||
## Parameters |
||||
- `reward` (`%__MODULE__{}`): The election reward struct. |
||||
|
||||
## Returns |
||||
- A map representing the address paging parameters. |
||||
|
||||
## Examples |
||||
|
||||
iex> ElectionReward.to_address_paging_params(%ElectionReward{block_number: 1, amount: 1000, associated_account_address_hash: "0x456", type: :voter}) |
||||
%{"block_number" => 1, "amount" => 1000, "associated_account_address_hash" => "0x456", "type" => :voter} |
||||
""" |
||||
def to_address_paging_params(%__MODULE__{ |
||||
block: %Block{number: block_number}, |
||||
amount: amount, |
||||
associated_account_address_hash: associated_account_address_hash, |
||||
type: type |
||||
}) do |
||||
%{ |
||||
"block_number" => block_number, |
||||
"amount" => amount, |
||||
"associated_account_address_hash" => associated_account_address_hash, |
||||
"type" => type |
||||
} |
||||
end |
||||
end |
@ -0,0 +1,117 @@ |
||||
defmodule Explorer.Chain.Celo.EpochReward do |
||||
@moduledoc """ |
||||
Represents the distributions in the Celo epoch. Each log index points to a |
||||
token transfer event in the `TokenTransfer` relation. These include the |
||||
reserve bolster, community, and carbon offsetting transfers. |
||||
""" |
||||
use Explorer.Schema |
||||
|
||||
import Ecto.Query, only: [from: 2] |
||||
import Explorer.Chain, only: [select_repo: 1] |
||||
|
||||
alias Explorer.Chain.Celo.EpochReward |
||||
alias Explorer.Chain.{Block, Hash, TokenTransfer} |
||||
|
||||
@required_attrs ~w(block_hash)a |
||||
@optional_attrs ~w(reserve_bolster_transfer_log_index community_transfer_log_index carbon_offsetting_transfer_log_index)a |
||||
@allowed_attrs @required_attrs ++ @optional_attrs |
||||
|
||||
@primary_key false |
||||
typed_schema "celo_epoch_rewards" do |
||||
field(:reserve_bolster_transfer_log_index, :integer) |
||||
field(:community_transfer_log_index, :integer) |
||||
field(:carbon_offsetting_transfer_log_index, :integer) |
||||
field(:reserve_bolster_transfer, :any, virtual: true) :: TokenTransfer.t() | nil |
||||
field(:community_transfer, :any, virtual: true) :: TokenTransfer.t() | nil |
||||
field(:carbon_offsetting_transfer, :any, virtual: true) :: TokenTransfer.t() | nil |
||||
|
||||
belongs_to( |
||||
:block, |
||||
Block, |
||||
primary_key: true, |
||||
foreign_key: :block_hash, |
||||
references: :hash, |
||||
type: Hash.Full, |
||||
null: false |
||||
) |
||||
|
||||
timestamps() |
||||
end |
||||
|
||||
@spec changeset(%__MODULE__{}, map()) :: Ecto.Changeset.t() |
||||
def changeset(%__MODULE__{} = rewards, attrs) do |
||||
rewards |
||||
|> cast(attrs, @allowed_attrs) |
||||
|> validate_required(@required_attrs) |
||||
|> foreign_key_constraint(:block_hash) |
||||
|> unique_constraint(:block_hash) |
||||
end |
||||
|
||||
@doc """ |
||||
Loads the token transfers for the given epoch reward. |
||||
|
||||
This function retrieves token transfers related to the specified epoch reward |
||||
by knowing the index of the log of the token transfer and populates the |
||||
virtual fields in the `EpochReward` struct. We manually preload token |
||||
transfers since Ecto does not support automatically preloading objects by |
||||
composite key (i.e., `log_index` and `block_hash`). |
||||
|
||||
## Parameters |
||||
- `epoch_reward` (`EpochReward.t()`): The epoch reward struct. |
||||
- `options` (`Keyword.t()`): Optional parameters for selecting the repository. |
||||
|
||||
## Returns |
||||
- `EpochReward.t()`: The epoch reward struct with the token transfers loaded. |
||||
|
||||
## Example |
||||
|
||||
iex> epoch_reward = %Explorer.Chain.Celo.EpochReward{block_hash: "some_hash", reserve_bolster_transfer_log_index: 1} |
||||
iex> Explorer.Chain.Celo.EpochReward.load_token_transfers(epoch_reward) |
||||
%Explorer.Chain.Celo.EpochReward{ |
||||
block_hash: "some_hash", |
||||
reserve_bolster_transfer_log_index: 1, |
||||
reserve_bolster_transfer: %Explorer.Chain.TokenTransfer{log_index: 1, ...} |
||||
} |
||||
""" |
||||
|
||||
@spec load_token_transfers(EpochReward.t()) :: EpochReward.t() |
||||
def load_token_transfers( |
||||
%EpochReward{ |
||||
reserve_bolster_transfer_log_index: reserve_bolster_transfer_log_index, |
||||
community_transfer_log_index: community_transfer_log_index, |
||||
carbon_offsetting_transfer_log_index: carbon_offsetting_transfer_log_index |
||||
} = epoch_reward, |
||||
options \\ [] |
||||
) do |
||||
virtual_field_to_log_index = [ |
||||
reserve_bolster_transfer: reserve_bolster_transfer_log_index, |
||||
community_transfer: community_transfer_log_index, |
||||
carbon_offsetting_transfer: carbon_offsetting_transfer_log_index |
||||
] |
||||
|
||||
log_indexes = |
||||
virtual_field_to_log_index |
||||
|> Enum.map(&elem(&1, 1)) |
||||
|> Enum.reject(&is_nil/1) |
||||
|
||||
query = |
||||
from( |
||||
tt in TokenTransfer.only_consensus_transfers_query(), |
||||
where: tt.log_index in ^log_indexes and tt.block_hash == ^epoch_reward.block_hash, |
||||
select: {tt.log_index, tt}, |
||||
preload: [ |
||||
:token, |
||||
[from_address: [:names, :smart_contract, :proxy_implementations]], |
||||
[to_address: [:names, :smart_contract, :proxy_implementations]] |
||||
] |
||||
) |
||||
|
||||
log_index_to_token_transfer = query |> select_repo(options).all() |> Map.new() |
||||
|
||||
Enum.reduce(virtual_field_to_log_index, epoch_reward, fn |
||||
{field, log_index}, acc -> |
||||
token_transfer = Map.get(log_index_to_token_transfer, log_index) |
||||
Map.put(acc, field, token_transfer) |
||||
end) |
||||
end |
||||
end |
@ -0,0 +1,94 @@ |
||||
defmodule Explorer.Chain.Celo.Helper do |
||||
@moduledoc """ |
||||
Common helper functions for Celo. |
||||
""" |
||||
|
||||
import Explorer.Chain.Cache.CeloCoreContracts, only: [atom_to_contract_name: 0] |
||||
|
||||
alias Explorer.Chain.Block |
||||
|
||||
@blocks_per_epoch 17_280 |
||||
@core_contract_atoms atom_to_contract_name() |> Map.keys() |
||||
|
||||
@doc """ |
||||
Returns the number of blocks per epoch in the Celo network. |
||||
""" |
||||
@spec blocks_per_epoch() :: non_neg_integer() |
||||
def blocks_per_epoch, do: @blocks_per_epoch |
||||
|
||||
defguard is_epoch_block_number(block_number) |
||||
when is_integer(block_number) and |
||||
block_number > 0 and |
||||
rem(block_number, @blocks_per_epoch) == 0 |
||||
|
||||
defguard is_core_contract_atom(atom) |
||||
when atom in @core_contract_atoms |
||||
|
||||
@doc """ |
||||
Validates if a block number is an epoch block number. |
||||
|
||||
## Parameters |
||||
- `block_number` (`Block.block_number()`): The block number to validate. |
||||
|
||||
## Returns |
||||
- `:ok` if the block number is an epoch block number. |
||||
- `{:error, :not_found}` if the block number is not an epoch block number. |
||||
|
||||
## Examples |
||||
|
||||
iex> Explorer.Chain.Celo.Helper.validate_epoch_block_number(17280) |
||||
:ok |
||||
|
||||
iex> Explorer.Chain.Celo.Helper.validate_epoch_block_number(17281) |
||||
{:error, :not_found} |
||||
""" |
||||
@spec validate_epoch_block_number(Block.block_number()) :: :ok | {:error, :not_found} |
||||
def validate_epoch_block_number(block_number) when is_epoch_block_number(block_number), |
||||
do: :ok |
||||
|
||||
def validate_epoch_block_number(_block_number), do: {:error, :not_found} |
||||
|
||||
@doc """ |
||||
Checks if a block number belongs to a block that finalized an epoch. |
||||
|
||||
## Parameters |
||||
- `block_number` (`Block.block_number()`): The block number to check. |
||||
|
||||
## Returns |
||||
- `boolean()`: `true` if the block number is an epoch block number, `false` |
||||
otherwise. |
||||
|
||||
## Examples |
||||
|
||||
iex> Explorer.Chain.Celo.Helper.epoch_block_number?(17280) |
||||
true |
||||
|
||||
iex> Explorer.Chain.Celo.Helper.epoch_block_number?(17281) |
||||
false |
||||
""" |
||||
@spec epoch_block_number?(block_number :: Block.block_number()) :: boolean() |
||||
def epoch_block_number?(block_number) when is_epoch_block_number(block_number), do: true |
||||
def epoch_block_number?(_), do: false |
||||
|
||||
@doc """ |
||||
Converts a block number to an epoch number. |
||||
|
||||
## Parameters |
||||
- `block_number` (`Block.block_number()`): The block number to convert. |
||||
|
||||
## Returns |
||||
- `non_neg_integer()`: The corresponding epoch number. |
||||
|
||||
## Examples |
||||
|
||||
iex> Explorer.Chain.Celo.Helper.block_number_to_epoch_number(17280) |
||||
1 |
||||
|
||||
iex> Explorer.Chain.Celo.Helper.block_number_to_epoch_number(17281) |
||||
2 |
||||
""" |
||||
@spec block_number_to_epoch_number(block_number :: Block.block_number()) :: non_neg_integer() |
||||
def block_number_to_epoch_number(block_number) when is_integer(block_number) do |
||||
(block_number / @blocks_per_epoch) |> Float.ceil() |> trunc() |
||||
end |
||||
end |
@ -0,0 +1,74 @@ |
||||
defmodule Explorer.Chain.Celo.PendingEpochBlockOperation do |
||||
@moduledoc """ |
||||
Tracks an epoch block that has pending operation. |
||||
""" |
||||
|
||||
use Explorer.Schema |
||||
|
||||
import Explorer.Chain, only: [add_fetcher_limit: 2] |
||||
alias Explorer.Chain.{Block, Hash} |
||||
alias Explorer.Repo |
||||
|
||||
@required_attrs ~w(block_hash)a |
||||
|
||||
@typedoc """ |
||||
* `block_hash` - the hash of the block that has pending epoch operations. |
||||
""" |
||||
@primary_key false |
||||
typed_schema "celo_pending_epoch_block_operations" do |
||||
belongs_to(:block, Block, |
||||
foreign_key: :block_hash, |
||||
primary_key: true, |
||||
references: :hash, |
||||
type: Hash.Full, |
||||
null: false |
||||
) |
||||
|
||||
timestamps() |
||||
end |
||||
|
||||
def changeset(%__MODULE__{} = pending_ops, attrs) do |
||||
pending_ops |
||||
|> cast(attrs, @required_attrs) |
||||
|> validate_required(@required_attrs) |
||||
|> foreign_key_constraint(:block_hash) |
||||
|> unique_constraint(:block_hash, name: :pending_epoch_block_operations_pkey) |
||||
end |
||||
|
||||
@doc """ |
||||
Returns a stream of all blocks with unfetched epochs, using |
||||
the `celo_pending_epoch_block_operations` table. |
||||
|
||||
iex> unfetched = insert(:block, number: 1 * blocks_per_epoch()) |
||||
iex> insert(:celo_pending_epoch_block_operation, block: unfetched) |
||||
iex> {:ok, blocks} = PendingEpochBlockOperation.stream_epoch_blocks_with_unfetched_rewards( |
||||
...> [], |
||||
...> fn block, acc -> |
||||
...> [block | acc] |
||||
...> end |
||||
...> ) |
||||
iex> [{unfetched.number, unfetched.hash}] == blocks |
||||
true |
||||
""" |
||||
@spec stream_epoch_blocks_with_unfetched_rewards( |
||||
initial :: accumulator, |
||||
reducer :: (entry :: term(), accumulator -> accumulator), |
||||
limited? :: boolean() |
||||
) :: {:ok, accumulator} |
||||
when accumulator: term() |
||||
def stream_epoch_blocks_with_unfetched_rewards(initial, reducer, limited? \\ false) |
||||
when is_function(reducer, 2) do |
||||
query = |
||||
from( |
||||
op in __MODULE__, |
||||
join: block in assoc(op, :block), |
||||
select: %{block_number: block.number, block_hash: block.hash}, |
||||
where: block.consensus == true, |
||||
order_by: [desc: block.number] |
||||
) |
||||
|
||||
query |
||||
|> add_fetcher_limit(limited?) |
||||
|> Repo.stream_reduce(initial, reducer) |
||||
end |
||||
end |
@ -0,0 +1,193 @@ |
||||
defmodule Explorer.Chain.Celo.Reader do |
||||
@moduledoc """ |
||||
Read functions for Celo modules. |
||||
""" |
||||
|
||||
import Ecto.Query, only: [limit: 2] |
||||
|
||||
import Explorer.Chain, |
||||
only: [ |
||||
select_repo: 1, |
||||
join_associations: 2, |
||||
default_paging_options: 0 |
||||
] |
||||
|
||||
alias Explorer.Chain.Cache.CeloCoreContracts |
||||
alias Explorer.Chain.Celo.ElectionReward |
||||
alias Explorer.Chain.{Hash, Token, Wei} |
||||
|
||||
@election_reward_types ElectionReward.types() |
||||
@default_paging_options default_paging_options() |
||||
|
||||
@doc """ |
||||
Retrieves election rewards associated with a given address hash. |
||||
|
||||
## Parameters |
||||
- `address_hash` (`Hash.Address.t()`): The address hash to search for election |
||||
rewards. |
||||
- `options` (`Keyword.t()`): Optional parameters for fetching data. |
||||
|
||||
## Returns |
||||
- `[ElectionReward.t()]`: A list of election rewards associated with the |
||||
address hash. |
||||
|
||||
## Examples |
||||
|
||||
iex> address_hash = %Hash.Address{ |
||||
...> byte_count: 20, |
||||
...> bytes: <<0x1d1f7f0e1441c37e28b89e0b5e1edbbd34d77649 :: size(160)>> |
||||
...> } |
||||
iex> Explorer.Chain.Celo.Reader.address_hash_to_election_rewards(address_hash) |
||||
[%ElectionReward{}, ...] |
||||
""" |
||||
@spec address_hash_to_election_rewards( |
||||
Hash.Address.t(), |
||||
Keyword.t() |
||||
) :: [ElectionReward.t()] |
||||
def address_hash_to_election_rewards(address_hash, options \\ []) do |
||||
necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) |
||||
paging_options = Keyword.get(options, :paging_options, @default_paging_options) |
||||
|
||||
address_hash |
||||
|> ElectionReward.address_hash_to_ordered_rewards_query() |
||||
|> ElectionReward.paginate(paging_options) |
||||
|> limit(^paging_options.page_size) |
||||
|> join_associations(necessity_by_association) |
||||
|> select_repo(options).all() |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves election rewards by block hash and reward type. |
||||
|
||||
## Parameters |
||||
- `block_hash` (`Hash.t()`): The block hash to search for election rewards. |
||||
- `reward_type` (`ElectionReward.type()`): The type of reward to filter. |
||||
- `options` (`Keyword.t()`): Optional parameters for fetching data. |
||||
|
||||
## Returns |
||||
- `[ElectionReward.t()]`: A list of election rewards filtered by block hash |
||||
and reward type. |
||||
|
||||
## Examples |
||||
|
||||
iex> block_hash = %Hash.Full{ |
||||
...> byte_count: 32, |
||||
...> bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>> |
||||
...> } |
||||
iex> Explorer.Chain.Celo.Reader.block_hash_to_election_rewards_by_type(block_hash, :voter_reward) |
||||
[%ElectionReward{}, ...] |
||||
""" |
||||
@spec block_hash_to_election_rewards_by_type( |
||||
Hash.t(), |
||||
ElectionReward.type(), |
||||
Keyword.t() |
||||
) :: [ElectionReward.t()] |
||||
def block_hash_to_election_rewards_by_type(block_hash, reward_type, options \\ []) |
||||
when reward_type in @election_reward_types do |
||||
necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) |
||||
paging_options = Keyword.get(options, :paging_options, @default_paging_options) |
||||
|
||||
block_hash |
||||
|> ElectionReward.block_hash_to_rewards_by_type_query(reward_type) |
||||
|> ElectionReward.paginate(paging_options) |
||||
|> limit(^paging_options.page_size) |
||||
|> join_associations(necessity_by_association) |
||||
|> select_repo(options).all() |
||||
end |
||||
|
||||
@doc """ |
||||
Retrieves aggregated election rewards by block hash. |
||||
|
||||
## Parameters |
||||
- `block_hash` (`Hash.Full.t()`): The block hash to aggregate election |
||||
rewards. |
||||
- `options` (`Keyword.t()`): Optional parameters for fetching data. |
||||
|
||||
## Returns |
||||
- `%{atom() => Wei.t() | nil}`: A map of aggregated election rewards by type. |
||||
|
||||
## Examples |
||||
|
||||
iex> block_hash = %Hash.Full{ |
||||
...> byte_count: 32, |
||||
...> bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>> |
||||
...> } |
||||
iex> Explorer.Chain.Celo.Reader.block_hash_to_aggregated_election_rewards_by_type(block_hash) |
||||
%{voter_reward: %{total: %Decimal{}, count: 2}, ...} |
||||
""" |
||||
@spec block_hash_to_aggregated_election_rewards_by_type( |
||||
Hash.Full.t(), |
||||
Keyword.t() |
||||
) :: %{atom() => Wei.t() | nil} |
||||
def block_hash_to_aggregated_election_rewards_by_type(block_hash, options \\ []) do |
||||
reward_type_to_token = |
||||
block_hash_to_election_reward_token_addresses_by_type( |
||||
block_hash, |
||||
options |
||||
) |
||||
|
||||
reward_type_to_aggregated_rewards = |
||||
block_hash |
||||
|> ElectionReward.block_hash_to_aggregated_rewards_by_type_query() |
||||
|> select_repo(options).all() |
||||
|> Map.new(fn {type, total, count} -> |
||||
{type, %{total: total, count: count}} |
||||
end) |
||||
|
||||
ElectionReward.types() |
||||
|> Map.new(&{&1, %{total: Decimal.new(0), count: 0}}) |
||||
|> Map.merge(reward_type_to_aggregated_rewards) |
||||
|> Map.new(fn {type, aggregated_reward} -> |
||||
token = Map.get(reward_type_to_token, type) |
||||
aggregated_reward_with_token = Map.put(aggregated_reward, :token, token) |
||||
{type, aggregated_reward_with_token} |
||||
end) |
||||
end |
||||
|
||||
# Retrieves the token for each type of election reward on the given block. |
||||
# |
||||
# ## Parameters |
||||
# - `block_hash` (`Hash.Full.t()`): The block hash to search for token |
||||
# addresses. |
||||
# - `options` (`Keyword.t()`): Optional parameters for fetching data. |
||||
# |
||||
# ## Returns |
||||
# - `%{atom() => Token.t() | nil}`: A map of reward types to token. |
||||
# |
||||
# ## Examples |
||||
# |
||||
# iex> block_hash = %Hash.Full{ |
||||
# ...> byte_count: 32, |
||||
# ...> bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>> |
||||
# ...> } |
||||
# iex> Explorer.Chain.Celo.Reader.block_hash_to_election_reward_token_addresses_by_type(block_hash) |
||||
# %{voter_reward: %Token{}, ...} |
||||
@spec block_hash_to_election_reward_token_addresses_by_type( |
||||
Hash.Full.t(), |
||||
Keyword.t() |
||||
) :: %{atom() => Token.t() | nil} |
||||
defp block_hash_to_election_reward_token_addresses_by_type(block_hash, options) do |
||||
contract_address_hash_to_atom = |
||||
ElectionReward.reward_type_atom_to_token_atom() |
||||
|> Map.values() |
||||
|> Map.new(fn token_atom -> |
||||
{:ok, contract_address_hash} = CeloCoreContracts.get_address(token_atom, block_hash) |
||||
{contract_address_hash, token_atom} |
||||
end) |
||||
|
||||
token_atom_to_token = |
||||
contract_address_hash_to_atom |
||||
|> Map.keys() |
||||
|> Token.get_by_contract_address_hashes(options) |
||||
|> Map.new(fn token -> |
||||
hash = to_string(token.contract_address_hash) |
||||
atom = contract_address_hash_to_atom[hash] |
||||
{atom, token} |
||||
end) |
||||
|
||||
ElectionReward.reward_type_atom_to_token_atom() |
||||
|> Map.new(fn {reward_type_atom, token_atom} -> |
||||
{reward_type_atom, Map.get(token_atom_to_token, token_atom)} |
||||
end) |
||||
end |
||||
end |
@ -0,0 +1,76 @@ |
||||
defmodule Explorer.Chain.Celo.ValidatorGroupVote do |
||||
@moduledoc """ |
||||
Represents the information about a vote for a validator group made by an |
||||
account. |
||||
""" |
||||
|
||||
use Explorer.Schema |
||||
|
||||
alias Explorer.Chain.{Address, Block, Hash, Transaction} |
||||
|
||||
@types_enum ~w(activated revoked)a |
||||
@required_attrs ~w(account_address_hash group_address_hash type transaction_hash block_hash block_number)a |
||||
|
||||
@typedoc """ |
||||
* `account_address_hash` - the address of the account that made the vote. |
||||
* `group_address_hash` - the address of the validator group that |
||||
was voted for. |
||||
* `type` - whether this vote is `activated` or `revoked`. |
||||
* `block_number` - the block number of the vote. |
||||
* `block_hash` - the hash of the block that contains the vote. |
||||
* `transaction_hash` - the hash of the transaction that made the vote. |
||||
""" |
||||
@primary_key false |
||||
typed_schema "celo_validator_group_votes" do |
||||
belongs_to(:account_address, Address, |
||||
foreign_key: :account_address_hash, |
||||
references: :hash, |
||||
type: Hash.Address |
||||
) |
||||
|
||||
belongs_to(:group_address, Address, |
||||
foreign_key: :group_address_hash, |
||||
references: :hash, |
||||
type: Hash.Address |
||||
) |
||||
|
||||
field(:type, Ecto.Enum, |
||||
values: @types_enum, |
||||
null: false |
||||
) |
||||
|
||||
field(:block_number, :integer, null: false) |
||||
|
||||
belongs_to(:block, Block, |
||||
foreign_key: :block_hash, |
||||
primary_key: true, |
||||
references: :hash, |
||||
type: Hash.Full, |
||||
null: false |
||||
) |
||||
|
||||
belongs_to(:transaction, Transaction, |
||||
foreign_key: :transaction_hash, |
||||
primary_key: true, |
||||
references: :hash, |
||||
type: Hash.Full, |
||||
null: false |
||||
) |
||||
|
||||
timestamps() |
||||
end |
||||
|
||||
@spec changeset( |
||||
__MODULE__.t(), |
||||
:invalid | %{optional(:__struct__) => none, optional(atom | binary) => any} |
||||
) :: Ecto.Changeset.t() |
||||
def changeset(%__MODULE__{} = vote, attrs \\ %{}) do |
||||
vote |
||||
|> cast(attrs, @required_attrs) |
||||
|> validate_required(@required_attrs) |
||||
|> foreign_key_constraint(:account_address_hash) |
||||
|> foreign_key_constraint(:group_address_hash) |
||||
|> foreign_key_constraint(:block_hash) |
||||
|> foreign_key_constraint(:transaction_hash) |
||||
end |
||||
end |
@ -0,0 +1,93 @@ |
||||
defmodule Explorer.Chain.Import.Runner.Celo.ElectionRewards do |
||||
@moduledoc """ |
||||
Bulk imports `t:Explorer.Chain.Celo.ElectionReward.t/0`. |
||||
""" |
||||
|
||||
require Ecto.Query |
||||
|
||||
alias Ecto.{Changeset, Multi, Repo} |
||||
alias Explorer.Chain.Celo.ElectionReward |
||||
alias Explorer.Chain.Import |
||||
alias Explorer.Prometheus.Instrumenter |
||||
|
||||
@behaviour Import.Runner |
||||
|
||||
# milliseconds |
||||
@timeout 60_000 |
||||
|
||||
@type imported :: [ElectionReward.t()] |
||||
|
||||
@impl Import.Runner |
||||
def ecto_schema_module, do: ElectionReward |
||||
|
||||
@impl Import.Runner |
||||
def option_key, do: :celo_election_rewards |
||||
|
||||
@impl Import.Runner |
||||
@spec imported_table_row() :: %{:value_description => binary(), :value_type => binary()} |
||||
def imported_table_row do |
||||
%{ |
||||
value_type: "[#{ecto_schema_module()}.t()]", |
||||
value_description: "List of `t:#{ecto_schema_module()}.t/0`s" |
||||
} |
||||
end |
||||
|
||||
@impl Import.Runner |
||||
@spec run(Multi.t(), list(), map()) :: Multi.t() |
||||
def run(multi, changes_list, %{timestamps: timestamps} = options) do |
||||
insert_options = |
||||
options |
||||
|> Map.get(option_key(), %{}) |
||||
|> Map.take(~w(on_conflict timeout)a) |
||||
|> Map.put_new(:timeout, @timeout) |
||||
|> Map.put(:timestamps, timestamps) |
||||
|
||||
Multi.run(multi, :insert_celo_election_rewards, fn repo, _ -> |
||||
Instrumenter.block_import_stage_runner( |
||||
fn -> insert(repo, changes_list, insert_options) end, |
||||
:block_referencing, |
||||
:celo_election_rewards, |
||||
:celo_election_rewards |
||||
) |
||||
end) |
||||
end |
||||
|
||||
@impl Import.Runner |
||||
def timeout, do: @timeout |
||||
|
||||
@spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: |
||||
{:ok, [ElectionReward.t()]} |
||||
| {:error, [Changeset.t()]} |
||||
def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = _options) when is_list(changes_list) do |
||||
# Enforce Celo.Epoch.ElectionReward ShareLocks order (see docs: sharelock.md) |
||||
ordered_changes_list = |
||||
Enum.sort_by( |
||||
changes_list, |
||||
&{ |
||||
&1.block_hash, |
||||
&1.type, |
||||
&1.account_address_hash, |
||||
&1.associated_account_address_hash |
||||
} |
||||
) |
||||
|
||||
{:ok, inserted} = |
||||
Import.insert_changes_list( |
||||
repo, |
||||
ordered_changes_list, |
||||
for: ElectionReward, |
||||
returning: true, |
||||
timeout: timeout, |
||||
timestamps: timestamps, |
||||
conflict_target: [ |
||||
:block_hash, |
||||
:type, |
||||
:account_address_hash, |
||||
:associated_account_address_hash |
||||
], |
||||
on_conflict: :replace_all |
||||
) |
||||
|
||||
{:ok, inserted} |
||||
end |
||||
end |
@ -0,0 +1,139 @@ |
||||
defmodule Explorer.Chain.Import.Runner.Celo.EpochRewards do |
||||
@moduledoc """ |
||||
Bulk imports `t:Explorer.Chain.Celo.EpochReward.t/0`. |
||||
""" |
||||
|
||||
require Ecto.Query |
||||
|
||||
alias Ecto.{Changeset, Multi, Repo} |
||||
alias Explorer.Chain.Celo.{EpochReward, PendingEpochBlockOperation} |
||||
alias Explorer.Chain.Import |
||||
alias Explorer.Prometheus.Instrumenter |
||||
|
||||
import Ecto.Query |
||||
|
||||
@behaviour Import.Runner |
||||
|
||||
# milliseconds |
||||
@timeout 60_000 |
||||
|
||||
@type imported :: [EpochReward.t()] |
||||
|
||||
@impl Import.Runner |
||||
def ecto_schema_module, do: EpochReward |
||||
|
||||
@impl Import.Runner |
||||
def option_key, do: :celo_epoch_rewards |
||||
|
||||
@impl Import.Runner |
||||
@spec imported_table_row() :: %{:value_description => binary(), :value_type => binary()} |
||||
def imported_table_row do |
||||
%{ |
||||
value_type: "[#{ecto_schema_module()}.t()]", |
||||
value_description: "List of `t:#{ecto_schema_module()}.t/0`s" |
||||
} |
||||
end |
||||
|
||||
@impl Import.Runner |
||||
@spec run(Multi.t(), list(), map()) :: Multi.t() |
||||
def run(multi, changes_list, %{timestamps: timestamps} = options) do |
||||
insert_options = |
||||
options |
||||
|> Map.get(option_key(), %{}) |
||||
|> Map.take(~w(on_conflict timeout)a) |
||||
|> Map.put_new(:timeout, @timeout) |
||||
|> Map.put(:timestamps, timestamps) |
||||
|
||||
multi |
||||
|> Multi.run(:acquire_pending_epoch_block_operations, fn repo, _ -> |
||||
Instrumenter.block_import_stage_runner( |
||||
fn -> acquire_pending_epoch_block_operations(repo, changes_list) end, |
||||
:block_pending, |
||||
:celo_epoch_rewards, |
||||
:acquire_pending_epoch_block_operations |
||||
) |
||||
end) |
||||
|> Multi.run(:insert_celo_epoch_rewards, fn repo, _ -> |
||||
Instrumenter.block_import_stage_runner( |
||||
fn -> insert(repo, changes_list, insert_options) end, |
||||
:block_referencing, |
||||
:celo_epoch_rewards, |
||||
:celo_epoch_rewards |
||||
) |
||||
end) |
||||
|> Multi.run( |
||||
:delete_pending_epoch_block_operations, |
||||
fn repo, |
||||
%{ |
||||
acquire_pending_epoch_block_operations: pending_block_hashes |
||||
} -> |
||||
Instrumenter.block_import_stage_runner( |
||||
fn -> delete_pending_epoch_block_operations(repo, pending_block_hashes) end, |
||||
:block_pending, |
||||
:celo_epoch_rewards, |
||||
:delete_pending_epoch_block_operations |
||||
) |
||||
end |
||||
) |
||||
end |
||||
|
||||
@impl Import.Runner |
||||
def timeout, do: @timeout |
||||
|
||||
@spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: |
||||
{:ok, [EpochReward.t()]} |
||||
| {:error, [Changeset.t()]} |
||||
def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = _options) when is_list(changes_list) do |
||||
# Enforce Celo.EpochReward ShareLocks order (see docs: sharelock.md) |
||||
ordered_changes_list = Enum.sort_by(changes_list, & &1.block_hash) |
||||
|
||||
{:ok, inserted} = |
||||
Import.insert_changes_list( |
||||
repo, |
||||
ordered_changes_list, |
||||
for: EpochReward, |
||||
returning: true, |
||||
timeout: timeout, |
||||
timestamps: timestamps, |
||||
conflict_target: :block_hash, |
||||
on_conflict: :replace_all |
||||
) |
||||
|
||||
{:ok, inserted} |
||||
end |
||||
|
||||
def acquire_pending_epoch_block_operations(repo, changes_list) do |
||||
block_hashes = Enum.map(changes_list, & &1.block_hash) |
||||
|
||||
query = |
||||
from( |
||||
pending_ops in PendingEpochBlockOperation, |
||||
where: pending_ops.block_hash in ^block_hashes, |
||||
select: pending_ops.block_hash, |
||||
# Enforce PendingBlockOperation ShareLocks order (see docs: sharelocks.md) |
||||
order_by: [asc: pending_ops.block_hash], |
||||
lock: "FOR UPDATE" |
||||
) |
||||
|
||||
{:ok, repo.all(query)} |
||||
end |
||||
|
||||
def delete_pending_epoch_block_operations(repo, block_hashes) do |
||||
delete_query = |
||||
from( |
||||
pending_ops in PendingEpochBlockOperation, |
||||
where: pending_ops.block_hash in ^block_hashes |
||||
) |
||||
|
||||
try do |
||||
# ShareLocks order already enforced by |
||||
# `acquire_pending_epoch_block_operations` (see docs: sharelocks.md) |
||||
{_count, deleted} = repo.delete_all(delete_query, []) |
||||
|
||||
{:ok, deleted} |
||||
rescue |
||||
postgrex_error in Postgrex.Error -> |
||||
{:error, %{exception: postgrex_error, pending_hashes: block_hashes}} |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,87 @@ |
||||
defmodule Explorer.Chain.Import.Runner.Celo.ValidatorGroupVotes do |
||||
@moduledoc """ |
||||
Bulk imports `t:Explorer.Chain.Celo.ValidatorGroupVote.t/0`. |
||||
""" |
||||
|
||||
require Ecto.Query |
||||
|
||||
alias Ecto.{Changeset, Multi, Repo} |
||||
alias Explorer.Chain.Celo.ValidatorGroupVote |
||||
alias Explorer.Chain.Import |
||||
alias Explorer.Prometheus.Instrumenter |
||||
|
||||
@behaviour Import.Runner |
||||
|
||||
# milliseconds |
||||
@timeout 60_000 |
||||
|
||||
@type imported :: [ValidatorGroupVote.t()] |
||||
|
||||
@impl Import.Runner |
||||
def ecto_schema_module, do: ValidatorGroupVote |
||||
|
||||
@impl Import.Runner |
||||
def option_key, do: :celo_validator_group_votes |
||||
|
||||
@impl Import.Runner |
||||
@spec imported_table_row() :: %{:value_description => binary(), :value_type => binary()} |
||||
def imported_table_row do |
||||
%{ |
||||
value_type: "[#{ecto_schema_module()}.t()]", |
||||
value_description: "List of `t:#{ecto_schema_module()}.t/0`s" |
||||
} |
||||
end |
||||
|
||||
@impl Import.Runner |
||||
@spec run(Multi.t(), list(), map()) :: Multi.t() |
||||
def run(multi, changes_list, %{timestamps: timestamps} = options) do |
||||
insert_options = |
||||
options |
||||
|> Map.get(option_key(), %{}) |
||||
|> Map.take(~w(on_conflict timeout)a) |
||||
|> Map.put_new(:timeout, @timeout) |
||||
|> Map.put(:timestamps, timestamps) |
||||
|
||||
Multi.run(multi, :insert_celo_validator_group_votes, fn repo, _ -> |
||||
Instrumenter.block_import_stage_runner( |
||||
fn -> insert(repo, changes_list, insert_options) end, |
||||
:block_referencing, |
||||
:celo_validator_group_votes, |
||||
:celo_validator_group_votes |
||||
) |
||||
end) |
||||
end |
||||
|
||||
@impl Import.Runner |
||||
def timeout, do: @timeout |
||||
|
||||
@spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: |
||||
{:ok, [ValidatorGroupVote.t()]} |
||||
| {:error, [Changeset.t()]} |
||||
def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = _options) when is_list(changes_list) do |
||||
# Enforce Celo.Epoch.ValidatorGroupVote ShareLocks order (see docs: sharelock.md) |
||||
ordered_changes_list = |
||||
Enum.sort_by( |
||||
changes_list, |
||||
&{ |
||||
&1.transaction_hash, |
||||
&1.account_address_hash, |
||||
&1.group_address_hash |
||||
} |
||||
) |
||||
|
||||
{:ok, inserted} = |
||||
Import.insert_changes_list( |
||||
repo, |
||||
ordered_changes_list, |
||||
for: ValidatorGroupVote, |
||||
returning: true, |
||||
timeout: timeout, |
||||
timestamps: timestamps, |
||||
conflict_target: :transaction_hash, |
||||
on_conflict: :replace_all |
||||
) |
||||
|
||||
{:ok, inserted} |
||||
end |
||||
end |
@ -0,0 +1,236 @@ |
||||
defmodule Mix.Tasks.FetchCeloCoreContracts do |
||||
@moduledoc """ |
||||
Fetch the addresses of celo core contracts: `mix help celo-contracts` |
||||
""" |
||||
@shortdoc "Fetches the addresses of celo core contracts" |
||||
|
||||
use Mix.Task |
||||
|
||||
import Explorer.Helper, |
||||
only: [ |
||||
decode_data: 2, |
||||
truncate_address_hash: 1 |
||||
] |
||||
|
||||
alias Mix.Task |
||||
|
||||
alias EthereumJSONRPC.Logs |
||||
alias Explorer.Chain.Cache.CeloCoreContracts |
||||
alias Indexer.Helper, as: IndexerHelper |
||||
|
||||
@registry_proxy_contract_address "0x000000000000000000000000000000000000ce10" |
||||
@registry_updated_event_signature "0x4166d073a7a5e704ce0db7113320f88da2457f872d46dc020c805c562c1582a0" |
||||
@carbon_offsetting_fund_set_event_signature "0xe296227209b47bb8f4a76768ebd564dcde1c44be325a5d262f27c1fd4fd4538b" |
||||
@fee_beneficiary_set_event_signature "0xf7015098f8d6fa48f0560725ffd5f81bf687ee5ac45153b590bdcb04648bbdd3" |
||||
@burn_fraction_set_event_signature "0x41c679f4bcdc2c95f79a3647e2237162d9763d86685ef6c667781230c8ba9157" |
||||
|
||||
@chunk_size 200_000 |
||||
@max_request_retries 3 |
||||
|
||||
def run(_) do |
||||
Task.run("app.start") |
||||
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) |
||||
atom_to_contract_name = CeloCoreContracts.atom_to_contract_name() |
||||
atom_to_contract_event_names = CeloCoreContracts.atom_to_contract_event_names() |
||||
contract_names = atom_to_contract_name |> Map.values() |
||||
{:ok, latest_block_number} = EthereumJSONRPC.fetch_block_number_by_tag("latest", json_rpc_named_arguments) |
||||
|
||||
core_contract_addresses = |
||||
0..latest_block_number |
||||
|> fetch_logs_by_chunks( |
||||
fn chunk_start, chunk_end -> |
||||
[ |
||||
Logs.request( |
||||
0, |
||||
%{ |
||||
from_block: chunk_start, |
||||
to_block: chunk_end, |
||||
address: @registry_proxy_contract_address, |
||||
topics: [@registry_updated_event_signature] |
||||
} |
||||
) |
||||
] |
||||
end, |
||||
json_rpc_named_arguments |
||||
) |
||||
|> Enum.reduce(%{}, fn log, acc -> |
||||
[contract_name] = decode_data(log.data, [:string]) |
||||
new_contract_address = truncate_address_hash(log.third_topic) |
||||
|
||||
entry = %{ |
||||
address: new_contract_address, |
||||
updated_at_block_number: log.block_number |
||||
} |
||||
|
||||
if contract_name in contract_names do |
||||
acc |
||||
|> Map.update( |
||||
contract_name, |
||||
[entry], |
||||
&[entry | &1] |
||||
) |
||||
else |
||||
acc |
||||
end |
||||
end) |
||||
|> Map.new(fn {contract_name, entries} -> |
||||
{contract_name, Enum.reverse(entries)} |
||||
end) |
||||
|
||||
epoch_rewards_events = |
||||
[@carbon_offsetting_fund_set_event_signature] |
||||
|> fetch_events_for_contract( |
||||
:epoch_rewards, |
||||
core_contract_addresses, |
||||
latest_block_number, |
||||
json_rpc_named_arguments |
||||
) |
||||
|> Map.new(fn {address, logs} -> |
||||
entries = |
||||
logs |
||||
|> Enum.map( |
||||
&%{ |
||||
address: truncate_address_hash(&1.second_topic), |
||||
updated_at_block_number: &1.block_number |
||||
} |
||||
) |
||||
|
||||
event_name = atom_to_contract_event_names[:epoch_rewards][:carbon_offsetting_fund_set] |
||||
{address, %{event_name => entries}} |
||||
end) |
||||
|
||||
fee_handler_events = |
||||
[ |
||||
@fee_beneficiary_set_event_signature, |
||||
@burn_fraction_set_event_signature |
||||
] |
||||
|> fetch_events_for_contract( |
||||
:fee_handler, |
||||
core_contract_addresses, |
||||
latest_block_number, |
||||
json_rpc_named_arguments |
||||
) |
||||
|> Map.new(fn {address, logs} -> |
||||
topic_to_logs = logs |> Enum.group_by(& &1.first_topic) |
||||
|
||||
fee_beneficiary_set_event_name = atom_to_contract_event_names[:fee_handler][:fee_beneficiary_set] |
||||
burn_fraction_set_event_name = atom_to_contract_event_names[:fee_handler][:burn_fraction_set] |
||||
|
||||
{ |
||||
address, |
||||
%{ |
||||
fee_beneficiary_set_event_name => |
||||
topic_to_logs |
||||
|> Map.get(@fee_beneficiary_set_event_signature, []) |
||||
|> Enum.map( |
||||
&%{ |
||||
address: truncate_address_hash(&1.data), |
||||
updated_at_block_number: &1.block_number |
||||
} |
||||
), |
||||
burn_fraction_set_event_name => |
||||
topic_to_logs |
||||
|> Map.get(@burn_fraction_set_event_signature, []) |
||||
|> Enum.map(fn log -> |
||||
[fraction] = decode_data(log.data, [{:int, 256}]) |
||||
|
||||
%{ |
||||
value: fraction, |
||||
updated_at_block_number: log.block_number |
||||
} |
||||
end) |
||||
} |
||||
} |
||||
end) |
||||
|
||||
core_contracts_json = |
||||
%{ |
||||
"addresses" => core_contract_addresses, |
||||
"events" => %{ |
||||
atom_to_contract_name[:epoch_rewards] => epoch_rewards_events, |
||||
atom_to_contract_name[:fee_handler] => fee_handler_events |
||||
} |
||||
} |
||||
|> Jason.encode!() |
||||
|
||||
IO.puts("CELO_CORE_CONTRACTS=#{core_contracts_json}") |
||||
end |
||||
|
||||
defp fetch_logs_by_chunks(from_block..to_block, requests_func, json_rpc_named_arguments) do |
||||
from_block..to_block |
||||
|> IndexerHelper.range_chunk_every(@chunk_size) |
||||
|> Enum.reduce([], fn chunk_start..chunk_end, acc -> |
||||
IndexerHelper.log_blocks_chunk_handling(chunk_start, chunk_end, 0, to_block, nil, :L1) |
||||
|
||||
requests = requests_func.(chunk_start, chunk_end) |
||||
|
||||
{:ok, responses} = |
||||
IndexerHelper.repeated_batch_rpc_call( |
||||
requests, |
||||
json_rpc_named_arguments, |
||||
fn message -> "Could not fetch logs: #{message}" end, |
||||
@max_request_retries |
||||
) |
||||
|
||||
{:ok, logs} = Logs.from_responses(responses) |
||||
|
||||
acc ++ logs |
||||
end) |
||||
end |
||||
|
||||
defp fetch_events_for_contract( |
||||
event_signatures, |
||||
contract_atom, |
||||
core_contract_addresses, |
||||
latest_block_number, |
||||
json_rpc_named_arguments |
||||
) do |
||||
contract_name = |
||||
CeloCoreContracts.atom_to_contract_name() |
||||
|> Map.get(contract_atom) |
||||
|
||||
core_contract_addresses |
||||
|> Map.get(contract_name, []) |
||||
|> Enum.chunk_every(2, 1) |
||||
|> Enum.map(fn |
||||
[entry, %{updated_at_block_number: to_block}] -> |
||||
{entry, to_block} |
||||
|
||||
[entry] -> |
||||
{entry, latest_block_number} |
||||
end) |
||||
|> Enum.map(fn {%{address: address}, to_block} -> |
||||
logs = |
||||
fetch_events_for_address( |
||||
0..to_block, |
||||
event_signatures, |
||||
address, |
||||
json_rpc_named_arguments |
||||
) |
||||
|
||||
{address, logs} |
||||
end) |
||||
end |
||||
|
||||
defp fetch_events_for_address(chunk_range, event_signatures, address, json_rpc_named_arguments) do |
||||
fetch_logs_by_chunks( |
||||
chunk_range, |
||||
fn chunk_start, chunk_end -> |
||||
event_signatures |
||||
|> Enum.with_index() |
||||
|> Enum.map(fn {signature, index} -> |
||||
Logs.request( |
||||
index, |
||||
%{ |
||||
from_block: chunk_start, |
||||
to_block: chunk_end, |
||||
address: address, |
||||
topics: [signature] |
||||
} |
||||
) |
||||
end) |
||||
end, |
||||
json_rpc_named_arguments |
||||
) |
||||
end |
||||
end |
@ -0,0 +1,11 @@ |
||||
defmodule Explorer.Repo.Celo.Migrations.AddCustomFields do |
||||
use Ecto.Migration |
||||
|
||||
def change do |
||||
alter table(:transactions) do |
||||
add(:gateway_fee, :numeric, precision: 100, null: true) |
||||
add(:gas_token_contract_address_hash, :bytea, null: true) |
||||
add(:gas_fee_recipient_address_hash, :bytea, null: true) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,14 @@ |
||||
defmodule Explorer.Repo.Celo.Migrations.AddPendingEpochBlockOperations do |
||||
use Ecto.Migration |
||||
|
||||
def change do |
||||
create table(:celo_pending_epoch_block_operations, primary_key: false) do |
||||
add(:block_hash, references(:blocks, column: :hash, type: :bytea, on_delete: :delete_all), |
||||
null: false, |
||||
primary_key: true |
||||
) |
||||
|
||||
timestamps() |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,26 @@ |
||||
defmodule Explorer.Repo.Celo.Migrations.RemoveTransactionHashFromPrimaryKeyInLogs do |
||||
use Ecto.Migration |
||||
|
||||
def change do |
||||
execute( |
||||
""" |
||||
ALTER TABLE logs |
||||
DROP CONSTRAINT logs_pkey, |
||||
ADD PRIMARY KEY (block_hash, index); |
||||
""", |
||||
""" |
||||
ALTER TABLE logs |
||||
DROP CONSTRAINT logs_pkey, |
||||
ADD PRIMARY KEY (transaction_hash, block_hash, index); |
||||
""" |
||||
) |
||||
|
||||
execute( |
||||
"ALTER TABLE logs ALTER COLUMN transaction_hash DROP NOT NULL", |
||||
"ALTER TABLE logs ALTER COLUMN transaction_hash SET NOT NULL" |
||||
) |
||||
|
||||
drop(unique_index(:logs, [:transaction_hash, :index])) |
||||
create_if_not_exists(index(:logs, [:transaction_hash, :index])) |
||||
end |
||||
end |
@ -0,0 +1,23 @@ |
||||
defmodule Explorer.Repo.Celo.Migrations.RemoveTransactionHashFromPrimaryKeyInTokenTransfers do |
||||
use Ecto.Migration |
||||
|
||||
def change do |
||||
execute( |
||||
""" |
||||
ALTER TABLE token_transfers |
||||
DROP CONSTRAINT token_transfers_pkey, |
||||
ADD PRIMARY KEY (block_hash, log_index); |
||||
""", |
||||
""" |
||||
ALTER TABLE token_transfers |
||||
DROP CONSTRAINT token_transfers_pkey, |
||||
ADD PRIMARY KEY (transaction_hash, block_hash, log_index); |
||||
""" |
||||
) |
||||
|
||||
execute( |
||||
"ALTER TABLE token_transfers ALTER COLUMN transaction_hash DROP NOT NULL", |
||||
"ALTER TABLE token_transfers ALTER COLUMN transaction_hash SET NOT NULL" |
||||
) |
||||
end |
||||
end |
@ -0,0 +1,62 @@ |
||||
defmodule Explorer.Repo.Celo.Migrations.AddEpochRewards do |
||||
use Ecto.Migration |
||||
|
||||
def change do |
||||
create table(:celo_epoch_rewards, primary_key: false) do |
||||
add(:reserve_bolster_transfer_log_index, :integer) |
||||
add(:community_transfer_log_index, :integer) |
||||
add(:carbon_offsetting_transfer_log_index, :integer) |
||||
|
||||
add( |
||||
:block_hash, |
||||
references(:blocks, column: :hash, type: :bytea, on_delete: :delete_all), |
||||
null: false, |
||||
primary_key: true |
||||
) |
||||
|
||||
timestamps() |
||||
end |
||||
|
||||
execute( |
||||
""" |
||||
ALTER TABLE celo_epoch_rewards |
||||
ADD CONSTRAINT celo_epoch_rewards_reserve_bolster_transfer_log_index_fkey |
||||
FOREIGN KEY (reserve_bolster_transfer_log_index, block_hash) |
||||
REFERENCES token_transfers (log_index, block_hash) |
||||
ON DELETE CASCADE |
||||
""", |
||||
""" |
||||
ALTER TABLE celo_epoch_rewards |
||||
DROP CONSTRAINT celo_epoch_rewards_reserve_bolster_transfer_log_index_fkey |
||||
""" |
||||
) |
||||
|
||||
execute( |
||||
""" |
||||
ALTER TABLE celo_epoch_rewards |
||||
ADD CONSTRAINT celo_epoch_rewards_community_transfer_log_index_fkey |
||||
FOREIGN KEY (community_transfer_log_index, block_hash) |
||||
REFERENCES token_transfers (log_index, block_hash) |
||||
ON DELETE CASCADE |
||||
""", |
||||
""" |
||||
ALTER TABLE celo_epoch_rewards |
||||
DROP CONSTRAINT celo_epoch_rewards_community_transfer_log_index_fkey |
||||
""" |
||||
) |
||||
|
||||
execute( |
||||
""" |
||||
ALTER TABLE celo_epoch_rewards |
||||
ADD CONSTRAINT celo_epoch_rewards_carbon_offsetting_transfer_log_index_fkey |
||||
FOREIGN KEY (carbon_offsetting_transfer_log_index, block_hash) |
||||
REFERENCES token_transfers (log_index, block_hash) |
||||
ON DELETE CASCADE |
||||
""", |
||||
""" |
||||
ALTER TABLE celo_epoch_rewards |
||||
DROP CONSTRAINT celo_epoch_rewards_carbon_offsetting_transfer_log_index_fkey |
||||
""" |
||||
) |
||||
end |
||||
end |
@ -0,0 +1,36 @@ |
||||
defmodule Explorer.Repo.Celo.Migrations.AddValidatorGroupVotes do |
||||
use Ecto.Migration |
||||
|
||||
def change do |
||||
execute( |
||||
"CREATE TYPE celo_validator_group_vote_type AS ENUM ('activated', 'revoked')", |
||||
"DROP TYPE celo_validator_group_vote_type" |
||||
) |
||||
|
||||
create table(:celo_validator_group_votes, primary_key: false) do |
||||
add( |
||||
:account_address_hash, |
||||
references(:addresses, column: :hash, on_delete: :delete_all, type: :bytea), |
||||
null: false |
||||
) |
||||
|
||||
add( |
||||
:group_address_hash, |
||||
references(:addresses, column: :hash, on_delete: :delete_all, type: :bytea), |
||||
null: false |
||||
) |
||||
|
||||
add(:value, :numeric, precision: 100, null: false) |
||||
add(:units, :numeric, precision: 100, null: false) |
||||
|
||||
add(:type, :celo_validator_group_vote_type, null: false) |
||||
|
||||
add(:transaction_hash, :bytea, null: false, primary_key: true) |
||||
|
||||
add(:block_number, :integer, null: false) |
||||
add(:block_hash, :bytea, null: false) |
||||
|
||||
timestamps() |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,38 @@ |
||||
defmodule Explorer.Repo.Celo.Migrations.AddElectionRewards do |
||||
use Ecto.Migration |
||||
|
||||
def change do |
||||
execute( |
||||
"CREATE TYPE celo_election_reward_type AS ENUM ('voter', 'validator', 'group', 'delegated_payment')", |
||||
"DROP TYPE celo_election_reward_type" |
||||
) |
||||
|
||||
create table(:celo_election_rewards, primary_key: false) do |
||||
add(:amount, :numeric, precision: 100, null: false) |
||||
add(:type, :celo_election_reward_type, null: false, primary_key: true) |
||||
|
||||
add( |
||||
:block_hash, |
||||
references(:blocks, column: :hash, type: :bytea, on_delete: :delete_all), |
||||
null: false, |
||||
primary_key: true |
||||
) |
||||
|
||||
add( |
||||
:account_address_hash, |
||||
references(:addresses, column: :hash, on_delete: :delete_all, type: :bytea), |
||||
null: false, |
||||
primary_key: true |
||||
) |
||||
|
||||
add( |
||||
:associated_account_address_hash, |
||||
references(:addresses, column: :hash, on_delete: :delete_all, type: :bytea), |
||||
null: false, |
||||
primary_key: true |
||||
) |
||||
|
||||
timestamps() |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,10 @@ |
||||
defmodule Explorer.Repo.Celo.Migrations.RemoveUnusedFieldsFromValidatorGroupVotes do |
||||
use Ecto.Migration |
||||
|
||||
def change do |
||||
alter table(:celo_validator_group_votes) do |
||||
remove(:value, :numeric, precision: 100, null: false, default: 0) |
||||
remove(:units, :numeric, precision: 100, null: false, default: 0) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,84 @@ |
||||
defmodule Explorer.Chain.Cache.CeloCoreContractsTest do |
||||
use ExUnit.Case, async: true |
||||
|
||||
alias Explorer.Chain.Cache.CeloCoreContracts |
||||
|
||||
describe "get_address/2" do |
||||
test "returns address according to block number" do |
||||
first_address = "0xb10ee11244526b94879e1956745ba2e35ae2ba20" |
||||
|
||||
Application.put_env(:explorer, Explorer.Chain.Cache.CeloCoreContracts, |
||||
contracts: %{ |
||||
"addresses" => %{ |
||||
"EpochRewards" => [ |
||||
%{ |
||||
"address" => first_address, |
||||
"updated_at_block_number" => 100 |
||||
} |
||||
] |
||||
} |
||||
} |
||||
) |
||||
|
||||
on_exit(fn -> |
||||
Application.put_env(:explorer, Explorer.Chain.Cache.CeloCoreContracts, contracts: %{}) |
||||
end) |
||||
|
||||
assert {:error, :address_does_not_exist} = CeloCoreContracts.get_address(:epoch_rewards, 99) |
||||
assert {:ok, ^first_address} = CeloCoreContracts.get_address(:epoch_rewards, 100) |
||||
assert {:ok, ^first_address} = CeloCoreContracts.get_address(:epoch_rewards, 10_000) |
||||
end |
||||
end |
||||
|
||||
describe "get_event/3" do |
||||
test "returns event according to block number" do |
||||
first_address = "0x0000000000000000000000000000000000000000" |
||||
second_address = "0x22579ca45ee22e2e16ddf72d955d6cf4c767b0ef" |
||||
|
||||
Application.put_env(:explorer, Explorer.Chain.Cache.CeloCoreContracts, |
||||
contracts: %{ |
||||
"addresses" => %{ |
||||
"EpochRewards" => [ |
||||
%{ |
||||
"address" => "0xb10ee11244526b94879e1956745ba2e35ae2ba20", |
||||
"updated_at_block_number" => 100 |
||||
} |
||||
] |
||||
}, |
||||
"events" => %{ |
||||
"EpochRewards" => %{ |
||||
"0xb10ee11244526b94879e1956745ba2e35ae2ba20" => %{ |
||||
"CarbonOffsettingFundSet" => [ |
||||
%{ |
||||
"address" => first_address, |
||||
"updated_at_block_number" => 598 |
||||
}, |
||||
%{ |
||||
"address" => second_address, |
||||
"updated_at_block_number" => 15_049_265 |
||||
} |
||||
] |
||||
} |
||||
} |
||||
} |
||||
} |
||||
) |
||||
|
||||
on_exit(fn -> |
||||
Application.put_env(:explorer, Explorer.Chain.Cache.CeloCoreContracts, contracts: %{}) |
||||
end) |
||||
|
||||
assert {:error, :address_does_not_exist} = |
||||
CeloCoreContracts.get_event(:epoch_rewards, :carbon_offsetting_fund_set, 99) |
||||
|
||||
assert {:ok, %{"address" => ^first_address, "updated_at_block_number" => 598}} = |
||||
CeloCoreContracts.get_event(:epoch_rewards, :carbon_offsetting_fund_set, 598) |
||||
|
||||
assert {:ok, %{"address" => ^first_address, "updated_at_block_number" => 598}} = |
||||
CeloCoreContracts.get_event(:epoch_rewards, :carbon_offsetting_fund_set, 599) |
||||
|
||||
assert {:ok, %{"address" => ^second_address, "updated_at_block_number" => 15_049_265}} = |
||||
CeloCoreContracts.get_event(:epoch_rewards, :carbon_offsetting_fund_set, 16_000_000) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,146 @@ |
||||
defmodule Indexer.Fetcher.Celo.EpochBlockOperations do |
||||
@moduledoc """ |
||||
Tracks epoch blocks awaiting processing by the epoch fetcher. |
||||
""" |
||||
|
||||
import Explorer.Chain.Celo.Helper, only: [epoch_block_number?: 1] |
||||
|
||||
alias Explorer.Chain |
||||
alias Explorer.Chain.Block |
||||
alias Explorer.Chain.Celo.PendingEpochBlockOperation |
||||
alias Indexer.Fetcher.Celo.EpochBlockOperations.Supervisor, as: EpochBlockOperationsSupervisor |
||||
alias Indexer.Transform.Addresses |
||||
alias Indexer.{BufferedTask, Tracer} |
||||
|
||||
alias Indexer.Fetcher.Celo.EpochBlockOperations.{ |
||||
DelegatedPayments, |
||||
Distributions, |
||||
ValidatorAndGroupPayments, |
||||
VoterPayments |
||||
} |
||||
|
||||
require Logger |
||||
|
||||
use Indexer.Fetcher, restart: :permanent |
||||
use Spandex.Decorators |
||||
|
||||
@behaviour BufferedTask |
||||
|
||||
@default_max_batch_size 1 |
||||
@default_max_concurrency 1 |
||||
|
||||
@doc false |
||||
def child_spec([init_options, gen_server_options]) do |
||||
{state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments) |
||||
|
||||
unless state do |
||||
raise ArgumentError, |
||||
":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec " <> |
||||
"to allow for json_rpc calls when running." |
||||
end |
||||
|
||||
merged_init_opts = |
||||
defaults() |
||||
|> Keyword.merge(mergeable_init_options) |
||||
|> Keyword.put(:state, state) |
||||
|
||||
Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_opts}, gen_server_options]}, id: __MODULE__) |
||||
end |
||||
|
||||
def defaults do |
||||
[ |
||||
poll: false, |
||||
flush_interval: :timer.seconds(3), |
||||
max_concurrency: Application.get_env(:indexer, __MODULE__)[:concurrency] || @default_max_concurrency, |
||||
max_batch_size: Application.get_env(:indexer, __MODULE__)[:batch_size] || @default_max_batch_size, |
||||
task_supervisor: Indexer.Fetcher.Celo.EpochBlockOperations.TaskSupervisor, |
||||
metadata: [fetcher: :celo_epoch_rewards] |
||||
] |
||||
end |
||||
|
||||
@spec async_fetch( |
||||
[%{block_number: Block.block_number(), block_hash: Hash.Full}], |
||||
boolean(), |
||||
integer() |
||||
) :: :ok |
||||
def async_fetch(entries, realtime?, timeout \\ 5000) when is_list(entries) do |
||||
if EpochBlockOperationsSupervisor.disabled?() do |
||||
:ok |
||||
else |
||||
filtered_entries = Enum.filter(entries, &epoch_block_number?(&1.block_number)) |
||||
BufferedTask.buffer(__MODULE__, filtered_entries, realtime?, timeout) |
||||
end |
||||
end |
||||
|
||||
@impl BufferedTask |
||||
def init(initial, reducer, _json_rpc_named_arguments) do |
||||
{:ok, final} = |
||||
PendingEpochBlockOperation.stream_epoch_blocks_with_unfetched_rewards( |
||||
initial, |
||||
reducer, |
||||
true |
||||
) |
||||
|
||||
final |
||||
end |
||||
|
||||
@impl BufferedTask |
||||
@decorate trace( |
||||
name: "fetch", |
||||
resource: "Indexer.Fetcher.Celo.EpochBlockOperations.run/2", |
||||
service: :indexer, |
||||
tracer: Tracer |
||||
) |
||||
def run(pending_operations, json_rpc_named_arguments) do |
||||
Enum.each(pending_operations, fn pending_operation -> |
||||
fetch(pending_operation, json_rpc_named_arguments) |
||||
end) |
||||
|
||||
:ok |
||||
end |
||||
|
||||
defp fetch(pending_operation, json_rpc_named_arguments) do |
||||
{:ok, distributions} = Distributions.fetch(pending_operation) |
||||
{:ok, validator_and_group_payments} = ValidatorAndGroupPayments.fetch(pending_operation) |
||||
|
||||
{:ok, voter_payments} = |
||||
VoterPayments.fetch( |
||||
pending_operation, |
||||
json_rpc_named_arguments |
||||
) |
||||
|
||||
{:ok, delegated_payments} = |
||||
validator_and_group_payments |
||||
|> Enum.filter(&(&1.type == :validator)) |
||||
|> Enum.map(& &1.account_address_hash) |
||||
|> DelegatedPayments.fetch( |
||||
pending_operation, |
||||
json_rpc_named_arguments |
||||
) |
||||
|
||||
election_rewards = |
||||
[ |
||||
validator_and_group_payments, |
||||
voter_payments, |
||||
delegated_payments |
||||
] |
||||
|> Enum.concat() |
||||
|> Enum.filter(&(&1.amount > 0)) |
||||
|
||||
addresses_params = |
||||
Addresses.extract_addresses(%{ |
||||
celo_election_rewards: election_rewards |
||||
}) |
||||
|
||||
{:ok, imported} = |
||||
Chain.import(%{ |
||||
addresses: %{params: addresses_params}, |
||||
celo_election_rewards: %{params: election_rewards}, |
||||
celo_epoch_rewards: %{params: [distributions]} |
||||
}) |
||||
|
||||
Logger.info("Fetched epoch rewards for block number: #{pending_operation.block_number}") |
||||
|
||||
{:ok, imported} |
||||
end |
||||
end |
@ -0,0 +1,92 @@ |
||||
defmodule Indexer.Fetcher.Celo.EpochBlockOperations.CoreContractVersion do |
||||
@moduledoc """ |
||||
Fetches the version of the celo core contract. |
||||
""" |
||||
import Indexer.Fetcher.Celo.Helper, only: [abi_to_method_id: 1] |
||||
import Indexer.Helper, only: [read_contracts_with_retries: 5] |
||||
|
||||
@repeated_request_max_retries 3 |
||||
|
||||
@get_version_number_abi [ |
||||
%{ |
||||
"name" => "getVersionNumber", |
||||
"type" => "function", |
||||
"payable" => false, |
||||
"constant" => true, |
||||
"stateMutability" => "pure", |
||||
"inputs" => [], |
||||
"outputs" => [ |
||||
%{"type" => "uint256"}, |
||||
%{"type" => "uint256"}, |
||||
%{"type" => "uint256"}, |
||||
%{"type" => "uint256"} |
||||
] |
||||
} |
||||
] |
||||
|
||||
@get_version_number_method_id @get_version_number_abi |> abi_to_method_id() |
||||
|
||||
@doc """ |
||||
Fetches the version number of a Celo core contract at a given block. |
||||
|
||||
## Parameters |
||||
- `contract_address` (`EthereumJSONRPC.address()`): The address of the |
||||
contract. |
||||
- `block_number` (`EthereumJSONRPC.block_number()`): The block number at |
||||
which to fetch the version. |
||||
- `json_rpc_named_arguments` (`EthereumJSONRPC.json_rpc_named_arguments()`): |
||||
The JSON RPC named arguments. |
||||
|
||||
## Returns |
||||
- `{:ok, {integer(), integer(), integer(), integer()}}`: A tuple containing |
||||
the version number components if successful. |
||||
- `{:ok, {1, 1, 0, 0}}`: A default version number if the `getVersionNumber` |
||||
function does not exist for the core contract at the requested block. |
||||
- `{:error, [{any(), any()}]}`: An error tuple with the list of errors. |
||||
""" |
||||
@spec fetch( |
||||
EthereumJSONRPC.address(), |
||||
EthereumJSONRPC.block_number(), |
||||
EthereumJSONRPC.json_rpc_named_arguments() |
||||
) :: |
||||
{ |
||||
:error, |
||||
[{any(), any()}] |
||||
} |
||||
| { |
||||
:ok, |
||||
{integer(), integer(), integer(), integer()} |
||||
} |
||||
def fetch(contract_address, block_number, json_rpc_named_arguments) do |
||||
request = %{ |
||||
contract_address: contract_address, |
||||
method_id: @get_version_number_method_id, |
||||
args: [], |
||||
block_number: block_number |
||||
} |
||||
|
||||
[request] |
||||
|> read_contracts_with_retries( |
||||
@get_version_number_abi, |
||||
json_rpc_named_arguments, |
||||
@repeated_request_max_retries, |
||||
false |
||||
) |
||||
|> elem(0) |
||||
|> case do |
||||
[ok: [storage, major, minor, patch]] -> |
||||
{:ok, {storage, major, minor, patch}} |
||||
|
||||
# Celo Core Contracts deployed to a live network without the |
||||
# `getVersionNumber()` function, such as the original set of core |
||||
# contracts, are to be considered version 1.1.0.0. |
||||
# |
||||
# https://docs.celo.org/community/release-process/smart-contracts#core-contracts |
||||
[error: "(-32000) execution reverted"] -> |
||||
{:ok, {1, 1, 0, 0}} |
||||
|
||||
errors -> |
||||
{:error, errors} |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,166 @@ |
||||
defmodule Indexer.Fetcher.Celo.EpochBlockOperations.DelegatedPayments do |
||||
@moduledoc """ |
||||
Fetches delegated validator payments for the epoch block. |
||||
""" |
||||
import Ecto.Query, only: [from: 2] |
||||
import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] |
||||
import Indexer.Fetcher.Celo.Helper, only: [abi_to_method_id: 1] |
||||
import Indexer.Helper, only: [read_contracts_with_retries: 4] |
||||
|
||||
alias Explorer.Chain.Cache.CeloCoreContracts |
||||
alias Explorer.Chain.{Hash, TokenTransfer} |
||||
alias Explorer.Repo |
||||
alias Indexer.Fetcher.Celo.EpochBlockOperations.CoreContractVersion |
||||
|
||||
require Logger |
||||
|
||||
@mint_address_hash_string burn_address_hash_string() |
||||
|
||||
@repeated_request_max_retries 3 |
||||
|
||||
# The method `getPaymentDelegation` was introduced in the following. Thus, we |
||||
# set version hardcoded in `getVersionNumber` method. |
||||
# |
||||
# https://github.com/celo-org/celo-monorepo/blob/d7c8936dc529f46d56799365f8b3383a23cc220b/packages/protocol/contracts/common/Accounts.sol#L128-L130 |
||||
@get_payment_delegation_available_since_version {1, 1, 3, 0} |
||||
@get_payment_delegation_abi [ |
||||
%{ |
||||
"name" => "getPaymentDelegation", |
||||
"type" => "function", |
||||
"payable" => false, |
||||
"constant" => true, |
||||
"stateMutability" => "view", |
||||
"inputs" => [ |
||||
%{"name" => "account", "type" => "address"} |
||||
], |
||||
"outputs" => [ |
||||
%{"type" => "address"}, |
||||
%{"type" => "uint256"} |
||||
] |
||||
} |
||||
] |
||||
@get_payment_delegation_method_id @get_payment_delegation_abi |> abi_to_method_id() |
||||
|
||||
@spec fetch( |
||||
[EthereumJSONRPC.address()], |
||||
%{ |
||||
:block_hash => EthereumJSONRPC.hash(), |
||||
:block_number => EthereumJSONRPC.block_number() |
||||
}, |
||||
EthereumJSONRPC.json_rpc_named_arguments() |
||||
) :: |
||||
{:ok, list()} |
||||
| {:error, any()} |
||||
def fetch( |
||||
validator_addresses, |
||||
%{block_number: block_number, block_hash: block_hash} = _pending_operation, |
||||
json_rpc_named_arguments |
||||
) do |
||||
with {:ok, accounts_contract_address} <- |
||||
CeloCoreContracts.get_address(:accounts, block_number), |
||||
{:ok, accounts_contract_version} <- |
||||
CoreContractVersion.fetch( |
||||
accounts_contract_address, |
||||
block_number, |
||||
json_rpc_named_arguments |
||||
), |
||||
true <- accounts_contract_version >= @get_payment_delegation_available_since_version, |
||||
{:ok, usd_token_contract_address} <- |
||||
CeloCoreContracts.get_address(:usd_token, block_number), |
||||
{responses, []} <- |
||||
read_payment_delegations( |
||||
validator_addresses, |
||||
accounts_contract_address, |
||||
block_number, |
||||
json_rpc_named_arguments |
||||
) do |
||||
query = |
||||
from( |
||||
tt in TokenTransfer.only_consensus_transfers_query(), |
||||
where: |
||||
tt.block_hash == ^block_hash and |
||||
tt.token_contract_address_hash == ^usd_token_contract_address and |
||||
tt.from_address_hash == ^@mint_address_hash_string and |
||||
is_nil(tt.transaction_hash), |
||||
select: {tt.to_address_hash, tt.amount} |
||||
) |
||||
|
||||
beneficiary_address_to_amount = |
||||
query |
||||
|> Repo.all() |
||||
|> Map.new(fn {address, amount} -> |
||||
{Hash.to_string(address), amount} |
||||
end) |
||||
|
||||
rewards = |
||||
validator_addresses |
||||
|> Enum.zip(responses) |
||||
|> Enum.filter(&match?({_, {:ok, [_, fraction]}} when fraction > 0, &1)) |
||||
|> Enum.map(fn |
||||
{validator_address, {:ok, [beneficiary_address, _]}} -> |
||||
amount = Map.get(beneficiary_address_to_amount, beneficiary_address, 0) |
||||
|
||||
%{ |
||||
block_hash: block_hash, |
||||
account_address_hash: beneficiary_address, |
||||
amount: amount, |
||||
associated_account_address_hash: validator_address, |
||||
type: :delegated_payment |
||||
} |
||||
end) |
||||
|
||||
{:ok, rewards} |
||||
else |
||||
false -> |
||||
Logger.info(fn -> |
||||
[ |
||||
"Do not fetch payment delegations since `getPaymentDelegation` ", |
||||
"method is not available on block #{block_number}" |
||||
] |
||||
end) |
||||
|
||||
{:ok, []} |
||||
|
||||
{_, ["(-32000) execution reverted"]} -> |
||||
# todo: we should start fetching payment delegations only after the |
||||
# first `PaymentDelegationSet` event is emitted. Unfortunately, relying |
||||
# on contract version is not enough since the method could not be |
||||
# present. |
||||
Logger.info(fn -> |
||||
[ |
||||
"Could not fetch payment delegations since `getPaymentDelegation` constantly returns error. ", |
||||
"Most likely, the method is not available on block #{block_number}. " |
||||
] |
||||
end) |
||||
|
||||
{:ok, []} |
||||
|
||||
error -> |
||||
Logger.error("Could not fetch payment delegations: #{inspect(error)}") |
||||
|
||||
error |
||||
end |
||||
end |
||||
|
||||
defp read_payment_delegations( |
||||
validator_addresses, |
||||
accounts_contract_address, |
||||
block_number, |
||||
json_rpc_named_arguments |
||||
) do |
||||
validator_addresses |
||||
|> Enum.map( |
||||
&%{ |
||||
contract_address: accounts_contract_address, |
||||
method_id: @get_payment_delegation_method_id, |
||||
args: [&1], |
||||
block_number: block_number |
||||
} |
||||
) |
||||
|> read_contracts_with_retries( |
||||
@get_payment_delegation_abi, |
||||
json_rpc_named_arguments, |
||||
@repeated_request_max_retries |
||||
) |
||||
end |
||||
end |
@ -0,0 +1,101 @@ |
||||
defmodule Indexer.Fetcher.Celo.EpochBlockOperations.Distributions do |
||||
@moduledoc """ |
||||
Fetches Reserve bolster, Community, and Carbon offsetting distributions for |
||||
the epoch block. |
||||
""" |
||||
import Ecto.Query, only: [from: 2, subquery: 1] |
||||
import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] |
||||
|
||||
alias Explorer.Repo |
||||
|
||||
alias Explorer.Chain.{ |
||||
Cache.CeloCoreContracts, |
||||
Hash, |
||||
TokenTransfer |
||||
} |
||||
|
||||
@mint_address_hash_string burn_address_hash_string() |
||||
|
||||
@spec fetch(%{ |
||||
:block_hash => EthereumJSONRPC.hash(), |
||||
:block_number => EthereumJSONRPC.block_number() |
||||
}) :: |
||||
{:ok, map()} |
||||
| {:error, :multiple_transfers_to_same_address} |
||||
def fetch(%{block_number: block_number, block_hash: block_hash} = _pending_operation) do |
||||
{:ok, celo_token_contract_address_hash} = CeloCoreContracts.get_address(:celo_token, block_number) |
||||
{:ok, reserve_contract_address_hash} = CeloCoreContracts.get_address(:reserve, block_number) |
||||
{:ok, community_contract_address_hash} = CeloCoreContracts.get_address(:governance, block_number) |
||||
|
||||
{:ok, %{"address" => carbon_offsetting_contract_address_hash}} = |
||||
CeloCoreContracts.get_event(:epoch_rewards, :carbon_offsetting_fund_set, block_number) |
||||
|
||||
celo_mint_transfers_query = |
||||
from( |
||||
tt in TokenTransfer.only_consensus_transfers_query(), |
||||
where: |
||||
tt.block_hash == ^block_hash and |
||||
tt.token_contract_address_hash == ^celo_token_contract_address_hash and |
||||
tt.from_address_hash == ^@mint_address_hash_string and |
||||
is_nil(tt.transaction_hash) |
||||
) |
||||
|
||||
# Every epoch has at least one CELO transfer from the zero address to the |
||||
# reserve. This is how cUSD is minted before it is distributed to |
||||
# validators. If there is only one CELO transfer, then there was no |
||||
# Reserve bolster distribution for that epoch. If there are multiple CELO |
||||
# transfers, then the last one is the Reserve bolster distribution. |
||||
reserve_bolster_transfer_log_index_query = |
||||
from( |
||||
tt in subquery( |
||||
from( |
||||
tt in subquery(celo_mint_transfers_query), |
||||
where: tt.to_address_hash == ^reserve_contract_address_hash, |
||||
order_by: tt.log_index, |
||||
offset: 1 |
||||
) |
||||
), |
||||
select: max(tt.log_index) |
||||
) |
||||
|
||||
query = |
||||
from( |
||||
tt in subquery(celo_mint_transfers_query), |
||||
where: |
||||
tt.to_address_hash in ^[ |
||||
community_contract_address_hash, |
||||
carbon_offsetting_contract_address_hash |
||||
] or |
||||
tt.log_index == subquery(reserve_bolster_transfer_log_index_query), |
||||
select: {tt.to_address_hash, tt.log_index} |
||||
) |
||||
|
||||
transfers_with_log_index = query |> Repo.all() |
||||
|
||||
unique_addresses_count = |
||||
transfers_with_log_index |
||||
|> Enum.map(&elem(&1, 0)) |
||||
|> Enum.uniq() |
||||
|> Enum.count() |
||||
|
||||
address_to_key = %{ |
||||
reserve_contract_address_hash => :reserve_bolster_transfer_log_index, |
||||
community_contract_address_hash => :community_transfer_log_index, |
||||
carbon_offsetting_contract_address_hash => :carbon_offsetting_transfer_log_index |
||||
} |
||||
|
||||
if unique_addresses_count == Enum.count(transfers_with_log_index) do |
||||
distributions = |
||||
transfers_with_log_index |
||||
|> Enum.reduce(%{}, fn {address, log_index}, acc -> |
||||
key = Map.get(address_to_key, address |> Hash.to_string()) |
||||
Map.put(acc, key, log_index) |
||||
end) |
||||
|> Map.put(:block_hash, block_hash) |
||||
|
||||
{:ok, distributions} |
||||
else |
||||
{:error, :multiple_transfers_to_same_address} |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,67 @@ |
||||
defmodule Indexer.Fetcher.Celo.EpochBlockOperations.ValidatorAndGroupPayments do |
||||
@moduledoc """ |
||||
Fetches validator and group payments for the epoch block. |
||||
""" |
||||
import Ecto.Query, only: [from: 2] |
||||
|
||||
alias Explorer.Chain.Cache.CeloCoreContracts |
||||
alias Explorer.Chain.Log |
||||
alias Explorer.Repo |
||||
alias Indexer.Transform.Celo.ValidatorEpochPaymentDistributions |
||||
|
||||
@spec fetch(%{ |
||||
:block_hash => EthereumJSONRPC.hash(), |
||||
:block_number => EthereumJSONRPC.block_number() |
||||
}) :: {:ok, list()} |
||||
def fetch(%{block_number: block_number, block_hash: block_hash} = _pending_operation) do |
||||
epoch_payment_distributions_signature = ValidatorEpochPaymentDistributions.signature() |
||||
{:ok, validators_contract_address} = CeloCoreContracts.get_address(:validators, block_number) |
||||
|
||||
query = |
||||
from( |
||||
log in Log, |
||||
where: |
||||
log.block_hash == ^block_hash and |
||||
log.address_hash == ^validators_contract_address and |
||||
log.first_topic == ^epoch_payment_distributions_signature and |
||||
is_nil(log.transaction_hash), |
||||
select: log |
||||
) |
||||
|
||||
payments = |
||||
query |
||||
|> Repo.all() |
||||
|> ValidatorEpochPaymentDistributions.parse() |
||||
|> process_distribution_events(block_hash) |
||||
|
||||
{:ok, payments} |
||||
end |
||||
|
||||
defp process_distribution_events(distribution_events, block_hash) do |
||||
distribution_events |
||||
|> Enum.map(fn %{ |
||||
validator_address: validator_address, |
||||
validator_payment: validator_payment, |
||||
group_address: group_address, |
||||
group_payment: group_payment |
||||
} -> |
||||
[ |
||||
%{ |
||||
block_hash: block_hash, |
||||
account_address_hash: validator_address, |
||||
amount: validator_payment, |
||||
associated_account_address_hash: group_address, |
||||
type: :validator |
||||
}, |
||||
%{ |
||||
block_hash: block_hash, |
||||
account_address_hash: group_address, |
||||
amount: group_payment, |
||||
associated_account_address_hash: validator_address, |
||||
type: :group |
||||
} |
||||
] |
||||
end) |
||||
|> Enum.concat() |
||||
end |
||||
end |
@ -0,0 +1,201 @@ |
||||
defmodule Indexer.Fetcher.Celo.EpochBlockOperations.VoterPayments do |
||||
@moduledoc """ |
||||
Fetches voter payments for the epoch block. |
||||
""" |
||||
import Ecto.Query, only: [from: 2] |
||||
|
||||
import Explorer.Helper, only: [decode_data: 2] |
||||
import Indexer.Fetcher.Celo.Helper, only: [abi_to_method_id: 1] |
||||
import Indexer.Helper, only: [read_contracts_with_retries: 4] |
||||
|
||||
alias Explorer.Repo |
||||
alias Indexer.Fetcher.Celo.ValidatorGroupVotes |
||||
|
||||
alias Explorer.Chain.{ |
||||
Cache.CeloCoreContracts, |
||||
Celo.ValidatorGroupVote, |
||||
Hash, |
||||
Log |
||||
} |
||||
|
||||
require Logger |
||||
|
||||
@repeated_request_max_retries 3 |
||||
|
||||
@epoch_rewards_distributed_to_voters_topic "0x91ba34d62474c14d6c623cd322f4256666c7a45b7fdaa3378e009d39dfcec2a7" |
||||
|
||||
@get_active_votes_for_group_by_account_abi [ |
||||
%{ |
||||
"name" => "getActiveVotesForGroupByAccount", |
||||
"type" => "function", |
||||
"payable" => false, |
||||
"constant" => true, |
||||
"stateMutability" => "view", |
||||
"inputs" => [ |
||||
%{"name" => "group", "type" => "address"}, |
||||
%{"name" => "account", "type" => "address"} |
||||
], |
||||
"outputs" => [ |
||||
%{"type" => "uint256"} |
||||
] |
||||
} |
||||
] |
||||
|
||||
@get_active_votes_for_group_by_account_method_id @get_active_votes_for_group_by_account_abi |
||||
|> abi_to_method_id() |
||||
|
||||
@spec fetch( |
||||
%{ |
||||
:block_hash => EthereumJSONRPC.hash(), |
||||
:block_number => EthereumJSONRPC.block_number() |
||||
}, |
||||
EthereumJSONRPC.json_rpc_named_arguments() |
||||
) :: {:error, list()} | {:ok, list()} |
||||
def fetch( |
||||
%{block_number: block_number, block_hash: block_hash} = pending_operation, |
||||
json_rpc_named_arguments |
||||
) do |
||||
:ok = ValidatorGroupVotes.fetch(block_number) |
||||
|
||||
{:ok, election_contract_address} = CeloCoreContracts.get_address(:election, block_number) |
||||
|
||||
elected_groups_query = |
||||
from( |
||||
l in Log, |
||||
where: |
||||
l.block_hash == ^block_hash and |
||||
l.address_hash == ^election_contract_address and |
||||
l.first_topic == ^@epoch_rewards_distributed_to_voters_topic and |
||||
is_nil(l.transaction_hash), |
||||
select: fragment("SUBSTRING(? from 13)", l.second_topic) |
||||
) |
||||
|
||||
query = |
||||
from( |
||||
v in ValidatorGroupVote, |
||||
where: |
||||
v.group_address_hash in subquery(elected_groups_query) and |
||||
v.block_number <= ^block_number, |
||||
distinct: true, |
||||
select: {v.account_address_hash, v.group_address_hash} |
||||
) |
||||
|
||||
accounts_with_activated_votes = |
||||
query |
||||
|> Repo.all() |
||||
|> Enum.map(fn |
||||
{account_address_hash, group_address_hash} -> |
||||
{ |
||||
Hash.to_string(account_address_hash), |
||||
Hash.to_string(group_address_hash) |
||||
} |
||||
end) |
||||
|
||||
requests = |
||||
accounts_with_activated_votes |
||||
|> Enum.map(fn {account_address_hash, group_address_hash} -> |
||||
(block_number - 1)..block_number |
||||
|> Enum.map(fn block_number -> |
||||
%{ |
||||
contract_address: election_contract_address, |
||||
method_id: @get_active_votes_for_group_by_account_method_id, |
||||
args: [ |
||||
group_address_hash, |
||||
account_address_hash |
||||
], |
||||
block_number: block_number |
||||
} |
||||
end) |
||||
end) |
||||
|> Enum.concat() |
||||
|
||||
{responses, []} = |
||||
read_contracts_with_retries( |
||||
requests, |
||||
@get_active_votes_for_group_by_account_abi, |
||||
json_rpc_named_arguments, |
||||
@repeated_request_max_retries |
||||
) |
||||
|
||||
diffs = |
||||
responses |
||||
|> Enum.chunk_every(2) |
||||
|> Enum.map(fn |
||||
[ok: [votes_before], ok: [votes_after]] |
||||
when is_integer(votes_before) and |
||||
is_integer(votes_after) -> |
||||
votes_after - votes_before |
||||
end) |
||||
|
||||
# WARN: we do not count Revoked/Activated votes for the last epoch, but |
||||
# should we? |
||||
# |
||||
# See https://github.com/fedor-ivn/celo-blockscout/tree/master/apps/indexer/lib/indexer/fetcher/celo_epoch_data.ex#L179-L187 |
||||
# There is no case when those events occur in the epoch block. |
||||
rewards = |
||||
accounts_with_activated_votes |
||||
|> Enum.zip_with( |
||||
diffs, |
||||
fn {account_address_hash, group_address_hash}, diff -> |
||||
%{ |
||||
block_hash: block_hash, |
||||
account_address_hash: account_address_hash, |
||||
amount: diff, |
||||
associated_account_address_hash: group_address_hash, |
||||
type: :voter |
||||
} |
||||
end |
||||
) |
||||
|
||||
ok_or_error = validate_voter_rewards(pending_operation, rewards) |
||||
|
||||
{ok_or_error, rewards} |
||||
end |
||||
|
||||
# Validates voter rewards by comparing the sum of what we got from the |
||||
# `EpochRewardsDistributedToVoters` event and the sum of what we calculated |
||||
# manually by fetching the votes for each account that has or had an activated |
||||
# vote. |
||||
defp validate_voter_rewards(pending_operation, voter_rewards) do |
||||
manual_voters_total = voter_rewards |> Enum.map(& &1.amount) |> Enum.sum() |
||||
{:ok, election_contract_address} = CeloCoreContracts.get_address(:election, pending_operation.block_number) |
||||
|
||||
query = |
||||
from( |
||||
l in Log, |
||||
where: |
||||
l.block_hash == ^pending_operation.block_hash and |
||||
l.address_hash == ^election_contract_address and |
||||
l.first_topic == ^@epoch_rewards_distributed_to_voters_topic and |
||||
is_nil(l.transaction_hash), |
||||
select: l.data |
||||
) |
||||
|
||||
voter_rewards_from_event_total = |
||||
query |
||||
|> Repo.all() |
||||
|> Enum.map(fn data -> |
||||
[amount] = decode_data(data, [{:uint, 256}]) |
||||
amount |
||||
end) |
||||
|> Enum.sum() |
||||
|
||||
voter_rewards_count = Enum.count(voter_rewards) |
||||
voter_rewards_diff = voter_rewards_from_event_total - manual_voters_total |
||||
|
||||
if voter_rewards_diff < voter_rewards_count or voter_rewards_count == 0 do |
||||
:ok |
||||
else |
||||
Logger.warning(fn -> |
||||
[ |
||||
"Total voter rewards do not match. ", |
||||
"Amount calculated manually: #{manual_voters_total}. ", |
||||
"Amount got from `EpochRewardsDistributedToVoters` events: #{voter_rewards_from_event_total}. ", |
||||
"Voter rewards count: #{voter_rewards_count}." |
||||
] |
||||
end) |
||||
|
||||
:error |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,121 @@ |
||||
defmodule Indexer.Fetcher.Celo.EpochLogs do |
||||
@moduledoc """ |
||||
Fetches logs that are not associated which are not linked to transaction, but |
||||
to the block. |
||||
""" |
||||
|
||||
import Explorer.Chain.Celo.Helper, only: [epoch_block_number?: 1] |
||||
|
||||
alias EthereumJSONRPC.Logs |
||||
alias Explorer.Chain.Cache.CeloCoreContracts |
||||
alias Indexer.Helper, as: IndexerHelper |
||||
|
||||
alias Explorer.Chain.TokenTransfer |
||||
alias Indexer.Transform.Celo.ValidatorEpochPaymentDistributions |
||||
|
||||
require Logger |
||||
|
||||
@max_request_retries 3 |
||||
|
||||
@epoch_block_targets [ |
||||
# TargetVotingYieldUpdated |
||||
epoch_rewards: "0x49d8cdfe05bae61517c234f65f4088454013bafe561115126a8fe0074dc7700e", |
||||
celo_token: TokenTransfer.constant(), |
||||
usd_token: TokenTransfer.constant(), |
||||
validators: ValidatorEpochPaymentDistributions.signature(), |
||||
# ValidatorScoreUpdated |
||||
validators: "0xedf9f87e50e10c533bf3ae7f5a7894ae66c23e6cbbe8773d7765d20ad6f995e9", |
||||
# EpochRewardsDistributedToVoters |
||||
election: "0x91ba34d62474c14d6c623cd322f4256666c7a45b7fdaa3378e009d39dfcec2a7" |
||||
] |
||||
|
||||
@default_block_targets [ |
||||
# GasPriceMinimumUpdated |
||||
gas_price_minimum: "0x6e53b2f8b69496c2a175588ad1326dbabe2f66df4d82f817aeca52e3474807fb" |
||||
] |
||||
|
||||
@spec fetch( |
||||
[Indexer.Transform.Blocks.block()], |
||||
EthereumJSONRPC.json_rpc_named_arguments() |
||||
) :: Logs.t() |
||||
def fetch(blocks, json_rpc_named_arguments) |
||||
|
||||
def fetch(blocks, json_rpc_named_arguments) do |
||||
if Application.get_env(:explorer, :chain_type) == :celo do |
||||
do_fetch(blocks, json_rpc_named_arguments) |
||||
else |
||||
[] |
||||
end |
||||
end |
||||
|
||||
defp do_fetch(blocks, json_rpc_named_arguments) do |
||||
requests = |
||||
blocks |
||||
|> Enum.reduce({[], 0}, &blocks_reducer/2) |
||||
|> elem(0) |
||||
|> Enum.reverse() |
||||
|> Enum.concat() |
||||
|
||||
with {:ok, responses} <- do_requests(requests, json_rpc_named_arguments), |
||||
{:ok, logs} <- Logs.from_responses(responses) do |
||||
logs |
||||
|> Enum.filter(&(&1.transaction_hash == &1.block_hash)) |
||||
|> Enum.map(&Map.put(&1, :transaction_hash, nil)) |
||||
end |
||||
end |
||||
|
||||
# Workaround in order to fix block fetcher tests. |
||||
# |
||||
# If the requests is empty, we still send the requests to the JSON RPC |
||||
defp do_requests(requests, json_rpc_named_arguments) do |
||||
if Enum.empty?(requests) do |
||||
{:ok, []} |
||||
else |
||||
IndexerHelper.repeated_batch_rpc_call( |
||||
requests, |
||||
json_rpc_named_arguments, |
||||
fn message -> "Could not fetch epoch logs: #{message}" end, |
||||
@max_request_retries |
||||
) |
||||
end |
||||
end |
||||
|
||||
defp blocks_reducer(%{number: number}, {acc, start_request_id}) do |
||||
targets = |
||||
@default_block_targets ++ |
||||
if epoch_block_number?(number) do |
||||
@epoch_block_targets |
||||
else |
||||
[] |
||||
end |
||||
|
||||
requests = |
||||
targets |
||||
|> Enum.map(fn {contract_atom, topic} -> |
||||
res = CeloCoreContracts.get_address(contract_atom, number) |
||||
{res, topic} |
||||
end) |
||||
|> Enum.split_with(&match?({{:ok, _address}, _topic}, &1)) |
||||
|> tap(fn {_, not_found} -> |
||||
if not Enum.empty?(not_found) do |
||||
Logger.info("Could not fetch addresses for the following contract atoms: #{inspect(not_found)}") |
||||
end |
||||
end) |
||||
|> elem(0) |
||||
|> Enum.with_index(start_request_id) |
||||
|> Enum.map(fn {{{:ok, address}, topic}, request_id} -> |
||||
Logs.request( |
||||
request_id, |
||||
%{ |
||||
from_block: number, |
||||
to_block: number, |
||||
address: address, |
||||
topics: [topic] |
||||
} |
||||
) |
||||
end) |
||||
|
||||
next_start_request_id = start_request_id + length(requests) |
||||
{[requests | acc], next_start_request_id} |
||||
end |
||||
end |
@ -0,0 +1,29 @@ |
||||
defmodule Indexer.Fetcher.Celo.Helper do |
||||
@moduledoc """ |
||||
Helper functions for the Celo fetchers. |
||||
""" |
||||
|
||||
@doc """ |
||||
Extracts the method ID from an ABI specification. |
||||
|
||||
## Parameters |
||||
- `method` ([map()] | map()): The ABI specification, either as a single map |
||||
or a list containing one map. |
||||
|
||||
## Returns |
||||
- `binary()`: The method ID extracted from the ABI specification. |
||||
|
||||
## Examples |
||||
|
||||
iex> Indexer.Fetcher.Celo.Helper.abi_to_method_id([%{"name" => "transfer", "type" => "function", "inputs" => [%{"name" => "to", "type" => "address"}]}]) |
||||
<<26, 105, 82, 48>> |
||||
|
||||
""" |
||||
@spec abi_to_method_id([map()] | map()) :: binary() |
||||
def abi_to_method_id([method]), do: abi_to_method_id(method) |
||||
|
||||
def abi_to_method_id(method) when is_map(method) do |
||||
[parsed_method] = ABI.parse_specification([method]) |
||||
parsed_method.method_id |
||||
end |
||||
end |
@ -0,0 +1,217 @@ |
||||
defmodule Indexer.Fetcher.Celo.ValidatorGroupVotes do |
||||
@moduledoc """ |
||||
Fetches validator group votes from the Celo blockchain. |
||||
""" |
||||
|
||||
use GenServer |
||||
use Indexer.Fetcher |
||||
|
||||
import Explorer.Helper, |
||||
only: [ |
||||
truncate_address_hash: 1, |
||||
safe_parse_non_negative_integer: 1 |
||||
] |
||||
|
||||
alias EthereumJSONRPC.Logs |
||||
alias Explorer.Application.Constants |
||||
alias Explorer.Chain |
||||
alias Explorer.Chain.Cache.CeloCoreContracts |
||||
alias Indexer.Helper, as: IndexerHelper |
||||
alias Indexer.Transform.Addresses |
||||
|
||||
require Logger |
||||
|
||||
@last_fetched_block_key "celo_validator_group_votes_last_fetched_block_number" |
||||
|
||||
@max_request_retries 3 |
||||
|
||||
@validator_group_vote_activated_topic "0x45aac85f38083b18efe2d441a65b9c1ae177c78307cb5a5d4aec8f7dbcaeabfe" |
||||
@validator_group_active_vote_revoked_topic "0xae7458f8697a680da6be36406ea0b8f40164915ac9cc40c0dad05a2ff6e8c6a8" |
||||
|
||||
@spec fetch(block_number :: EthereumJSONRPC.block_number()) :: :ok |
||||
def fetch(block_number) do |
||||
GenServer.call(__MODULE__, {:fetch, block_number}) |
||||
end |
||||
|
||||
def child_spec(start_link_arguments) do |
||||
spec = %{ |
||||
id: __MODULE__, |
||||
start: {__MODULE__, :start_link, start_link_arguments}, |
||||
type: :worker |
||||
} |
||||
|
||||
Supervisor.child_spec(spec, []) |
||||
end |
||||
|
||||
def start_link(args, gen_server_options \\ []) do |
||||
GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) |
||||
end |
||||
|
||||
@impl GenServer |
||||
def init(args) do |
||||
Logger.metadata(fetcher: :celo_validator_group_votes) |
||||
|
||||
{ |
||||
:ok, |
||||
%{ |
||||
config: %{ |
||||
batch_size: Application.get_env(:indexer, __MODULE__)[:batch_size], |
||||
json_rpc_named_arguments: args[:json_rpc_named_arguments] |
||||
}, |
||||
data: %{} |
||||
}, |
||||
{:continue, :ok} |
||||
} |
||||
end |
||||
|
||||
@impl GenServer |
||||
def handle_continue( |
||||
:ok, |
||||
%{ |
||||
config: %{ |
||||
batch_size: batch_size, |
||||
json_rpc_named_arguments: json_rpc_named_arguments |
||||
} |
||||
} = state |
||||
) do |
||||
{:ok, latest_block_number} = |
||||
EthereumJSONRPC.fetch_block_number_by_tag( |
||||
"latest", |
||||
json_rpc_named_arguments |
||||
) |
||||
|
||||
Logger.info("Fetching votes up to latest block number #{latest_block_number}") |
||||
|
||||
fetch_up_to_block_number(latest_block_number, batch_size, json_rpc_named_arguments) |
||||
|
||||
{:noreply, state} |
||||
end |
||||
|
||||
@impl GenServer |
||||
def handle_info({ref, _result}, state) do |
||||
Process.demonitor(ref, [:flush]) |
||||
{:noreply, state} |
||||
end |
||||
|
||||
@impl GenServer |
||||
def handle_call( |
||||
{:fetch, block_number}, |
||||
_from, |
||||
%{ |
||||
config: %{ |
||||
batch_size: batch_size, |
||||
json_rpc_named_arguments: json_rpc_named_arguments |
||||
} |
||||
} = state |
||||
) do |
||||
Logger.info("Fetching votes on demand up to block number #{block_number}") |
||||
|
||||
fetch_up_to_block_number(block_number, batch_size, json_rpc_named_arguments) |
||||
|
||||
{:reply, :ok, state} |
||||
end |
||||
|
||||
defp fetch_up_to_block_number(block_number, batch_size, json_rpc_named_arguments) do |
||||
{:ok, last_fetched_block_number} = |
||||
@last_fetched_block_key |
||||
|> Constants.get_constant_value() |
||||
|> case do |
||||
nil -> CeloCoreContracts.get_first_update_block_number(:election) |
||||
value -> safe_parse_non_negative_integer(value) |
||||
end |
||||
|
||||
if last_fetched_block_number < block_number do |
||||
block_range = last_fetched_block_number..block_number |
||||
|
||||
block_range |
||||
|> IndexerHelper.range_chunk_every(batch_size) |
||||
|> Enum.each(&process_chunk(&1, block_range, json_rpc_named_arguments)) |
||||
|
||||
Logger.info("Fetched validator group votes up to block number #{block_number}") |
||||
else |
||||
Logger.info("No new validator group votes to fetch") |
||||
end |
||||
end |
||||
|
||||
defp process_chunk(_..chunk_to_block = chunk, block_range, json_rpc_named_arguments) do |
||||
validator_group_votes = |
||||
chunk |
||||
|> fetch_logs_chunk(block_range, json_rpc_named_arguments) |
||||
|> Enum.map(&log_to_entry/1) |
||||
|
||||
addresses_params = |
||||
Addresses.extract_addresses(%{ |
||||
celo_validator_group_votes: validator_group_votes |
||||
}) |
||||
|
||||
{:ok, _imported} = |
||||
Chain.import(%{ |
||||
addresses: %{params: addresses_params}, |
||||
celo_validator_group_votes: %{params: validator_group_votes} |
||||
}) |
||||
|
||||
Constants.set_constant_value(@last_fetched_block_key, to_string(chunk_to_block)) |
||||
|
||||
:ok |
||||
end |
||||
|
||||
defp fetch_logs_chunk( |
||||
chunk_from_block..chunk_to_block, |
||||
from_block..to_block, |
||||
json_rpc_named_arguments |
||||
) do |
||||
IndexerHelper.log_blocks_chunk_handling(chunk_from_block, chunk_to_block, from_block, to_block, nil, :L1) |
||||
|
||||
{:ok, election_contract_address} = CeloCoreContracts.get_address(:election, chunk_from_block) |
||||
|
||||
requests = |
||||
[ |
||||
@validator_group_active_vote_revoked_topic, |
||||
@validator_group_vote_activated_topic |
||||
] |
||||
|> Enum.with_index() |
||||
|> Enum.map(fn {topic, request_id} -> |
||||
Logs.request( |
||||
request_id, |
||||
%{ |
||||
from_block: chunk_from_block, |
||||
to_block: chunk_to_block, |
||||
address: election_contract_address, |
||||
topics: [topic] |
||||
} |
||||
) |
||||
end) |
||||
|
||||
{:ok, responses} = |
||||
IndexerHelper.repeated_batch_rpc_call( |
||||
requests, |
||||
json_rpc_named_arguments, |
||||
fn message -> Logger.error("Could not fetch logs: #{message}") end, |
||||
@max_request_retries |
||||
) |
||||
|
||||
{:ok, logs} = Logs.from_responses(responses) |
||||
|
||||
logs |
||||
end |
||||
|
||||
defp log_to_entry(log) do |
||||
type = |
||||
case log.first_topic do |
||||
@validator_group_vote_activated_topic -> :activated |
||||
@validator_group_active_vote_revoked_topic -> :revoked |
||||
end |
||||
|
||||
account_address_hash = truncate_address_hash(log.second_topic) |
||||
group_address_hash = truncate_address_hash(log.third_topic) |
||||
|
||||
%{ |
||||
account_address_hash: account_address_hash, |
||||
group_address_hash: group_address_hash, |
||||
type: type, |
||||
block_number: log.block_number, |
||||
block_hash: log.block_hash, |
||||
transaction_hash: log.transaction_hash |
||||
} |
||||
end |
||||
end |
@ -0,0 +1,42 @@ |
||||
defmodule Indexer.Transform.Celo.TransactionGasTokens do |
||||
@moduledoc """ |
||||
Helper functions for extracting tokens specified as gas fee currency. |
||||
""" |
||||
|
||||
alias Explorer.Chain.Hash |
||||
|
||||
@doc """ |
||||
Parses transactions and extracts tokens specified as gas fee currency. |
||||
""" |
||||
@spec parse([ |
||||
%{ |
||||
optional(:gas_token_contract_address_hash) => Hash.Address.t() | nil |
||||
} |
||||
]) :: [ |
||||
%{ |
||||
contract_address_hash: String.t(), |
||||
type: String.t() |
||||
} |
||||
] |
||||
def parse(transactions) do |
||||
if Application.get_env(:explorer, :chain_type) == :celo do |
||||
transactions |
||||
|> Enum.reduce( |
||||
MapSet.new(), |
||||
fn |
||||
%{gas_token_contract_address_hash: address_hash}, acc when not is_nil(address_hash) -> |
||||
MapSet.put(acc, %{ |
||||
contract_address_hash: address_hash, |
||||
type: "ERC-20" |
||||
}) |
||||
|
||||
_, acc -> |
||||
acc |
||||
end |
||||
) |
||||
|> MapSet.to_list() |
||||
else |
||||
[] |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,138 @@ |
||||
defmodule Indexer.Transform.Celo.TransactionTokenTransfers do |
||||
@moduledoc """ |
||||
Helper functions for generating ERC20 token transfers from native Celo coin |
||||
transfers. |
||||
|
||||
CELO has a feature referred to as "token duality", where the native chain |
||||
asset (CELO) can be used as both a native chain currency and as an ERC-20 |
||||
token. Unfortunately native chain asset transfers do not emit ERC-20 transfer |
||||
events, which requires the artificial creation of entries in the |
||||
`token_transfers` table. |
||||
""" |
||||
require Logger |
||||
|
||||
import Indexer.Transform.TokenTransfers, |
||||
only: [ |
||||
filter_tokens_for_supply_update: 1 |
||||
] |
||||
|
||||
alias Explorer.Chain.Cache.CeloCoreContracts |
||||
alias Explorer.Chain.Hash |
||||
alias Indexer.Fetcher.TokenTotalSupplyUpdater |
||||
@token_type "ERC-20" |
||||
@transaction_buffer_size 20_000 |
||||
|
||||
@doc """ |
||||
In order to avoid conflicts with real token transfers, for native token |
||||
transfers we put a negative `log_index`. |
||||
|
||||
Each transaction within the block is assigned a so-called _buffer_ of |
||||
#{@transaction_buffer_size} entries. Thus, according to the formula, |
||||
transactions with indices 0, 1, 2 would have log indices -20000, -40000, |
||||
-60000. |
||||
|
||||
The spare intervals between the log indices (0..-19_999, -20_001..-39_999, |
||||
-40_001..59_999) are reserved for native token transfers fetched from |
||||
internal transactions. |
||||
""" |
||||
@spec parse_transactions([ |
||||
%{ |
||||
required(:value) => non_neg_integer(), |
||||
optional(:to_address_hash) => Hash.Address.t() | nil, |
||||
optional(:created_contract_address_hash) => Hash.Address.t() | nil |
||||
} |
||||
]) :: %{ |
||||
token_transfers: list(), |
||||
tokens: list() |
||||
} |
||||
def parse_transactions(transactions) do |
||||
token_transfers = |
||||
if Application.get_env(:explorer, :chain_type) == :celo do |
||||
transactions |
||||
|> Enum.filter(fn tx -> tx.value > 0 end) |
||||
|> Enum.map(fn tx -> |
||||
to_address_hash = Map.get(tx, :to_address_hash) || Map.get(tx, :created_contract_address_hash) |
||||
log_index = -1 * (tx.index + 1) * @transaction_buffer_size |
||||
{:ok, celo_token_address} = CeloCoreContracts.get_address(:celo_token, tx.block_number) |
||||
|
||||
%{ |
||||
amount: Decimal.new(tx.value), |
||||
block_hash: tx.block_hash, |
||||
block_number: tx.block_number, |
||||
from_address_hash: tx.from_address_hash, |
||||
log_index: log_index, |
||||
to_address_hash: to_address_hash, |
||||
token_contract_address_hash: celo_token_address, |
||||
token_ids: nil, |
||||
token_type: @token_type, |
||||
transaction_hash: tx.hash |
||||
} |
||||
end) |
||||
|> tap(&Logger.debug("Found #{length(&1)} Celo token transfers.")) |
||||
else |
||||
[] |
||||
end |
||||
|
||||
token_transfers |
||||
|> filter_tokens_for_supply_update() |
||||
|> TokenTotalSupplyUpdater.add_tokens() |
||||
|
||||
%{ |
||||
token_transfers: token_transfers, |
||||
tokens: to_tokens(token_transfers) |
||||
} |
||||
end |
||||
|
||||
def parse_internal_transactions(internal_transactions, block_number_to_block_hash) do |
||||
token_transfers = |
||||
internal_transactions |
||||
|> Enum.filter(fn itx -> |
||||
itx.value > 0 && |
||||
itx.index > 0 && |
||||
not Map.has_key?(itx, :error) && |
||||
(not Map.has_key?(itx, :call_type) || itx.call_type != "delegatecall") |
||||
end) |
||||
|> Enum.map(fn itx -> |
||||
to_address_hash = Map.get(itx, :to_address_hash) || Map.get(itx, :created_contract_address_hash) |
||||
log_index = -1 * (itx.transaction_index * @transaction_buffer_size + itx.index) |
||||
{:ok, celo_token_address} = CeloCoreContracts.get_address(:celo_token, itx.block_number) |
||||
|
||||
%{ |
||||
amount: Decimal.new(itx.value), |
||||
block_hash: block_number_to_block_hash[itx.block_number], |
||||
block_number: itx.block_number, |
||||
from_address_hash: itx.from_address_hash, |
||||
log_index: log_index, |
||||
to_address_hash: to_address_hash, |
||||
token_contract_address_hash: celo_token_address, |
||||
token_ids: nil, |
||||
token_type: @token_type, |
||||
transaction_hash: itx.transaction_hash |
||||
} |
||||
end) |
||||
|
||||
Logger.debug("Found #{length(token_transfers)} Celo token transfers from internal transactions.") |
||||
|
||||
token_transfers |
||||
|> filter_tokens_for_supply_update() |
||||
|> TokenTotalSupplyUpdater.add_tokens() |
||||
|
||||
%{ |
||||
token_transfers: token_transfers, |
||||
tokens: to_tokens(token_transfers) |
||||
} |
||||
end |
||||
|
||||
defp to_tokens([]), do: [] |
||||
|
||||
defp to_tokens(token_transfers) do |
||||
token_transfers |
||||
|> Enum.map( |
||||
&%{ |
||||
contract_address_hash: &1.token_contract_address_hash, |
||||
type: @token_type |
||||
} |
||||
) |
||||
|> Enum.uniq() |
||||
end |
||||
end |
@ -0,0 +1,72 @@ |
||||
defmodule Indexer.Transform.Celo.ValidatorEpochPaymentDistributions do |
||||
@moduledoc """ |
||||
Extracts data from `ValidatorEpochPaymentDistributed` event logs of the |
||||
`Validators` Celo core contract. |
||||
""" |
||||
alias ABI.FunctionSelector |
||||
|
||||
alias Explorer.Chain.Cache.CeloCoreContracts |
||||
alias Explorer.Chain.{Hash, Log} |
||||
|
||||
require Logger |
||||
|
||||
@event_signature "0x6f5937add2ec38a0fa4959bccd86e3fcc2aafb706cd3e6c0565f87a7b36b9975" |
||||
|
||||
@event_abi [ |
||||
%{ |
||||
"name" => "ValidatorEpochPaymentDistributed", |
||||
"type" => "event", |
||||
"anonymous" => false, |
||||
"inputs" => [ |
||||
%{ |
||||
"indexed" => true, |
||||
"name" => "validator", |
||||
"type" => "address" |
||||
}, |
||||
%{ |
||||
"indexed" => false, |
||||
"name" => "validatorPayment", |
||||
"type" => "uint256" |
||||
}, |
||||
%{ |
||||
"indexed" => true, |
||||
"name" => "group", |
||||
"type" => "address" |
||||
}, |
||||
%{ |
||||
"indexed" => false, |
||||
"name" => "groupPayment", |
||||
"type" => "uint256" |
||||
} |
||||
] |
||||
} |
||||
] |
||||
|
||||
def signature, do: @event_signature |
||||
|
||||
def parse(logs) do |
||||
logs |
||||
|> Enum.filter(fn log -> |
||||
{:ok, validators_contract_address} = CeloCoreContracts.get_address(:validators, log.block_number) |
||||
|
||||
Hash.to_string(log.address_hash) == validators_contract_address and |
||||
Hash.to_string(log.first_topic) == @event_signature |
||||
end) |
||||
|> Enum.map(fn log -> |
||||
{:ok, %FunctionSelector{}, |
||||
[ |
||||
{"validator", "address", true, validator_address}, |
||||
{"validatorPayment", "uint256", false, validator_payment}, |
||||
{"group", "address", true, group_address}, |
||||
{"groupPayment", "uint256", false, group_payment} |
||||
]} = Log.find_and_decode(@event_abi, log, log.block_hash) |
||||
|
||||
%{ |
||||
validator_address: "0x" <> Base.encode16(validator_address, case: :lower), |
||||
validator_payment: validator_payment, |
||||
group_address: "0x" <> Base.encode16(group_address, case: :lower), |
||||
group_payment: group_payment |
||||
} |
||||
end) |
||||
end |
||||
end |
@ -0,0 +1,46 @@ |
||||
defmodule Indexer.Fetcher.Celo.EpochBlockOperationsTest do |
||||
# MUST be `async: false` so that {:shared, pid} is set for connection to allow CoinBalanceFetcher's self-send to have |
||||
# connection allowed immediately. |
||||
# use EthereumJSONRPC.Case, async: false |
||||
|
||||
use EthereumJSONRPC.Case |
||||
use Explorer.DataCase |
||||
|
||||
import Mox |
||||
# import EthereumJSONRPC, only: [integer_to_quantity: 1] |
||||
|
||||
import Explorer.Chain.Celo.Helper, only: [blocks_per_epoch: 0] |
||||
|
||||
alias Indexer.Fetcher.Celo.EpochBlockOperations |
||||
|
||||
# @moduletag :capture_log |
||||
|
||||
# MUST use global mode because we aren't guaranteed to get `start_supervised`'s pid back fast enough to `allow` it to |
||||
# use expectations and stubs from test's pid. |
||||
setup :set_mox_global |
||||
|
||||
setup :verify_on_exit! |
||||
|
||||
if Application.compile_env(:explorer, :chain_type) == :celo do |
||||
describe "init/3" do |
||||
test "buffers blocks with pending epoch operation", %{ |
||||
json_rpc_named_arguments: json_rpc_named_arguments |
||||
} do |
||||
unfetched = insert(:block, number: 1 * blocks_per_epoch()) |
||||
insert(:celo_pending_epoch_block_operation, block: unfetched) |
||||
|
||||
assert [ |
||||
%{ |
||||
block_number: unfetched.number, |
||||
block_hash: unfetched.hash |
||||
} |
||||
] == |
||||
EpochBlockOperations.init( |
||||
[], |
||||
fn block_number, acc -> [block_number | acc] end, |
||||
json_rpc_named_arguments |
||||
) |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,17 @@ |
||||
defmodule Indexer.Fetcher.Celo.EpochBlockOperations.Supervisor.Case do |
||||
alias Indexer.Fetcher.Celo.EpochBlockOperations |
||||
|
||||
def start_supervised!(fetcher_arguments \\ []) when is_list(fetcher_arguments) do |
||||
merged_fetcher_arguments = |
||||
Keyword.merge( |
||||
fetcher_arguments, |
||||
flush_interval: 50, |
||||
max_batch_size: 1, |
||||
max_concurrency: 1 |
||||
) |
||||
|
||||
[merged_fetcher_arguments] |
||||
|> EpochBlockOperations.Supervisor.child_spec() |
||||
|> ExUnit.Callbacks.start_supervised!() |
||||
end |
||||
end |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue