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