feat: Backfiller for omitted WETH transfers (#10466)
* feat: Backfiller for omitted WETH transfers * todo: add token balance update * done RestoreOmittedWETHTransfers migrator * Remove dbg * remove dbg * Fix credo * Process review commentspull/10497/head
parent
f59191935f
commit
c6ff374d8c
@ -0,0 +1,269 @@ |
||||
defmodule Explorer.Migrator.RestoreOmittedWETHTransfers do |
||||
@moduledoc """ |
||||
Inserts missed WETH token transfers |
||||
""" |
||||
|
||||
use GenServer, restart: :transient |
||||
|
||||
alias Explorer.{Chain, Helper} |
||||
alias Explorer.Chain.{Log, TokenTransfer} |
||||
alias Explorer.Migrator.MigrationStatus |
||||
|
||||
import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] |
||||
|
||||
require Logger |
||||
|
||||
@enqueue_busy_waiting_timeout 500 |
||||
@migration_timeout 250 |
||||
@migration_name "restore_omitted_weth_transfers" |
||||
|
||||
def start_link(_) do |
||||
GenServer.start_link(__MODULE__, :ok, name: __MODULE__) |
||||
end |
||||
|
||||
@impl true |
||||
def init(_) do |
||||
{:ok, %{}, {:continue, :check_env}} |
||||
end |
||||
|
||||
@impl true |
||||
def handle_continue(:check_env, state) do |
||||
list = Application.get_env(:explorer, Explorer.Chain.TokenTransfer)[:whitelisted_weth_contracts] |
||||
|
||||
cond do |
||||
Enum.empty?(list) -> |
||||
{:stop, :normal, state} |
||||
|
||||
check_token_types(list) -> |
||||
{:noreply, %{}, {:continue, :check_migration_status}} |
||||
|
||||
true -> |
||||
Logger.error("Stopping") |
||||
{:stop, :normal, state} |
||||
end |
||||
end |
||||
|
||||
@impl true |
||||
def handle_continue(:check_migration_status, state) do |
||||
case MigrationStatus.get_status(@migration_name) do |
||||
"completed" -> |
||||
{:stop, :normal, state} |
||||
|
||||
_ -> |
||||
MigrationStatus.set_status(@migration_name, "started") |
||||
{:noreply, %{}, {:continue, :ok}} |
||||
end |
||||
end |
||||
|
||||
@impl true |
||||
def handle_continue(:ok, _state) do |
||||
%{ref: ref} = |
||||
Task.async(fn -> |
||||
Log.stream_unfetched_weth_token_transfers(&enqueue_if_queue_is_not_full/1) |
||||
end) |
||||
|
||||
to_insert = |
||||
Application.get_env(:explorer, Explorer.Chain.TokenTransfer)[:whitelisted_weth_contracts] |
||||
|> Enum.map(fn contract_address_hash_string -> |
||||
if !Chain.token_from_address_hash_exists?(contract_address_hash_string, []) do |
||||
%{ |
||||
contract_address_hash: contract_address_hash_string, |
||||
type: "ERC-20" |
||||
} |
||||
end |
||||
end) |
||||
|> Enum.reject(&is_nil/1) |
||||
|
||||
if !Enum.empty?(to_insert) do |
||||
Chain.import(%{tokens: %{params: to_insert}}) |
||||
end |
||||
|
||||
Process.send_after(self(), :migrate, @migration_timeout) |
||||
|
||||
{:noreply, %{queue: [], current_concurrency: 0, stream_ref: ref, stream_is_over: false}} |
||||
end |
||||
|
||||
defp enqueue_if_queue_is_not_full(log) do |
||||
if GenServer.call(__MODULE__, :not_full?) do |
||||
GenServer.cast(__MODULE__, {:append_to_queue, log}) |
||||
else |
||||
:timer.sleep(@enqueue_busy_waiting_timeout) |
||||
|
||||
enqueue_if_queue_is_not_full(log) |
||||
end |
||||
end |
||||
|
||||
@impl true |
||||
def handle_call(:not_full?, _from, %{queue: queue} = state) do |
||||
{:reply, Enum.count(queue) < max_queue_size(), state} |
||||
end |
||||
|
||||
@impl true |
||||
def handle_cast({:append_to_queue, log}, %{queue: queue} = state) do |
||||
{:noreply, %{state | queue: [log | queue]}} |
||||
end |
||||
|
||||
@impl true |
||||
def handle_info(:migrate, %{queue: [], stream_is_over: true, current_concurrency: current_concurrency} = state) do |
||||
if current_concurrency > 0 do |
||||
{:noreply, state} |
||||
else |
||||
Logger.info("RestoreOmittedWETHTransfers migration is complete.") |
||||
|
||||
MigrationStatus.set_status(@migration_name, "completed") |
||||
{:stop, :normal, state} |
||||
end |
||||
end |
||||
|
||||
# fetch token balances |
||||
@impl true |
||||
def handle_info(:migrate, %{queue: queue, current_concurrency: current_concurrency} = state) do |
||||
if Enum.count(queue) > 0 and current_concurrency < concurrency() do |
||||
to_take = batch_size() * (concurrency() - current_concurrency) |
||||
{to_process, remainder} = Enum.split(queue, to_take) |
||||
|
||||
spawned_tasks = |
||||
to_process |
||||
|> Enum.chunk_every(batch_size()) |
||||
|> Enum.map(fn batch -> |
||||
run_task(batch) |
||||
end) |
||||
|
||||
if Enum.empty?(remainder) do |
||||
Process.send_after(self(), :migrate, migration_timeout()) |
||||
else |
||||
Process.send(self(), :migrate, []) |
||||
end |
||||
|
||||
{:noreply, %{state | queue: remainder, current_concurrency: current_concurrency + Enum.count(spawned_tasks)}} |
||||
else |
||||
Process.send_after(self(), :migrate, migration_timeout()) |
||||
{:noreply, state} |
||||
end |
||||
end |
||||
|
||||
@impl true |
||||
def handle_info({ref, _answer}, %{stream_ref: ref} = state) do |
||||
{:noreply, %{state | stream_is_over: true}} |
||||
end |
||||
|
||||
@impl true |
||||
def handle_info({ref, _answer}, %{current_concurrency: counter} = state) do |
||||
Process.demonitor(ref, [:flush]) |
||||
Process.send(self(), :migrate, []) |
||||
{:noreply, %{state | current_concurrency: counter - 1}} |
||||
end |
||||
|
||||
@impl true |
||||
def handle_info({:DOWN, ref, :process, _pid, _reason}, %{stream_ref: ref} = state) do |
||||
{:noreply, %{state | stream_is_over: true}} |
||||
end |
||||
|
||||
@impl true |
||||
def handle_info({:DOWN, _ref, :process, _pid, _reason}, %{current_concurrency: counter} = state) do |
||||
Process.send(self(), :migrate, []) |
||||
{:noreply, %{state | current_concurrency: counter - 1}} |
||||
end |
||||
|
||||
defp migrate_batch(batch) do |
||||
{token_transfers, token_balances} = |
||||
batch |
||||
|> Enum.map(fn log -> |
||||
with %{second_topic: second_topic, third_topic: nil, fourth_topic: nil, data: data} |
||||
when not is_nil(second_topic) <- |
||||
log, |
||||
[amount] <- Helper.decode_data(data, [{:uint, 256}]) do |
||||
{from_address_hash, to_address_hash, balance_address_hash} = |
||||
if log.first_topic == TokenTransfer.weth_deposit_signature() do |
||||
to_address_hash = Helper.truncate_address_hash(to_string(second_topic)) |
||||
{burn_address_hash_string(), to_address_hash, to_address_hash} |
||||
else |
||||
from_address_hash = Helper.truncate_address_hash(to_string(second_topic)) |
||||
{from_address_hash, burn_address_hash_string(), from_address_hash} |
||||
end |
||||
|
||||
token_transfer = %{ |
||||
amount: Decimal.new(amount || 0), |
||||
block_number: log.block_number, |
||||
block_hash: log.block_hash, |
||||
log_index: log.index, |
||||
from_address_hash: from_address_hash, |
||||
to_address_hash: to_address_hash, |
||||
token_contract_address_hash: log.address_hash, |
||||
transaction_hash: log.transaction_hash, |
||||
token_ids: nil, |
||||
token_type: "ERC-20" |
||||
} |
||||
|
||||
token_balance = %{ |
||||
address_hash: balance_address_hash, |
||||
token_contract_address_hash: log.address_hash, |
||||
block_number: log.block_number, |
||||
token_id: nil, |
||||
token_type: "ERC-20" |
||||
} |
||||
|
||||
{token_transfer, token_balance} |
||||
else |
||||
_ -> |
||||
Logger.error( |
||||
"Failed to decode log: (tx_hash, block_hash, index) = #{to_string(log.transaction_hash)}, #{to_string(log.block_hash)}, #{to_string(log.index)}" |
||||
) |
||||
|
||||
nil |
||||
end |
||||
end) |
||||
|> Enum.reject(&is_nil/1) |
||||
|> Enum.unzip() |
||||
|
||||
current_token_balances = |
||||
token_balances |
||||
|> Enum.group_by(fn %{ |
||||
address_hash: address_hash, |
||||
token_contract_address_hash: token_contract_address_hash |
||||
} -> |
||||
{address_hash, token_contract_address_hash} |
||||
end) |
||||
|> Enum.map(fn {_, grouped_address_token_balances} -> |
||||
Enum.max_by(grouped_address_token_balances, fn %{block_number: block_number} -> block_number end) |
||||
end) |
||||
|> Enum.sort_by(&{&1.token_contract_address_hash, &1.address_hash}) |
||||
|
||||
if !Enum.empty?(token_transfers) do |
||||
Chain.import(%{ |
||||
token_transfers: %{params: token_transfers}, |
||||
address_token_balances: %{params: token_balances}, |
||||
address_current_token_balances: %{ |
||||
params: current_token_balances |
||||
} |
||||
}) |
||||
end |
||||
end |
||||
|
||||
defp run_task(batch) do |
||||
Task.Supervisor.async_nolink(Explorer.WETHMigratorSupervisor, fn -> |
||||
migrate_batch(batch) |
||||
end) |
||||
end |
||||
|
||||
defp check_token_types(token_address_hashes) do |
||||
token_address_hashes |
||||
|> Chain.get_token_types() |
||||
|> Enum.reduce(true, fn {token_hash, token_type}, acc -> |
||||
if token_type == "ERC-20" do |
||||
acc |
||||
else |
||||
Logger.error("Wrong token type of #{to_string(token_hash)}: #{token_type}") |
||||
false |
||||
end |
||||
end) |
||||
end |
||||
|
||||
def concurrency, do: Application.get_env(:explorer, __MODULE__)[:concurrency] |
||||
|
||||
def batch_size, do: Application.get_env(:explorer, __MODULE__)[:batch_size] |
||||
|
||||
def migration_timeout, do: Application.get_env(:explorer, __MODULE__)[:timeout] |
||||
|
||||
def max_queue_size, do: concurrency() * batch_size() * 2 |
||||
end |
Loading…
Reference in new issue