Add stability_fee for CHAIN_TYPE=stability (#8651)

* Add stability_fee for CHAIN_TYPE=stability

* Changelog

* Optimize token fee requests

* Remove unused function

* Process review comments

* Fixes after rebase
pull/8706/head
nikitosing 1 year ago committed by GitHub
parent 15efaac428
commit 533eb26329
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .dialyzer-ignore
  2. 1
      CHANGELOG.md
  3. 15
      apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex
  4. 42
      apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex
  5. 139
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs
  6. 11
      apps/explorer/lib/explorer/chain/log.ex
  7. 6
      apps/explorer/lib/explorer/chain/token.ex
  8. 103
      apps/explorer/lib/explorer/chain/transaction.ex

@ -25,4 +25,4 @@ lib/indexer/fetcher/zkevm/transaction_batch.ex:156
lib/indexer/fetcher/zkevm/transaction_batch.ex:252
lib/block_scout_web/views/api/v2/transaction_view.ex:431
lib/block_scout_web/views/api/v2/transaction_view.ex:472
lib/explorer/chain/transaction.ex:165
lib/explorer/chain/transaction.ex:166

@ -6,6 +6,7 @@
- [#8696](https://github.com/blockscout/blockscout/pull/8696) - Support tokenSymbol and tokenName in `/api/v2/import/token-info`
- [#8673](https://github.com/blockscout/blockscout/pull/8673) - Add a window for balances fetching from non-archive node
- [#8651](https://github.com/blockscout/blockscout/pull/8651) - Add `stability_fee` for CHAIN_TYPE=stability
- [#8556](https://github.com/blockscout/blockscout/pull/8556) - Suave functional
- [#8528](https://github.com/blockscout/blockscout/pull/8528) - Account: add pagination + envs for limits
- [#7584](https://github.com/blockscout/blockscout/pull/7584) - Add Polygon zkEVM batches fetcher

@ -6,6 +6,21 @@ defmodule BlockScoutWeb.API.V2.TokenView do
@api_true [api?: true]
def render("token.json", %{token: nil, contract_address_hash: contract_address_hash}) do
%{
"address" => Address.checksum(contract_address_hash),
"symbol" => nil,
"name" => nil,
"decimals" => nil,
"type" => nil,
"holders" => nil,
"exchange_rate" => nil,
"total_supply" => nil,
"icon_url" => nil,
"circulating_market_cap" => nil
}
end
def render("token.json", %{token: token}) do
%{
"address" => Address.checksum(token.contract_address_hash),

@ -16,6 +16,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
alias Timex.Duration
import BlockScoutWeb.Account.AuthController, only: [current_user: 1]
import Explorer.Chain.Transaction, only: [maybe_prepare_stability_fees: 1, bytes_to_address_hash: 1]
import Explorer.Helper, only: [decode_data: 2]
@api_true [api?: true]
@ -37,6 +38,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
%{
"items" =>
transactions
|> maybe_prepare_stability_fees()
|> Enum.zip(decoded_transactions)
|> Enum.map(fn {tx, decoded_input} ->
prepare_transaction(tx, conn, false, block_height, watchlist_names, decoded_input)
@ -54,6 +56,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
{decoded_transactions, _, _} = decode_transactions(transactions, true)
transactions
|> maybe_prepare_stability_fees()
|> Enum.zip(decoded_transactions)
|> Enum.map(fn {tx, decoded_input} ->
prepare_transaction(tx, conn, false, block_height, watchlist_names, decoded_input)
@ -67,6 +70,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
%{
"items" =>
transactions
|> maybe_prepare_stability_fees()
|> Enum.zip(decoded_transactions)
|> Enum.map(fn {tx, decoded_input} -> prepare_transaction(tx, conn, false, block_height, decoded_input) end),
"next_page_params" => next_page_params
@ -84,6 +88,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
{decoded_transactions, _, _} = decode_transactions(transactions, true)
transactions
|> maybe_prepare_stability_fees()
|> Enum.zip(decoded_transactions)
|> Enum.map(fn {tx, decoded_input} -> prepare_transaction(tx, conn, false, block_height, decoded_input) end)
end
@ -91,7 +96,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
def render("transaction.json", %{transaction: transaction, conn: conn}) do
block_height = Chain.block_height(@api_true)
{[decoded_input], _, _} = decode_transactions([transaction], false)
prepare_transaction(transaction, conn, true, block_height, decoded_input)
prepare_transaction(transaction |> maybe_prepare_stability_fees(), conn, true, block_height, decoded_input)
end
def render("raw_trace.json", %{internal_transactions: internal_transactions}) do
@ -416,7 +421,9 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
"has_error_in_internal_txs" => transaction.has_error_in_internal_txs
}
chain_type_fields(result, transaction, single_tx?, conn, watchlist_names)
result
|> chain_type_fields(transaction, single_tx?, conn, watchlist_names)
|> maybe_put_stability_fee(transaction)
end
defp chain_type_fields(result, transaction, single_tx?, conn, watchlist_names) do
@ -882,4 +889,35 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
_ -> {nil, nil}
end
end
defp maybe_put_stability_fee(body, transaction) do
with "stability" <- Application.get_env(:explorer, :chain_type),
[
{"token", "address", false, token_address_hash},
{"totalFee", "uint256", false, total_fee},
{"validator", "address", false, validator_address_hash},
{"validatorFee", "uint256", false, validator_fee},
{"dapp", "address", false, dapp_address_hash},
{"dappFee", "uint256", false, dapp_fee}
] <- transaction.transaction_fee_log do
stability_fee = %{
"token" =>
TokenView.render("token.json", %{
token: transaction.transaction_fee_token,
contract_address_hash: bytes_to_address_hash(token_address_hash)
}),
"validator_address" => Helper.address_with_info(nil, nil, bytes_to_address_hash(validator_address_hash), false),
"dapp_address" => Helper.address_with_info(nil, nil, bytes_to_address_hash(dapp_address_hash), false),
"total_fee" => to_string(total_fee),
"dapp_fee" => to_string(dapp_fee),
"validator_fee" => to_string(validator_fee)
}
body
|> Map.put("stability_fee", stability_fee)
else
_ ->
body
end
end
end

@ -954,6 +954,145 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do
end
end
describe "stability fees" do
setup %{conn: conn} do
old_env = Application.get_env(:explorer, :chain_type)
Application.put_env(:explorer, :chain_type, "stability")
on_exit(fn ->
Application.put_env(:explorer, :chain_type, old_env)
end)
%{conn: conn}
end
test "check stability fees", %{conn: conn} do
tx = insert(:transaction) |> with_block()
log =
insert(:log,
transaction: tx,
index: 1,
block: tx.block,
block_number: tx.block_number,
first_topic: "0x99e7b0ba56da2819c37c047f0511fd2bf6c9b4e27b4a979a19d6da0f74be8155",
data:
"0x000000000000000000000000dc2b93f3291030f3f7a6d9363ac37757f7ad5c4300000000000000000000000000000000000000000000000000002824369a100000000000000000000000000046b555cb3962bf9533c437cbd04a2f702dfdb999000000000000000000000000000000000000000000000000000014121b4d0800000000000000000000000000faf7a981360c2fab3a5ab7b3d6d8d0cf97a91eb9000000000000000000000000000000000000000000000000000014121b4d0800"
)
insert(:token, contract_address: build(:address, hash: "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"))
request = get(conn, "/api/v2/transactions")
assert %{
"items" => [
%{
"stability_fee" => %{
"token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"},
"validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"},
"dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"},
"total_fee" => "44136000000000",
"dapp_fee" => "22068000000000",
"validator_fee" => "22068000000000"
}
}
]
} = json_response(request, 200)
request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}")
assert %{
"stability_fee" => %{
"token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"},
"validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"},
"dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"},
"total_fee" => "44136000000000",
"dapp_fee" => "22068000000000",
"validator_fee" => "22068000000000"
}
} = json_response(request, 200)
request = get(conn, "/api/v2/addresses/#{to_string(tx.from_address_hash)}/transactions")
assert %{
"items" => [
%{
"stability_fee" => %{
"token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"},
"validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"},
"dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"},
"total_fee" => "44136000000000",
"dapp_fee" => "22068000000000",
"validator_fee" => "22068000000000"
}
}
]
} = json_response(request, 200)
end
test "check stability if token absent in DB", %{conn: conn} do
tx = insert(:transaction) |> with_block()
log =
insert(:log,
transaction: tx,
index: 1,
block: tx.block,
block_number: tx.block_number,
first_topic: "0x99e7b0ba56da2819c37c047f0511fd2bf6c9b4e27b4a979a19d6da0f74be8155",
data:
"0x000000000000000000000000dc2b93f3291030f3f7a6d9363ac37757f7ad5c4300000000000000000000000000000000000000000000000000002824369a100000000000000000000000000046b555cb3962bf9533c437cbd04a2f702dfdb999000000000000000000000000000000000000000000000000000014121b4d0800000000000000000000000000faf7a981360c2fab3a5ab7b3d6d8d0cf97a91eb9000000000000000000000000000000000000000000000000000014121b4d0800"
)
request = get(conn, "/api/v2/transactions")
assert %{
"items" => [
%{
"stability_fee" => %{
"token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"},
"validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"},
"dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"},
"total_fee" => "44136000000000",
"dapp_fee" => "22068000000000",
"validator_fee" => "22068000000000"
}
}
]
} = json_response(request, 200)
request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}")
assert %{
"stability_fee" => %{
"token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"},
"validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"},
"dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"},
"total_fee" => "44136000000000",
"dapp_fee" => "22068000000000",
"validator_fee" => "22068000000000"
}
} = json_response(request, 200)
request = get(conn, "/api/v2/addresses/#{to_string(tx.from_address_hash)}/transactions")
assert %{
"items" => [
%{
"stability_fee" => %{
"token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"},
"validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"},
"dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"},
"total_fee" => "44136000000000",
"dapp_fee" => "22068000000000",
"validator_fee" => "22068000000000"
}
}
]
} = json_response(request, 200)
end
end
defp compare_item(%Transaction{} = transaction, json) do
assert to_string(transaction.hash) == json["hash"]
assert transaction.block_number == json["block"]

@ -229,7 +229,9 @@ defmodule Explorer.Chain.Log do
end
end
defp find_and_decode(abi, log, transaction) do
@spec find_and_decode([map()], __MODULE__.t(), Transaction.t()) ::
{:error, any} | {:ok, ABI.FunctionSelector.t(), any}
def find_and_decode(abi, log, transaction) do
with {%FunctionSelector{} = selector, mapping} <-
abi
|> ABI.parse_specification(include_events?: true)
@ -314,4 +316,11 @@ defmodule Explorer.Chain.Log do
|> String.trim_leading("0x")
|> Base.decode16!(case: :lower)
end
def fetch_log_by_tx_hash_and_first_topic(tx_hash, first_topic, options \\ []) do
__MODULE__
|> where([l], l.transaction_hash == ^tx_hash and l.first_topic == ^first_topic)
|> limit(1)
|> Chain.select_repo(options).one()
end
end

@ -23,8 +23,8 @@ defmodule Explorer.Chain.Token do
import Ecto.{Changeset, Query}
alias Ecto.Changeset
alias Explorer.{Chain, PagingOptions}
alias Explorer.Chain.{Address, Hash, Token}
alias Explorer.PagingOptions
alias Explorer.SmartContract.Helper
@typedoc """
@ -333,4 +333,8 @@ defmodule Explorer.Chain.Token do
defp page_tokens_by_column(%{contract_address_hash: contract_address_hash}, :contract_address_hash, :asc, nil) do
dynamic([t], t.contract_address_hash > ^contract_address_hash)
end
def get_by_contract_address_hash(hash, options) do
Chain.select_repo(options).get_by(__MODULE__, contract_address_hash: hash)
end
end

@ -24,6 +24,7 @@ defmodule Explorer.Chain.Transaction do
InternalTransaction,
Log,
SmartContract,
Token,
TokenTransfer,
Transaction,
TransactionAction,
@ -197,7 +198,9 @@ defmodule Explorer.Chain.Transaction do
max_priority_fee_per_gas: wei_per_gas | nil,
max_fee_per_gas: wei_per_gas | nil,
type: non_neg_integer() | nil,
has_error_in_internal_txs: boolean()
has_error_in_internal_txs: boolean(),
transaction_fee_log: any(),
transaction_fee_token: any()
},
suave
)
@ -290,6 +293,10 @@ defmodule Explorer.Chain.Transaction do
field(:has_error_in_internal_txs, :boolean)
field(:has_token_transfers, :boolean, virtual: true)
# stability virtual fields
field(:transaction_fee_log, :any, virtual: true)
field(:transaction_fee_token, :any, virtual: true)
# A transient field for deriving old block hash during transaction upserts.
# Used to force refetch of a block in case a transaction is re-collated
# in a different block. See: https://github.com/blockscout/blockscout/issues/1911
@ -1111,4 +1118,98 @@ defmodule Explorer.Chain.Transaction do
_ -> false
end
end
@api_true [api?: true]
@transaction_fee_event_signature "0x99e7b0ba56da2819c37c047f0511fd2bf6c9b4e27b4a979a19d6da0f74be8155"
@transaction_fee_event_abi [
%{
"anonymous" => false,
"inputs" => [
%{
"indexed" => false,
"internalType" => "address",
"name" => "token",
"type" => "address"
},
%{
"indexed" => false,
"internalType" => "uint256",
"name" => "totalFee",
"type" => "uint256"
},
%{
"indexed" => false,
"internalType" => "address",
"name" => "validator",
"type" => "address"
},
%{
"indexed" => false,
"internalType" => "uint256",
"name" => "validatorFee",
"type" => "uint256"
},
%{
"indexed" => false,
"internalType" => "address",
"name" => "dapp",
"type" => "address"
},
%{
"indexed" => false,
"internalType" => "uint256",
"name" => "dappFee",
"type" => "uint256"
}
],
"name" => "TransactionFee",
"type" => "event"
}
]
def maybe_prepare_stability_fees(transactions) do
if Application.get_env(:explorer, :chain_type) == "stability" do
maybe_prepare_stability_fees_inner(transactions)
else
transactions
end
end
defp maybe_prepare_stability_fees_inner(transactions) when is_list(transactions) do
{transactions, _tokens_acc} =
Enum.map_reduce(transactions, %{}, fn transaction, tokens_acc ->
case Log.fetch_log_by_tx_hash_and_first_topic(transaction.hash, @transaction_fee_event_signature, @api_true) do
fee_log when not is_nil(fee_log) ->
{:ok, _selector, mapping} = Log.find_and_decode(@transaction_fee_event_abi, fee_log, transaction)
[{"token", "address", false, token_address_hash}, _, _, _, _, _] = mapping
{token, new_tokens_acc} = check_tokens_acc(bytes_to_address_hash(token_address_hash), tokens_acc)
{%Transaction{transaction | transaction_fee_log: mapping, transaction_fee_token: token}, new_tokens_acc}
_ ->
{transaction, tokens_acc}
end
end)
transactions
end
defp maybe_prepare_stability_fees_inner(transaction) do
[transaction] = maybe_prepare_stability_fees_inner([transaction])
transaction
end
defp check_tokens_acc(token_address_hash, tokens_acc) do
if Map.has_key?(tokens_acc, token_address_hash) do
{tokens_acc[token_address_hash], tokens_acc}
else
token = Token.get_by_contract_address_hash(token_address_hash, @api_true)
{token, Map.put(tokens_acc, token_address_hash, token)}
end
end
def bytes_to_address_hash(bytes), do: %Hash{byte_count: 20, bytes: bytes}
end

Loading…
Cancel
Save