ERC-1155 transfers support in UI

pull/3902/head
Viktor Baranov 4 years ago
parent 9386adfb0b
commit d8b6f073c1
  1. 1
      .dialyzer-ignore
  2. 9
      apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex
  3. 6
      apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex
  4. 12
      apps/block_scout_web/lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex
  5. 2
      apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/_token_balances.html.eex
  6. 9
      apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex
  7. 9
      apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers.html.eex
  8. 21
      apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex
  9. 9
      apps/block_scout_web/lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex
  10. 6
      apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex
  11. 8
      apps/block_scout_web/lib/block_scout_web/views/tokens/helpers.ex
  12. 12
      apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex
  13. 1
      apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex
  14. 1
      apps/block_scout_web/lib/block_scout_web/views/tokens/transfer_view.ex
  15. 2
      apps/block_scout_web/lib/block_scout_web/views/transaction_token_transfer_view.ex
  16. 163
      apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex
  17. 6
      apps/block_scout_web/test/block_scout_web/views/tokens/holder_view_test.exs
  18. 8
      apps/block_scout_web/test/block_scout_web/views/transaction_view_test.exs
  19. 6
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex
  20. 56
      apps/explorer/lib/explorer/chain.ex
  21. 8
      apps/explorer/lib/explorer/chain/address/current_token_balance.ex
  22. 1
      apps/explorer/lib/explorer/chain/token.ex
  23. 1
      apps/explorer/lib/explorer/chain/token_transfer.ex
  24. 41
      apps/explorer/lib/explorer/token/balance_reader.ex
  25. 12
      apps/explorer/priv/repo/migrations/20210422115740_add_token_id_to_current_token_balances.exs
  26. 9
      apps/explorer/test/explorer/chain/import_test.exs
  27. 16
      apps/indexer/test/indexer/fetcher/token_balance_test.exs

@ -28,3 +28,4 @@ lib/explorer/staking/stake_snapshotting.ex:15: Function do_snapshotting/7 has no
lib/explorer/staking/stake_snapshotting.ex:147 lib/explorer/staking/stake_snapshotting.ex:147
lib/explorer/third_party_integrations/sourcify.ex:65 lib/explorer/third_party_integrations/sourcify.ex:65
lib/explorer/third_party_integrations/sourcify.ex:68 lib/explorer/third_party_integrations/sourcify.ex:68
lib/block_scout_web/views/transaction_view.ex:141

@ -45,6 +45,15 @@
) %> ) %>
<% end %> <% end %>
<%= if Enum.any?(@token_balances, & &1.token.type == "ERC-1155") do %>
<%= render(
"_tokens.html",
conn: @conn,
token_balances: filter_by_type(@token_balances, "ERC-1155"),
type: "ERC-1155"
) %>
<% end %>
<%= if Enum.any?(@token_balances, & &1.token.type == "ERC-20") do %> <%= if Enum.any?(@token_balances, & &1.token.type == "ERC-20") do %>
<%= render( <%= render(
"_tokens.html", "_tokens.html",

@ -23,8 +23,12 @@
<% end %> <% end %>
</div> </div>
<div class="row"> <div class="row">
<p class="mb-0 col-md-6"> <% col_md = if token_balance.token.usd_value, do: "col-md-6", else: "col-md-12" %>
<p class="mb-0 <%= col_md%> ">
<%= format_according_to_decimals(token_balance.value, token_balance.token.decimals) %> <%= token_balance.token.symbol %> <%= format_according_to_decimals(token_balance.value, token_balance.token.decimals) %> <%= token_balance.token.symbol %>
<%= if token_balance.token_type == "ERC-1155" do %>
<%= " TokenID " <> to_string(token_balance.token_id) %>
<% end %>
</p> </p>
<%= if token_balance.token.usd_value do %> <%= if token_balance.token.usd_value do %>
<p class="mb-0 col-md-6 text-right text-muted"> <p class="mb-0 col-md-6 text-right text-muted">

@ -0,0 +1,12 @@
<%= case @type do %>
<% :token_burning -> %>
<%= gettext("Token Burning") %>
<% :token_minting -> %>
<%= gettext("Token Minting") %>
<% :token_spawning -> %>
<%= gettext("Token Creation") %>
<% :token_transfer -> %>
<%= gettext("Token Transfer") %>
<% _ -> %>
<%= gettext("Token Transfer") %>
<% end %>

@ -7,7 +7,7 @@
<span> <span>
<span class="text-dark"> <span class="text-dark">
<%= format_token_balance_value(@token_balance.value, @token) %> <%= @token.symbol %> <%= format_token_balance_value(@token_balance.value, @token_balance.token_id, @token) %> <%= @token.symbol %>
</span> </span>
<%= if show_total_supply_percentage?(@token.total_supply) do %> <%= if show_total_supply_percentage?(@token.total_supply) do %>

@ -3,14 +3,7 @@
<!-- Color Block --> <!-- Color Block -->
<div class="tile-transaction-type-block col-md-2 d-flex flex-row flex-md-column"> <div class="tile-transaction-type-block col-md-2 d-flex flex-row flex-md-column">
<span class="tile-label"> <span class="tile-label">
<%= cond do %> <%= render(BlockScoutWeb.CommonComponentsView, "_token_transfer_type_display_name.html", type: Chain.get_token_transfer_type(@token_transfer)) %>
<% @token_transfer.to_address.hash == @burn_address_hash -> %>
<%= gettext("Token Burning") %>
<% @token_transfer.from_address.hash == @burn_address_hash -> %>
<%= gettext("Token Minting") %>
<% true -> %>
<%= gettext("Token Transfer") %>
<% end %>
</span> </span>
</div> </div>
<!-- Content --> <!-- Content -->

@ -1,8 +1,15 @@
<%= case token_transfer_amount(@transfer) do %> <%= case token_transfer_amount(@transfer) do %>
<% {:ok, :erc721_instance} -> %> <% {:ok, :erc721_instance} -> %>
<%= "TokenID ["%><%= link(short_token_id(@transfer.token_id, 30), to: token_instance_path(BlockScoutWeb.Endpoint, :show, @transfer.token.contract_address_hash, to_string(@transfer.token_id)), "data-test": "token_link") %><%= "]" %> <%= "TokenID ["%><%= link(short_token_id(@transfer.token_id, 30), to: token_instance_path(BlockScoutWeb.Endpoint, :show, @transfer.token.contract_address_hash, to_string(@transfer.token_id)), "data-test": "token_link") %><%= "]" %>
<% {:ok, :erc1155_instance, value} -> %>
<% transfer_type = Chain.get_token_transfer_type(@transfer) %>
<%= if transfer_type == :token_spawning do %>
<%= "TokenID ["%><%= link(short_token_id(@transfer.token_id, 30), to: token_instance_path(BlockScoutWeb.Endpoint, :show, @transfer.token.contract_address_hash, to_string(@transfer.token_id)), "data-test": "token_link") %><%= "]" %>
<% else %>
<%= "#{value} TokenID ["%><%= link(short_token_id(@transfer.token_id, 30), to: token_instance_path(BlockScoutWeb.Endpoint, :show, @transfer.token.contract_address_hash, to_string(@transfer.token_id)), "data-test": "token_link") %><%= "]" %>
<% end %>
<% {:ok, value} -> %> <% {:ok, value} -> %>
<%= value %> <%= value %>
<% end %> <% end %>
<%= " "%> <%= " " %>
<%= link(token_symbol(@transfer.token), to: token_path(BlockScoutWeb.Endpoint, :show, @transfer.token.contract_address_hash), "data-test": "token_link") %> <%= link(token_symbol(@transfer.token), to: token_path(BlockScoutWeb.Endpoint, :show, @transfer.token.contract_address_hash), "data-test": "token_link") %>

@ -225,18 +225,17 @@
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<% token_transfers = aggregate_token_transfers(transaction_with_transfers.token_transfers) %> <% %{transfers: transfers, mintings: mintings, burnings: burnings, creations: creations} = aggregate_token_transfers(transaction_with_transfers.token_transfers) %>
<%= if Enum.count(token_transfers) > 0 do %> <%= if Enum.count(transfers) > 0 do %>
<h2 class="card-title balance-card-title"><%= token_type_name(type)%><%= gettext " Token Transfer" %></h2> <h2 class="card-title balance-card-title"><%= token_type_name(type)%><%= gettext " Token Transfer" %></h2>
<div class="text-right"> <div class="text-right">
<%= for transfer <- token_transfers do %> <%= for transfer <- transfers do %>
<h3 class="address-balance-text"> <h3 class="address-balance-text">
<%= render BlockScoutWeb.TransactionView, "_total_transfers.html", Map.put(assigns, :transfer, transfer) %> <%= render BlockScoutWeb.TransactionView, "_total_transfers.html", Map.put(assigns, :transfer, transfer) %>
</h3> </h3>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<% mintings = aggregate_token_mintings(transaction_with_transfers.token_transfers) %>
<%= if Enum.count(mintings) > 0 do %> <%= if Enum.count(mintings) > 0 do %>
<h2 class="card-title balance-card-title"><%= token_type_name(type)%><%= gettext " Token Minting" %></h2> <h2 class="card-title balance-card-title"><%= token_type_name(type)%><%= gettext " Token Minting" %></h2>
<div class="text-right"> <div class="text-right">
@ -247,7 +246,6 @@
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<% burnings = aggregate_token_burnings(transaction_with_transfers.token_transfers) %>
<%= if Enum.count(burnings) > 0 do %> <%= if Enum.count(burnings) > 0 do %>
<h2 class="card-title balance-card-title"><%= token_type_name(type)%><%= gettext " Token Burning" %></h2> <h2 class="card-title balance-card-title"><%= token_type_name(type)%><%= gettext " Token Burning" %></h2>
<div class="text-right"> <div class="text-right">
@ -258,6 +256,19 @@
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<%# ERC-1155 token creations %>
<%= if Enum.count(creations) > 0 do %>
<h2 class="card-title balance-card-title"><%= token_type_name(type)%><%= gettext " Token Creation" %></h2>
<div class="text-right">
<%= for transfer <- creations do %>
<h3 class="address-balance-text">
<%= "TokenID ["%><%= link(short_token_id(transfer.token_id, 30), to: token_instance_path(BlockScoutWeb.Endpoint, :show, transfer.token.contract_address_hash, to_string(transfer.token_id)), "data-test": "token_link") %><%= "]" %>
<%= " "%>
<%= link(token_symbol(transfer.token), to: token_path(BlockScoutWeb.Endpoint, :show, transfer.token.contract_address_hash), "data-test": "token_link") %>
</h3>
<% end %>
</div>
<% end %>
</div> </div>
</div> </div>
<% _ -> %> <% _ -> %>

@ -1,14 +1,7 @@
<div class="tile tile-type-token-transfer fade-in"> <div class="tile tile-type-token-transfer fade-in">
<div class="row justify-content-end"> <div class="row justify-content-end">
<div class="col-12 col-md-4 col-lg-2 d-flex align-items-center justify-content-start justify-content-lg-center tile-label"> <div class="col-12 col-md-4 col-lg-2 d-flex align-items-center justify-content-start justify-content-lg-center tile-label">
<%= cond do %> <%= render(BlockScoutWeb.CommonComponentsView, "_token_transfer_type_display_name.html", type: Chain.get_token_transfer_type(@token_transfer)) %>
<% @token_transfer.to_address.hash == @burn_address_hash -> %>
<%= gettext("Token Burning") %>
<% @token_transfer.from_address.hash == @burn_address_hash -> %>
<%= gettext("Token Minting") %>
<% true -> %>
<%= gettext("Token Transfer") %>
<% end %>
</div> </div>
<div class="col-12 col-md-8 col-lg-10 d-flex flex-column text-nowrap"> <div class="col-12 col-md-8 col-lg-10 d-flex flex-column text-nowrap">

@ -172,6 +172,12 @@ defmodule BlockScoutWeb.API.RPC.AddressView do
|> Map.put_new(:tokenID, token_transfer.token_id) |> Map.put_new(:tokenID, token_transfer.token_id)
end end
defp prepare_token_transfer(%{token_type: "ERC-1155"} = token_transfer) do
token_transfer
|> prepare_common_token_transfer()
|> Map.put_new(:tokenID, token_transfer.token_id)
end
defp prepare_token_transfer(%{token_type: "ERC-20"} = token_transfer) do defp prepare_token_transfer(%{token_type: "ERC-20"} = token_transfer) do
token_transfer token_transfer
|> prepare_common_token_transfer() |> prepare_common_token_transfer()

@ -32,14 +32,14 @@ defmodule BlockScoutWeb.Tokens.Helpers do
{:ok, CurrencyHelpers.format_according_to_decimals(amount, decimals)} {:ok, CurrencyHelpers.format_according_to_decimals(amount, decimals)}
end end
defp do_token_transfer_amount(%Token{type: "ERC-1155", decimals: decimals}, amount, _token_id) do
{:ok, CurrencyHelpers.format_according_to_decimals(amount, decimals)}
end
defp do_token_transfer_amount(%Token{type: "ERC-721"}, _amount, _token_id) do defp do_token_transfer_amount(%Token{type: "ERC-721"}, _amount, _token_id) do
{:ok, :erc721_instance} {:ok, :erc721_instance}
end end
defp do_token_transfer_amount(%Token{type: "ERC-1155", decimals: decimals}, amount, _token_id) do
{:ok, :erc1155_instance, CurrencyHelpers.format_according_to_decimals(amount, decimals)}
end
defp do_token_transfer_amount(_token, _amount, _token_id) do defp do_token_transfer_amount(_token, _amount, _token_id) do
nil nil
end end

@ -50,19 +50,23 @@ defmodule BlockScoutWeb.Tokens.HolderView do
## Examples ## Examples
iex> token = build(:token, type: "ERC-20", decimals: Decimal.new(2)) iex> token = build(:token, type: "ERC-20", decimals: Decimal.new(2))
iex> BlockScoutWeb.Tokens.HolderView.format_token_balance_value(100000, token) iex> BlockScoutWeb.Tokens.HolderView.format_token_balance_value(100000, nil, token)
"1,000" "1,000"
iex> token = build(:token, type: "ERC-721") iex> token = build(:token, type: "ERC-721")
iex> BlockScoutWeb.Tokens.HolderView.format_token_balance_value(1, token) iex> BlockScoutWeb.Tokens.HolderView.format_token_balance_value(1, nil, token)
1 1
""" """
def format_token_balance_value(value, %Token{type: "ERC-20", decimals: decimals}) do def format_token_balance_value(value, _id, %Token{type: "ERC-20", decimals: decimals}) do
format_according_to_decimals(value, decimals) format_according_to_decimals(value, decimals)
end end
def format_token_balance_value(value, _token) do def format_token_balance_value(value, id, %Token{type: "ERC-1155", decimals: decimals}) do
to_string(format_according_to_decimals(value, decimals)) <> " TokenID " <> to_string(id)
end
def format_token_balance_value(value, _id, _token) do
value value
end end
end end

@ -44,6 +44,7 @@ defmodule BlockScoutWeb.Tokens.OverviewView do
defp tab_name(["inventory"]), do: gettext("Inventory") defp tab_name(["inventory"]), do: gettext("Inventory")
def display_inventory?(%Token{type: "ERC-721"}), do: true def display_inventory?(%Token{type: "ERC-721"}), do: true
def display_inventory?(%Token{type: "ERC-1155"}), do: true
def display_inventory?(_), do: false def display_inventory?(_), do: false
def smart_contract_with_read_only_functions?( def smart_contract_with_read_only_functions?(

@ -2,5 +2,6 @@ defmodule BlockScoutWeb.Tokens.TransferView do
use BlockScoutWeb, :view use BlockScoutWeb, :view
alias BlockScoutWeb.Tokens.OverviewView alias BlockScoutWeb.Tokens.OverviewView
alias Explorer.Chain
alias Explorer.Chain.Address alias Explorer.Chain.Address
end end

@ -1,3 +1,5 @@
defmodule BlockScoutWeb.TransactionTokenTransferView do defmodule BlockScoutWeb.TransactionTokenTransferView do
use BlockScoutWeb, :view use BlockScoutWeb, :view
alias Explorer.Chain
end end

@ -16,16 +16,15 @@ defmodule BlockScoutWeb.TransactionView do
@tabs ["token-transfers", "internal-transactions", "logs", "raw-trace"] @tabs ["token-transfers", "internal-transactions", "logs", "raw-trace"]
{:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000")
@burn_address_hash burn_address_hash
@token_burning_title "Token Burning" @token_burning_title "Token Burning"
@token_minting_title "Token Minting" @token_minting_title "Token Minting"
@token_transfer_title "Token Transfer" @token_transfer_title "Token Transfer"
@token_creation_title "Token Creation"
@token_burning_type "token-burning" @token_burning_type :token_burning
@token_minting_type "token-minting" @token_minting_type :token_minting
@token_transfer_type "token-transfer" @token_creation_type :token_spawning
@token_transfer_type :token_transfer
defguardp is_transaction_type(mod) when mod in [InternalTransaction, Transaction] defguardp is_transaction_type(mod) when mod in [InternalTransaction, Transaction]
@ -63,52 +62,80 @@ defmodule BlockScoutWeb.TransactionView do
end end
def aggregate_token_transfers(token_transfers) do def aggregate_token_transfers(token_transfers) do
{transfers, nft_transfers} = %{
transfers: {ft_transfers, nft_transfers},
mintings: {ft_mintings, nft_mintings},
burnings: {ft_burnings, nft_burnings},
creations: {ft_creations, nft_creations}
} =
token_transfers token_transfers
|> Enum.reduce({%{}, []}, fn token_transfer, acc -> |> Enum.reduce(
if token_transfer.to_address_hash != @burn_address_hash && %{
token_transfer.from_address_hash != @burn_address_hash do transfers: {%{}, []},
aggregate_reducer(token_transfer, acc) mintings: {%{}, []},
else burnings: {%{}, []},
acc creations: {%{}, []}
},
fn token_transfer, acc ->
token_transfer_type = Chain.get_token_transfer_type(token_transfer)
case token_transfer_type do
:token_transfer ->
transfers = aggregate_reducer(token_transfer, acc.transfers)
%{
transfers: transfers,
mintings: acc.mintings,
burnings: acc.burnings,
creations: acc.creations
}
:token_burning ->
burnings = aggregate_reducer(token_transfer, acc.burnings)
%{
transfers: acc.transfers,
mintings: acc.mintings,
burnings: burnings,
creations: acc.creations
}
:token_minting ->
mintings = aggregate_reducer(token_transfer, acc.mintings)
%{
transfers: acc.transfers,
mintings: mintings,
burnings: acc.burnings,
creations: acc.creations
}
:token_spawning ->
creations = aggregate_reducer(token_transfer, acc.creations)
%{
transfers: acc.transfers,
mintings: acc.mintings,
burnings: acc.burnings,
creations: creations
}
end
end end
end) )
final_transfers = Map.values(transfers) final_ft_transfers = Map.values(ft_transfers)
transfers = final_ft_transfers ++ nft_transfers
final_transfers ++ nft_transfers final_ft_mintings = Map.values(ft_mintings)
end mintings = final_ft_mintings ++ nft_mintings
def aggregate_token_mintings(token_transfers) do final_ft_burnings = Map.values(ft_burnings)
{transfers, nft_transfers} = burnings = final_ft_burnings ++ nft_burnings
token_transfers
|> Enum.reduce({%{}, []}, fn token_transfer, acc ->
if token_transfer.from_address_hash == @burn_address_hash do
aggregate_reducer(token_transfer, acc)
else
acc
end
end)
final_transfers = Map.values(transfers) final_ft_creations = Map.values(ft_creations)
creations = final_ft_creations ++ nft_creations
final_transfers ++ nft_transfers %{transfers: transfers, mintings: mintings, burnings: burnings, creations: creations}
end
def aggregate_token_burnings(token_transfers) do
{transfers, nft_transfers} =
token_transfers
|> Enum.reduce({%{}, []}, fn token_transfer, acc ->
if token_transfer.to_address_hash == @burn_address_hash do
aggregate_reducer(token_transfer, acc)
else
acc
end
end)
final_transfers = Map.values(transfers)
final_transfers ++ nft_transfers
end end
defp aggregate_reducer(%{amount: amount} = token_transfer, {acc1, acc2}) when is_nil(amount) do defp aggregate_reducer(%{amount: amount} = token_transfer, {acc1, acc2}) when is_nil(amount) do
@ -147,6 +174,7 @@ defmodule BlockScoutWeb.TransactionView do
case type do case type do
:erc20 -> gettext("ERC-20 ") :erc20 -> gettext("ERC-20 ")
:erc721 -> gettext("ERC-721 ") :erc721 -> gettext("ERC-721 ")
:erc1155 -> gettext("ERC-1155 ")
_ -> "" _ -> ""
end end
end end
@ -336,11 +364,12 @@ defmodule BlockScoutWeb.TransactionView do
def transaction_display_type(%Transaction{} = transaction) do def transaction_display_type(%Transaction{} = transaction) do
cond do cond do
involves_token_transfers?(transaction) -> involves_token_transfers?(transaction) ->
token_transfer_type = get_token_transfer_type(transaction.token_transfers) token_transfer_type = get_transaction_type_from_token_transfers(transaction.token_transfers)
case token_transfer_type do case token_transfer_type do
@token_minting_type -> gettext(@token_minting_title) @token_minting_type -> gettext(@token_minting_title)
@token_burning_type -> gettext(@token_burning_title) @token_burning_type -> gettext(@token_burning_title)
@token_creation_type -> gettext(@token_creation_title)
@token_transfer_type -> gettext(@token_transfer_title) @token_transfer_type -> gettext(@token_transfer_title)
end end
@ -407,35 +436,27 @@ defmodule BlockScoutWeb.TransactionView do
defp tab_name(["logs"]), do: gettext("Logs") defp tab_name(["logs"]), do: gettext("Logs")
defp tab_name(["raw-trace"]), do: gettext("Raw Trace") defp tab_name(["raw-trace"]), do: gettext("Raw Trace")
defp get_token_transfer_type(token_transfers) do defp get_transaction_type_from_token_transfers(token_transfers) do
token_transfers token_transfers_types =
|> Enum.reduce("", fn token_transfer, type -> token_transfers
cond do |> Enum.map(fn token_transfer ->
token_transfer.to_address_hash == @burn_address_hash -> Chain.get_token_transfer_type(token_transfer)
update_transfer_type_if_burning(type) end)
token_transfer.from_address_hash == @burn_address_hash -> burnings_count =
update_transfer_type_if_minting(type) Enum.count(token_transfers_types, fn token_transfers_type -> token_transfers_type == @token_burning_type end)
true -> mintings_count =
@token_transfer_type Enum.count(token_transfers_types, fn token_transfers_type -> token_transfers_type == @token_minting_type end)
end
end)
end
defp update_transfer_type_if_minting(type) do creations_count =
case type do Enum.count(token_transfers_types, fn token_transfers_type -> token_transfers_type == @token_creation_type end)
"" -> @token_minting_type
@token_burning_type -> @token_transfer_type
_ -> type
end
end
defp update_transfer_type_if_burning(type) do cond do
case type do Enum.count(token_transfers_types) == burnings_count -> @token_burning_type
"" -> @token_burning_type Enum.count(token_transfers_types) == mintings_count -> @token_minting_type
@token_minting_type -> @token_transfer_type Enum.count(token_transfers_types) == creations_count -> @token_creation_type
_ -> type true -> @token_transfer_type
end end
end end
end end

@ -42,19 +42,19 @@ defmodule BlockScoutWeb.Tokens.HolderViewTest do
end end
end end
describe "format_token_balance_value/1" do describe "format_token_balance_value/3" do
test "formats according to token decimals when it's a ERC-20" do test "formats according to token decimals when it's a ERC-20" do
token = build(:token, type: "ERC-20", decimals: Decimal.new(2)) token = build(:token, type: "ERC-20", decimals: Decimal.new(2))
token_balance = build(:token_balance, value: 2_000_000) token_balance = build(:token_balance, value: 2_000_000)
assert HolderView.format_token_balance_value(token_balance.value, token) == "20,000" assert HolderView.format_token_balance_value(token_balance.value, nil, token) == "20,000"
end end
test "returns the value when it's ERC-721" do test "returns the value when it's ERC-721" do
token = build(:token, type: "ERC-721") token = build(:token, type: "ERC-721")
token_balance = build(:token_balance, value: 1) token_balance = build(:token_balance, value: 1)
assert HolderView.format_token_balance_value(token_balance.value, token) == 1 assert HolderView.format_token_balance_value(token_balance.value, nil, token) == 1
end end
end end
end end

@ -264,8 +264,8 @@ defmodule BlockScoutWeb.TransactionViewTest do
result = TransactionView.aggregate_token_transfers([token_transfer, token_transfer, token_transfer]) result = TransactionView.aggregate_token_transfers([token_transfer, token_transfer, token_transfer])
assert Enum.count(result) == 1 assert Enum.count(result.transfers) == 1
assert List.first(result).amount == Decimal.new(3) assert List.first(result.transfers).amount == Decimal.new(3)
end end
test "does not aggregate NFT tokens" do test "does not aggregate NFT tokens" do
@ -278,8 +278,8 @@ defmodule BlockScoutWeb.TransactionViewTest do
result = TransactionView.aggregate_token_transfers([token_transfer, token_transfer, token_transfer]) result = TransactionView.aggregate_token_transfers([token_transfer, token_transfer, token_transfer])
assert Enum.count(result) == 3 assert Enum.count(result.transfers) == 3
assert List.first(result).amount == nil assert List.first(result.transfers).amount == nil
end end
end end
end end

@ -168,7 +168,11 @@ defmodule EthereumJSONRPC do
""" """
@spec execute_contract_functions([Contract.call()], [map()], json_rpc_named_arguments) :: [Contract.call_result()] @spec execute_contract_functions([Contract.call()], [map()], json_rpc_named_arguments) :: [Contract.call_result()]
def execute_contract_functions(functions, abi, json_rpc_named_arguments) do def execute_contract_functions(functions, abi, json_rpc_named_arguments) do
Contract.execute_contract_functions(functions, abi, json_rpc_named_arguments) if Enum.count(functions) > 0 do
Contract.execute_contract_functions(functions, abi, json_rpc_named_arguments)
else
[]
end
end end
@doc """ @doc """

@ -98,6 +98,8 @@ defmodule Explorer.Chain do
# keccak256("Error(string)") # keccak256("Error(string)")
@revert_error_method_id "08c379a0" @revert_error_method_id "08c379a0"
@burn_address_hash_str "0x0000000000000000000000000000000000000000"
@typedoc """ @typedoc """
The name of an association on the `t:Ecto.Schema.t/0` The name of an association on the `t:Ecto.Schema.t/0`
""" """
@ -1635,7 +1637,7 @@ defmodule Explorer.Chain do
@spec fetch_sum_coin_total_supply_minus_burnt() :: non_neg_integer @spec fetch_sum_coin_total_supply_minus_burnt() :: non_neg_integer
def fetch_sum_coin_total_supply_minus_burnt do def fetch_sum_coin_total_supply_minus_burnt do
{:ok, burn_address_hash} = string_to_address_hash("0x0000000000000000000000000000000000000000") {:ok, burn_address_hash} = Chain.string_to_address_hash(@burn_address_hash_str)
query = query =
from( from(
@ -5187,7 +5189,7 @@ defmodule Explorer.Chain do
end end
@spec transaction_token_transfer_type(Transaction.t()) :: @spec transaction_token_transfer_type(Transaction.t()) ::
:erc20 | :erc721 | :token_transfer | nil :erc20 | :erc721 | :erc1155 | :token_transfer | nil
def transaction_token_transfer_type( def transaction_token_transfer_type(
%Transaction{ %Transaction{
status: :ok, status: :ok,
@ -5234,10 +5236,24 @@ defmodule Explorer.Chain do
find_erc721_token_transfer(transaction.token_transfers, {from_address, to_address}) find_erc721_token_transfer(transaction.token_transfers, {from_address, to_address})
# safeTransferFrom(address,address,uint256,uint256,bytes)
{"0xf242432a" <> params, ^zero_wei} ->
types = [:address, :address, {:uint, 256}, {:uint, 256}, :bytes]
[from_address, to_address, _id, _value, _data] = decode_params(params, types)
find_erc1155_token_transfer(transaction.token_transfers, {from_address, to_address})
# safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)
{"0x2eb2c2d6" <> params, ^zero_wei} ->
types = [:address, :address, [{:uint, 256}], [{:uint, 256}], :bytes]
[from_address, to_address, _ids, _values, _data] = decode_params(params, types)
find_erc1155_token_transfer(transaction.token_transfers, {from_address, to_address})
{"0xf907fc5b" <> _params, ^zero_wei} -> {"0xf907fc5b" <> _params, ^zero_wei} ->
:erc20 :erc20
# check for ERC 20 or for old ERC 721 token versions # check for ERC-20 or for old ERC-721, ERC-1155 token versions
{unquote(TokenTransfer.transfer_function_signature()) <> params, ^zero_wei} -> {unquote(TokenTransfer.transfer_function_signature()) <> params, ^zero_wei} ->
types = [:address, {:uint, 256}] types = [:address, {:uint, 256}]
@ -5245,7 +5261,7 @@ defmodule Explorer.Chain do
decimal_value = Decimal.new(value) decimal_value = Decimal.new(value)
find_erc721_or_erc20_token_transfer(transaction.token_transfers, {address, decimal_value}) find_erc721_or_erc20_or_erc1155_token_transfer(transaction.token_transfers, {address, decimal_value})
_ -> _ ->
nil nil
@ -5261,7 +5277,16 @@ defmodule Explorer.Chain do
if token_transfer, do: :erc721 if token_transfer, do: :erc721
end end
defp find_erc721_or_erc20_token_transfer(token_transfers, {address, decimal_value}) do defp find_erc1155_token_transfer(token_transfers, {from_address, to_address}) do
token_transfer =
Enum.find(token_transfers, fn token_transfer ->
token_transfer.from_address_hash.bytes == from_address && token_transfer.to_address_hash.bytes == to_address
end)
if token_transfer, do: :erc1155
end
defp find_erc721_or_erc20_or_erc1155_token_transfer(token_transfers, {address, decimal_value}) do
token_transfer = token_transfer =
Enum.find(token_transfers, fn token_transfer -> Enum.find(token_transfers, fn token_transfer ->
token_transfer.to_address_hash.bytes == address && token_transfer.amount == decimal_value token_transfer.to_address_hash.bytes == address && token_transfer.amount == decimal_value
@ -5271,6 +5296,7 @@ defmodule Explorer.Chain do
case token_transfer.token do case token_transfer.token do
%Token{type: "ERC-20"} -> :erc20 %Token{type: "ERC-20"} -> :erc20
%Token{type: "ERC-721"} -> :erc721 %Token{type: "ERC-721"} -> :erc721
%Token{type: "ERC-1155"} -> :erc1155
_ -> nil _ -> nil
end end
else else
@ -6317,4 +6343,24 @@ defmodule Explorer.Chain do
_ -> "" _ -> ""
end end
end end
@spec get_token_transfer_type(TokenTransfer.t()) ::
:token_burning | :token_minting | :token_spawning | :token_transfer
def get_token_transfer_type(transfer) do
{:ok, burn_address_hash} = Chain.string_to_address_hash(@burn_address_hash_str)
cond do
transfer.to_address_hash == burn_address_hash && transfer.from_address_hash !== burn_address_hash ->
:token_burning
transfer.to_address_hash !== burn_address_hash && transfer.from_address_hash == burn_address_hash ->
:token_minting
transfer.to_address_hash == burn_address_hash && transfer.from_address_hash == burn_address_hash ->
:token_spawning
true ->
:token_transfer
end
end
end end

@ -23,6 +23,8 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do
* `token_contract_address_hash` - The contract address hash foreign key. * `token_contract_address_hash` - The contract address hash foreign key.
* `block_number` - The block's number that the transfer took place. * `block_number` - The block's number that the transfer took place.
* `value` - The value that's represents the balance. * `value` - The value that's represents the balance.
* `token_id` - The token_id of the transferred token (applicable for ERC-1155 and ERC-721 tokens)
* `token_type` - The type of the token
""" """
@type t :: %__MODULE__{ @type t :: %__MODULE__{
address: %Ecto.Association.NotLoaded{} | Address.t(), address: %Ecto.Association.NotLoaded{} | Address.t(),
@ -39,6 +41,8 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do
field(:value, :decimal) field(:value, :decimal)
field(:block_number, :integer) field(:block_number, :integer)
field(:value_fetched_at, :utc_datetime_usec) field(:value_fetched_at, :utc_datetime_usec)
field(:token_id, :decimal)
field(:token_type, :string)
# A transient field for deriving token holder count deltas during address_current_token_balances upserts # A transient field for deriving token holder count deltas during address_current_token_balances upserts
field(:old_value, :decimal) field(:old_value, :decimal)
@ -56,8 +60,8 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do
timestamps() timestamps()
end end
@optional_fields ~w(value value_fetched_at)a @optional_fields ~w(value value_fetched_at token_id)a
@required_fields ~w(address_hash block_number token_contract_address_hash)a @required_fields ~w(address_hash block_number token_contract_address_hash token_type)a
@allowed_fields @optional_fields ++ @required_fields @allowed_fields @optional_fields ++ @required_fields
@doc false @doc false

@ -8,6 +8,7 @@ defmodule Explorer.Chain.Token do
* ERC-20 * ERC-20
* ERC-721 * ERC-721
* ERC-1155
## Token Specifications ## Token Specifications

@ -300,6 +300,7 @@ defmodule Explorer.Chain.TokenTransfer do
left_join: instance in Instance, left_join: instance in Instance,
on: tt.token_contract_address_hash == instance.token_contract_address_hash and tt.token_id == instance.token_id, on: tt.token_contract_address_hash == instance.token_contract_address_hash and tt.token_id == instance.token_id,
where: tt.token_contract_address_hash == ^contract_address_hash, where: tt.token_contract_address_hash == ^contract_address_hash,
where: tt.to_address_hash != ^"0x0000000000000000000000000000000000000000",
order_by: [desc: tt.block_number], order_by: [desc: tt.block_number],
distinct: [desc: tt.token_id], distinct: [desc: tt.token_id],
preload: [:to_address], preload: [:to_address],

@ -45,6 +45,13 @@ defmodule Explorer.Token.BalanceReader do
def get_balances_of(token_balance_requests) do def get_balances_of(token_balance_requests) do
regular_balances = regular_balances =
token_balance_requests token_balance_requests
|> Enum.filter(fn request ->
if Map.has_key?(request, :token_type) do
request.token_type !== "ERC-1155"
else
true
end
end)
|> Enum.map(&format_balance_request/1) |> Enum.map(&format_balance_request/1)
|> Reader.query_contracts(@balance_function_abi) |> Reader.query_contracts(@balance_function_abi)
|> Enum.map(&format_balance_result/1) |> Enum.map(&format_balance_result/1)
@ -52,21 +59,13 @@ defmodule Explorer.Token.BalanceReader do
erc1155_balances = erc1155_balances =
token_balance_requests token_balance_requests
|> Enum.filter(fn request -> |> Enum.filter(fn request ->
request.token_type == "ERC-1155" if Map.has_key?(request, :token_type) do
end) request.token_type == "ERC-1155"
|> Enum.map(fn %{ else
address_hash: address_hash, false
block_number: block_number, end
token_contract_address_hash: token_contract_address_hash,
token_id: token_id
} ->
%{
contract_address: token_contract_address_hash,
function_name: "balanceOf",
args: [address_hash, token_id],
block_number: block_number
}
end) end)
|> Enum.map(&format_erc_1155_balance_request/1)
|> Reader.query_contracts(@erc1155_balance_function_abi) |> Reader.query_contracts(@erc1155_balance_function_abi)
|> Enum.map(&format_balance_result/1) |> Enum.map(&format_balance_result/1)
@ -86,6 +85,20 @@ defmodule Explorer.Token.BalanceReader do
} }
end end
defp format_erc_1155_balance_request(%{
address_hash: address_hash,
block_number: block_number,
token_contract_address_hash: token_contract_address_hash,
token_id: token_id
}) do
%{
contract_address: token_contract_address_hash,
method_id: "00fdd58e",
args: [address_hash, token_id],
block_number: block_number
}
end
defp format_balance_result({:ok, [balance]}) do defp format_balance_result({:ok, [balance]}) do
{:ok, balance} {:ok, balance}
end end

@ -0,0 +1,12 @@
defmodule Explorer.Repo.Migrations.AddTokenIdToCurrentTokenBalances do
use Ecto.Migration
def change do
alter table(:address_current_token_balances) do
add(:token_id, :numeric, precision: 78, scale: 0, null: true)
add(:token_type, :string, null: true)
end
create(index(:address_current_token_balances, [:token_id]))
end
end

@ -428,13 +428,15 @@ defmodule Explorer.Chain.ImportTest do
address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
block_number: "37", block_number: "37",
value: 200 value: 200,
token_type: "ERC-20"
}, },
%{ %{
address_hash: "0x515c09c5bba1ed566b02a5b0599ec5d5d0aee73d", address_hash: "0x515c09c5bba1ed566b02a5b0599ec5d5d0aee73d",
token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
block_number: "37", block_number: "37",
value: 100 value: 100,
token_type: "ERC-20"
} }
], ],
timeout: 5 timeout: 5
@ -2264,7 +2266,8 @@ defmodule Explorer.Chain.ImportTest do
address_hash: address_hash, address_hash: address_hash,
token_contract_address_hash: token_contract_address_hash, token_contract_address_hash: token_contract_address_hash,
block_number: block_number, block_number: block_number,
value: value_after value: value_after,
token_type: "ERC-20"
} }
] ]
}, },

@ -76,22 +76,6 @@ defmodule Indexer.Fetcher.TokenBalanceTest do
token_balance_a = insert(:token_balance, value_fetched_at: nil, value: nil) token_balance_a = insert(:token_balance, value_fetched_at: nil, value: nil)
token_balance_b = insert(:token_balance, value_fetched_at: nil, value: nil) token_balance_b = insert(:token_balance, value_fetched_at: nil, value: nil)
expect(
EthereumJSONRPC.Mox,
:json_rpc,
1,
fn [%{id: id, method: "eth_call", params: [%{data: _, to: _}, _]}], _options ->
{:ok,
[
%{
error: %{code: -32015, data: "Reverted 0x", message: "VM execution error."},
id: id,
jsonrpc: "2.0"
}
]}
end
)
token_balances = [ token_balances = [
{ {
token_balance_a.address_hash.bytes, token_balance_a.address_hash.bytes,

Loading…
Cancel
Save