Merge pull request #4761 from blockscout/vb-erc-1155-public

ERC-1155 support
pull/4776/head
Victor Baranov 3 years ago committed by GitHub
commit eaadac2fcc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 77
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/holder_controller.ex
  3. 14
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/token_controller.ex
  4. 9
      apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex
  5. 17
      apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex
  6. 12
      apps/block_scout_web/lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex
  7. 2
      apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/_token_balances.html.eex
  8. 41
      apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/holder/index.html.eex
  9. 9
      apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex
  10. 52
      apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_token.html.eex
  11. 9
      apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex
  12. 1
      apps/block_scout_web/lib/block_scout_web/templates/transaction/_link_to_token_instance.html.eex
  13. 1
      apps/block_scout_web/lib/block_scout_web/templates/transaction/_link_to_token_symbol.html.eex
  14. 23
      apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers.html.eex
  15. 2
      apps/block_scout_web/lib/block_scout_web/templates/transaction/_transfer_token_with_id.html.eex
  16. 9
      apps/block_scout_web/lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex
  17. 6
      apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex
  18. 24
      apps/block_scout_web/lib/block_scout_web/views/tokens/helpers.ex
  19. 12
      apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex
  20. 5
      apps/block_scout_web/lib/block_scout_web/views/tokens/instance/holder_view.ex
  21. 1
      apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex
  22. 1
      apps/block_scout_web/lib/block_scout_web/views/tokens/inventory_view.ex
  23. 1
      apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex
  24. 1
      apps/block_scout_web/lib/block_scout_web/views/tokens/transfer_view.ex
  25. 2
      apps/block_scout_web/lib/block_scout_web/views/transaction_token_transfer_view.ex
  26. 44
      apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex
  27. 14
      apps/block_scout_web/lib/block_scout_web/web_router.ex
  28. 81
      apps/block_scout_web/priv/gettext/default.pot
  29. 81
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  30. 6
      apps/block_scout_web/test/block_scout_web/views/tokens/holder_view_test.exs
  31. 6
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex
  32. 70
      apps/explorer/lib/explorer/chain.ex
  33. 107
      apps/explorer/lib/explorer/chain/address/current_token_balance.ex
  34. 35
      apps/explorer/lib/explorer/chain/address/token.ex
  35. 17
      apps/explorer/lib/explorer/chain/address/token_balance.ex
  36. 126
      apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex
  37. 121
      apps/explorer/lib/explorer/chain/import/runner/address/token_balances.ex
  38. 120
      apps/explorer/lib/explorer/chain/import/runner/blocks.ex
  39. 71
      apps/explorer/lib/explorer/chain/import/runner/tokens.ex
  40. 1
      apps/explorer/lib/explorer/chain/token.ex
  41. 19
      apps/explorer/lib/explorer/chain/token_transfer.ex
  42. 51
      apps/explorer/lib/explorer/token/balance_reader.ex
  43. 118
      apps/explorer/lib/explorer/token/instance_metadata_retriever.ex
  44. 12
      apps/explorer/priv/repo/migrations/20200214152058_add_token_id_to_token_balances.exs
  45. 12
      apps/explorer/priv/repo/migrations/20210422115740_add_token_id_to_current_token_balances.exs
  46. 25
      apps/explorer/priv/repo/migrations/20210423084253_address_current_token_balances_add_token_id_to_unique_index.exs
  47. 25
      apps/explorer/priv/repo/migrations/20210423091652_address_token_balances_add_token_id_to_unique_index.exs
  48. 32
      apps/explorer/priv/repo/migrations/20210423094801_address_token_balances_change_unfetched_token_balances_unique_index.exs
  49. 10
      apps/explorer/priv/repo/migrations/20210423115108_extend_token_transfers_for_erc1155.exs
  50. 55
      apps/explorer/priv/repo/migrations/20211013190346_remove_duplicates_of_current_token_balances.exs
  51. 135
      apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs
  52. 60
      apps/explorer/test/explorer/chain/import/runner/address/token_balances_test.exs
  53. 25
      apps/explorer/test/explorer/chain/import_test.exs
  54. 6
      apps/explorer/test/explorer/token/balance_reader_test.exs
  55. 96
      apps/explorer/test/explorer/token/instance_metadata_retriever_test.exs
  56. 3
      apps/explorer/test/support/factory.ex
  57. 23
      apps/indexer/lib/indexer/fetcher/token_balance.ex
  58. 75
      apps/indexer/lib/indexer/token_balances.ex
  59. 29
      apps/indexer/lib/indexer/transform/address_token_balances.ex
  60. 92
      apps/indexer/lib/indexer/transform/token_transfers.ex
  61. 70
      apps/indexer/test/indexer/fetcher/token_balance_test.exs
  62. 311
      apps/indexer/test/indexer/token_balances_test.exs
  63. 11
      apps/indexer/test/indexer/transform/address_token_balances_test.exs
  64. 78
      apps/indexer/test/indexer/transform/token_transfers_test.exs

@ -1,6 +1,7 @@
## Current
### Features
- [#4761](https://github.com/blockscout/blockscout/pull/4761) - ERC-1155 support
- [#4739](https://github.com/blockscout/blockscout/pull/4739) - Improve logs and inputs decoding
- [#4747](https://github.com/blockscout/blockscout/pull/4747) - Advanced CSV export
- [#4745](https://github.com/blockscout/blockscout/pull/4745) - Vyper contracts verification

@ -0,0 +1,77 @@
defmodule BlockScoutWeb.Tokens.Instance.HolderController do
use BlockScoutWeb, :controller
alias BlockScoutWeb.Tokens.HolderView
alias Explorer.{Chain, Market}
alias Explorer.Chain.Address
alias Phoenix.View
import BlockScoutWeb.Chain, only: [split_list_by_page: 1, paging_options: 1, next_page_params: 3]
def index(conn, %{"token_id" => token_address_hash, "instance_id" => token_id, "type" => "JSON"} = params) do
with {:ok, address_hash} <- Chain.string_to_address_hash(token_address_hash),
{:ok, token} <- Chain.token_from_address_hash(address_hash),
token_holders <-
Chain.fetch_token_holders_from_token_hash_and_token_id(address_hash, token_id, paging_options(params)) do
{token_holders_paginated, next_page} = split_list_by_page(token_holders)
next_page_path =
case next_page_params(next_page, token_holders_paginated, params) do
nil ->
nil
next_page_params ->
token_instance_holder_path(
conn,
:index,
Address.checksum(token.contract_address_hash),
token_id,
Map.delete(next_page_params, "type")
)
end
holders_json =
token_holders_paginated
|> Enum.sort_by(& &1.value, &>=/2)
|> Enum.map(fn current_token_balance ->
View.render_to_string(
HolderView,
"_token_balances.html",
address_hash: address_hash,
token_balance: current_token_balance,
token: token
)
end)
json(conn, %{items: holders_json, next_page_path: next_page_path})
else
_ ->
not_found(conn)
end
end
def index(conn, %{"token_id" => token_address_hash, "instance_id" => token_id}) do
options = [necessity_by_association: %{[contract_address: :smart_contract] => :optional}]
with {:ok, hash} <- Chain.string_to_address_hash(token_address_hash),
{:ok, token} <- Chain.token_from_address_hash(hash, options),
{:ok, token_transfer} <-
Chain.erc721_token_instance_from_token_id_and_token_address(token_id, hash) do
render(
conn,
"index.html",
token_instance: token_transfer,
current_path: current_path(conn),
token: Market.add_price(token),
total_token_transfers: Chain.count_token_transfers_from_token_hash_and_token_id(hash, token_id)
)
else
_ ->
not_found(conn)
end
end
def index(conn, _) do
not_found(conn)
end
end

@ -12,18 +12,18 @@ defmodule BlockScoutWeb.Tokens.TokenController do
end
def token_counters(conn, %{"id" => address_hash_string}) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, token} <- Chain.token_from_address_hash(address_hash) do
{transfer_count, token_holder_count} = fetch_token_counters(token, address_hash, 200)
case Chain.string_to_address_hash(address_hash_string) do
{:ok, address_hash} ->
{transfer_count, token_holder_count} = fetch_token_counters(address_hash, 200)
json(conn, %{transfer_count: transfer_count, token_holder_count: token_holder_count})
json(conn, %{transfer_count: transfer_count, token_holder_count: token_holder_count})
else
_ ->
not_found(conn)
end
end
defp fetch_token_counters(token, address_hash, timeout) do
defp fetch_token_counters(address_hash, timeout) do
total_token_transfers_task =
Task.async(fn ->
TokenTransfersCounter.fetch(address_hash)
@ -31,7 +31,7 @@ defmodule BlockScoutWeb.Tokens.TokenController do
total_token_holders_task =
Task.async(fn ->
token.holder_count || TokenHoldersCounter.fetch(address_hash)
TokenHoldersCounter.fetch(address_hash)
end)
[total_token_transfers_task, total_token_holders_task]

@ -59,6 +59,15 @@
) %>
<% end %>
<%= if Enum.any?(@token_balances, fn {token_balance, _, _} -> token_balance.token.type == "ERC-1155" end) do %>
<%= render(
"_tokens.html",
conn: @conn,
token_balances: filter_by_type(@token_balances, "ERC-1155"),
type: "ERC-1155"
) %>
<% end %>
<%= if Enum.any?(@token_balances, fn {token_balance, _, _} -> token_balance.token.type == "ERC-20" end) do %>
<%= render(
"_tokens.html",

@ -10,8 +10,14 @@
data-token-name="<%= token_name(token_balance.token) %>"
data-token-symbol="<%= token_symbol(token_balance.token) %>"
>
<% path = cond do
token_balance.token_type == "ERC-721" && !is_nil(token_balance.token_id) -> token_instance_path(@conn, :show, token_balance.token.contract_address_hash, to_string(token_balance.token_id))
token_balance.token_type == "ERC-1155" && !is_nil(token_balance.token_id) -> token_instance_path(@conn, :show, token_balance.token.contract_address_hash, to_string(token_balance.token_id))
true -> token_path(@conn, :show, to_string(token_balance.token.contract_address_hash))
end
%>
<%= link(
to: token_path(@conn, :show, to_string(token.contract_address_hash)),
to: path,
class: "dropdown-item"
) do %>
<div class="row">
@ -36,7 +42,14 @@
<div class="row">
<% 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.decimals) %> <%= token_symbol(token) %>
<%= if token_balance.token_type == "ERC-721" && !is_nil(token_balance.token_id) do %>
1
<% else %>
<%= format_according_to_decimals(token_balance.value, token_balance.token.decimals) %> <%= token_symbol(token_balance.token) %>
<% end %>
<%= if (token_balance.token_type == "ERC-721" && !is_nil(token_balance.token_id)) or token_balance.token_type == "ERC-1155" do %>
<%= " TokenID " <> to_string(token_balance.token_id) %>
<% end %>
</p>
<%= if token_balance.token.usd_value do %>
<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 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>
<%= if show_total_supply_percentage?(@token.total_supply) do %>

@ -0,0 +1,41 @@
<section class="container">
<%= render(
OverviewView,
"_details.html",
token: @token,
total_token_transfers: @total_token_transfers,
token_id: @token_instance.token_id,
token_instance: @token_instance,
conn: @conn
) %>
<section>
<div class="card">
<%= render OverviewView, "_tabs.html", assigns %>
<div class="card-body" data-async-load data-async-listing="<%= @current_path %>">
<h2 class="card-title list-title-description"><%= gettext "Token Holders" %></h2>
<div class="list-top-pagination-container-wrapper">
<%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %>
</div>
<button data-error-message class="alert alert-danger col-12 text-left" style="display: none;">
<span href="#" class="alert-link"><%= gettext("Something went wrong, click to reload.") %></span>
</button>
<div data-empty-response-message class="tile tile-muted text-center" style="display: none;">
<span data-selector="empty-transactions-list">
<%= gettext "There are no transfers for this Token." %>
</span>
</div>
<div data-items>
<%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %>
</div>
<%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %>
</div>
</div>
</section>
</section>

@ -5,11 +5,18 @@
to: token_instance_path(@conn, :show, @token.contract_address_hash, to_string(@token_instance.token_id))
)
%>
<%= if @token_instance.instance do %>
<%= if @token_instance.instance do %>
<%= link(
gettext("Metadata"),
to: token_instance_metadata_path(@conn, :index, Address.checksum(@token.contract_address_hash), to_string(@token_instance.token_id)),
class: "card-tab #{tab_status("metadata", @conn.request_path)}")
%>
<% end %>
<%= if !Chain.token_id_1155_is_unique?(@token.contract_address_hash, @token_instance.token_id) and @token.type == "ERC-1155" do %>
<%= link(
gettext("Token Holders"),
to: token_instance_holder_path(@conn, :index, Address.checksum(@token.contract_address_hash), to_string(@token_instance.token_id)),
class: "card-tab #{tab_status("token-holders", @conn.request_path)}")
%>
<% end %>
</div>

@ -1,30 +1,46 @@
<% is_1155 = @token.type == "ERC-1155"%>
<% is_unique = Chain.token_id_1155_is_unique?(@token.contract_address_hash, @token_transfer.token_id) or not is_1155%>
<!-- use "tile-type-unique-token-image" to token with images -->
<div class="tile tile-type-unique-token fade-in">
<div class="row">
<div class="pl-5 col-md-2 d-flex flex-column align-items-left justify-content-start justify-content-lg-center">
<!-- substitute the span with <img class="tile-image" /> to token with images -->
<span class="tile-label"><%= gettext "Unique Token" %></span>
<%= if is_unique do%>
<span class="tile-label"><%= gettext "Unique Token" %></span>
<% else %>
<span class="tile-label"><%= gettext "Not unique Token" %></span>
<% end %>
</div>
<div class="tile-content col-md-7 col-lg-8 d-flex flex-column">
<span class="d-flex flex-md-row flex-column mt-3 mt-md-0">
<span class="mr-1"><%= gettext "Token ID" %>:</span>
<span class="tile-title">
<%= link(@token_transfer.token_id, to: token_instance_path(@conn, :show, "#{@token.contract_address_hash}", "#{@token_transfer.token_id}")) %>
<%= if is_unique do %>
<div class="tile-content col-md-7 col-lg-8 d-flex flex-column">
<span class="d-flex flex-md-row flex-column mt-3 mt-md-0">
<span class="mr-1"><%= gettext "Token ID" %>:</span>
<span class="tile-title">
<%= link(@token_transfer.token_id, to: token_instance_path(@conn, :show, "#{@token.contract_address_hash}", "#{@token_transfer.token_id}")) %>
</span>
</span>
</span>
<span class="d-flex flex-md-row flex-column mt-3 mt-md-0">
<span class="mr-1"><%= gettext "Owner Address" %>:</span>
<span class="tile-title">
<%= render BlockScoutWeb.AddressView,
"_link.html",
address: @token_transfer.to_address,
contract: false,
use_custom_tooltip: false %>
<span class="d-flex flex-md-row flex-column mt-3 mt-md-0">
<span class="mr-1"><%= gettext "Owner Address" %>:</span>
<span class="tile-title">
<%= render BlockScoutWeb.AddressView,
"_link.html",
address: @token_transfer.to_address,
contract: false,
use_custom_tooltip: false %>
</span>
</span>
</span>
</div>
</div>
<% else %>
<div class="tile-content col-md-7 col-lg-8 d-flex flex-column justify-content-lg-center">
<span class="d-flex flex-md-row flex-column mt-3 mt-md-0">
<span class="mr-1"><%= gettext "Token ID" %>:</span>
<span class="tile-title">
<%= link(@token_transfer.token_id, to: token_instance_path(@conn, :show, "#{@token.contract_address_hash}", "#{@token_transfer.token_id}")) %>
</span>
</span>
</div>
<% end %>
<div class="pl-5 col-md-2 d-flex flex-column align-items-left justify-content-start justify-content-lg-center">
<!-- substitute the span with <img class="tile-image" /> to token with images -->

@ -3,14 +3,7 @@
<!-- Color Block -->
<div class="tile-transaction-type-block col-md-2 d-flex flex-row flex-md-column">
<span class="tile-label">
<%= cond do %>
<% @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 %>
<%= render(BlockScoutWeb.CommonComponentsView, "_token_transfer_type_display_name.html", type: Chain.get_token_transfer_type(@token_transfer)) %>
</span>
</div>
<!-- Content -->

@ -0,0 +1 @@
<%= "[" %><%= link(short_token_id(@token_id, 30), to: token_instance_path(BlockScoutWeb.Endpoint, :show, @transfer.token.contract_address_hash, to_string(@token_id)), "data-test": "token_link") %><%= "]" %>

@ -0,0 +1 @@
<%= link(token_symbol(@transfer.token), to: token_path(BlockScoutWeb.Endpoint, :show, @transfer.token.contract_address_hash), "data-test": "token_link") %>

@ -1,8 +1,23 @@
<%= case token_transfer_amount(@transfer) do %>
<% {: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") %><%= "]" %>
<%= render BlockScoutWeb.TransactionView, "_transfer_token_with_id.html", transfer: @transfer, token_id: @transfer.token_id %>
<% {:ok, :erc1155_instance, value} -> %>
<% transfer_type = Chain.get_token_transfer_type(@transfer) %>
<%= if transfer_type == :token_spawning do %>
<%= render BlockScoutWeb.TransactionView, "_transfer_token_with_id.html", transfer: @transfer, token_id: @transfer.token_id %>
<% else %>
<%= "#{value} " %>
<%= render BlockScoutWeb.TransactionView, "_transfer_token_with_id.html", transfer: @transfer, token_id: @transfer.token_id %>
<% end %>
<% {:ok, :erc1155_instance, values, token_ids, _decimals} -> %>
<% values_ids = Enum.zip(values, token_ids) %>
<%= for {value, token_id} <- values_ids do %>
<div>
<%= "#{value} "%>
<%= render BlockScoutWeb.TransactionView, "_transfer_token_with_id.html", transfer: @transfer, token_id: token_id %>
</div>
<% end %>
<% {:ok, value} -> %>
<%= value %>
<% end %>
<%= " "%>
<%= link(token_symbol(@transfer.token), to: token_path(BlockScoutWeb.Endpoint, :show, @transfer.token.contract_address_hash), "data-test": "token_link") %>
<%= " " %><%= render BlockScoutWeb.TransactionView, "_link_to_token_symbol.html", transfer: @transfer %>
<% end %>

@ -0,0 +1,2 @@
<%= "TokenID " %><%= render BlockScoutWeb.TransactionView, "_link_to_token_instance.html", transfer: @transfer, token_id: @token_id %>
<%= " " %><%= render BlockScoutWeb.TransactionView, "_link_to_token_symbol.html", transfer: @transfer %>

@ -1,14 +1,7 @@
<div class="tile tile-type-token-transfer fade-in">
<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">
<%= cond do %>
<% @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 %>
<%= render(BlockScoutWeb.CommonComponentsView, "_token_transfer_type_display_name.html", type: Chain.get_token_transfer_type(@token_transfer)) %>
</div>
<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)
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
token_transfer
|> prepare_common_token_transfer()

@ -16,27 +16,39 @@ defmodule BlockScoutWeb.Tokens.Helpers do
When the token's type is ERC-721, the function will return a string with the token_id that
represents the ERC-721 token since this kind of token doesn't have amount and decimals.
"""
def token_transfer_amount(%{token: token, amount: amount, amounts: amounts, token_id: token_id, token_ids: token_ids}) do
do_token_transfer_amount(token, amount, amounts, token_id, token_ids)
end
def token_transfer_amount(%{token: token, amount: amount, token_id: token_id}) do
do_token_transfer_amount(token, amount, token_id)
do_token_transfer_amount(token, amount, nil, token_id, nil)
end
defp do_token_transfer_amount(%Token{type: "ERC-20"}, nil, _token_id) do
defp do_token_transfer_amount(%Token{type: "ERC-20"}, nil, nil, _token_id, _token_ids) do
{:ok, "--"}
end
defp do_token_transfer_amount(%Token{type: "ERC-20", decimals: nil}, amount, _token_id) do
defp do_token_transfer_amount(%Token{type: "ERC-20", decimals: nil}, amount, _amounts, _token_id, _token_ids) do
{:ok, CurrencyHelpers.format_according_to_decimals(amount, Decimal.new(0))}
end
defp do_token_transfer_amount(%Token{type: "ERC-20", decimals: decimals}, amount, _token_id) do
defp do_token_transfer_amount(%Token{type: "ERC-20", decimals: decimals}, amount, _amounts, _token_id, _token_ids) 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, _amounts, _token_id, _token_ids) do
{:ok, :erc721_instance}
end
defp do_token_transfer_amount(_token, _amount, _token_id) do
defp do_token_transfer_amount(%Token{type: "ERC-1155", decimals: decimals}, amount, amounts, _token_id, token_ids) do
if amount do
{:ok, :erc1155_instance, CurrencyHelpers.format_according_to_decimals(amount, decimals)}
else
{:ok, :erc1155_instance, amounts, token_ids, decimals}
end
end
defp do_token_transfer_amount(_token, _amount, _amounts, _token_id, _token_ids) do
nil
end

@ -54,19 +54,23 @@ defmodule BlockScoutWeb.Tokens.HolderView do
## Examples
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"
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
"""
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)
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
end
end

@ -0,0 +1,5 @@
defmodule BlockScoutWeb.Tokens.Instance.HolderView do
use BlockScoutWeb, :view
alias BlockScoutWeb.Tokens.Instance.OverviewView
end

@ -2,6 +2,7 @@ defmodule BlockScoutWeb.Tokens.Instance.OverviewView do
use BlockScoutWeb, :view
alias BlockScoutWeb.CurrencyHelpers
alias Explorer.Chain
alias Explorer.Chain.{Address, SmartContract, Token}
alias Explorer.SmartContract.Helper
alias FileInfo

@ -4,4 +4,5 @@ defmodule BlockScoutWeb.Tokens.InventoryView do
import BlockScoutWeb.Tokens.Instance.OverviewView, only: [media_src: 1, media_type: 1]
alias BlockScoutWeb.Tokens.OverviewView
alias Explorer.Chain
end

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

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

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

@ -184,33 +184,24 @@ defmodule BlockScoutWeb.TransactionView do
end)
new_acc1 =
if token_transfer.token_id do
[new_entry | acc1]
else
if existing_entry do
acc1
|> Enum.map(fn entry ->
if entry.to_address_hash == token_transfer.to_address_hash &&
entry.from_address_hash == token_transfer.from_address_hash &&
entry.token == token_transfer.token do
updated_entry =
if new_entry.amount do
%{
entry
| amount: Decimal.add(new_entry.amount, entry.amount)
}
else
entry
end
updated_entry
else
if existing_entry do
acc1
|> Enum.map(fn entry ->
if entry.to_address_hash == token_transfer.to_address_hash &&
entry.from_address_hash == token_transfer.from_address_hash &&
entry.token == token_transfer.token do
updated_entry = %{
entry
end
end)
else
[new_entry | acc1]
end
| amount: Decimal.add(new_entry.amount, entry.amount)
}
updated_entry
else
entry
end
end)
else
[new_entry | acc1]
end
{new_acc1, acc2}
@ -220,6 +211,7 @@ defmodule BlockScoutWeb.TransactionView do
case type do
:erc20 -> gettext("ERC-20 ")
:erc721 -> gettext("ERC-721 ")
:erc1155 -> gettext("ERC-1155 ")
_ -> ""
end
end

@ -267,6 +267,13 @@ defmodule BlockScoutWeb.WebRouter do
only: [:index],
as: :metadata
)
resources(
"/token-holders",
Tokens.Instance.HolderController,
only: [:index],
as: :holder
)
end
end
@ -318,6 +325,13 @@ defmodule BlockScoutWeb.WebRouter do
only: [:index],
as: :metadata
)
resources(
"/token-holders",
Tokens.Instance.HolderController,
only: [:index],
as: :holder
)
end
end

@ -81,7 +81,7 @@ msgid "%{subnetwork} Staking DApp - BlockScout"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:356
#: lib/block_scout_web/views/transaction_view.ex:348
msgid "(Awaiting internal transactions for status)"
msgstr ""
@ -348,7 +348,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/_link.html.eex:2
#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:28 lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:50
#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:28 lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:43
msgid "Block #%{number}"
msgstr ""
@ -580,7 +580,7 @@ msgid "Compiler version"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:349
#: lib/block_scout_web/views/transaction_view.ex:341
msgid "Confirmed"
msgstr ""
@ -654,12 +654,12 @@ msgid "Contract Address Pending"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:460
#: lib/block_scout_web/views/transaction_view.ex:452
msgid "Contract Call"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:457
#: lib/block_scout_web/views/transaction_view.ex:449
msgid "Contract Creation"
msgstr ""
@ -987,12 +987,12 @@ msgid "EIP-1167"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:221
#: lib/block_scout_web/views/transaction_view.ex:212
msgid "ERC-20 "
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:222
#: lib/block_scout_web/views/transaction_view.ex:213
msgid "ERC-721 "
msgstr ""
@ -1061,12 +1061,12 @@ msgid "Error trying to fetch balances."
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:360
#: lib/block_scout_web/views/transaction_view.ex:352
msgid "Error: %{reason}"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:358
#: lib/block_scout_web/views/transaction_view.ex:350
msgid "Error: (Awaiting internal transactions for reason)"
msgstr ""
@ -1313,7 +1313,7 @@ msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:28
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:21 lib/block_scout_web/templates/transaction/_tabs.html.eex:11
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:6 lib/block_scout_web/views/address_view.ex:346
#: lib/block_scout_web/views/transaction_view.ex:515
#: lib/block_scout_web/views/transaction_view.ex:507
msgid "Internal Transactions"
msgstr ""
@ -1440,7 +1440,7 @@ msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:41
#: lib/block_scout_web/templates/address_logs/index.html.eex:10 lib/block_scout_web/templates/transaction/_tabs.html.eex:17
#: lib/block_scout_web/templates/transaction_log/index.html.eex:8 lib/block_scout_web/views/address_view.ex:357
#: lib/block_scout_web/views/transaction_view.ex:516
#: lib/block_scout_web/views/transaction_view.ex:508
msgid "Logs"
msgstr ""
@ -1481,7 +1481,7 @@ msgid "Max Priority Fee per Gas"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:330
#: lib/block_scout_web/views/transaction_view.ex:322
msgid "Max of"
msgstr ""
@ -1497,7 +1497,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex:18
#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:10 lib/block_scout_web/views/tokens/instance/overview_view.ex:178
#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:10 lib/block_scout_web/views/tokens/instance/overview_view.ex:179
msgid "Metadata"
msgstr ""
@ -1684,7 +1684,7 @@ msgid "Other Explorers"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:18
#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:24
msgid "Owner Address"
msgstr ""
@ -1712,8 +1712,8 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:53
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:184 lib/block_scout_web/views/transaction_view.ex:355
#: lib/block_scout_web/views/transaction_view.ex:389
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:184 lib/block_scout_web/views/transaction_view.ex:347
#: lib/block_scout_web/views/transaction_view.ex:381
msgid "Pending"
msgstr ""
@ -1831,7 +1831,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:24
#: lib/block_scout_web/templates/transaction_raw_trace/index.html.eex:7 lib/block_scout_web/views/transaction_view.ex:517
#: lib/block_scout_web/templates/transaction_raw_trace/index.html.eex:7 lib/block_scout_web/views/transaction_view.ex:509
msgid "Raw Trace"
msgstr ""
@ -2037,9 +2037,9 @@ msgstr ""
#: lib/block_scout_web/templates/address_transaction/index.html.eex:54 lib/block_scout_web/templates/address_validation/index.html.eex:24
#: lib/block_scout_web/templates/block_transaction/index.html.eex:22 lib/block_scout_web/templates/chain/show.html.eex:180
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:22 lib/block_scout_web/templates/stakes/_table.html.eex:49
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:27 lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:23
#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:22 lib/block_scout_web/templates/tokens/transfer/index.html.eex:21
#: lib/block_scout_web/templates/transaction/index.html.eex:29
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:27 lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:23
#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:23 lib/block_scout_web/templates/tokens/inventory/index.html.eex:22
#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:21 lib/block_scout_web/templates/transaction/index.html.eex:29
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:13 lib/block_scout_web/templates/transaction_log/index.html.eex:15
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:14
msgid "Something went wrong, click to reload."
@ -2144,7 +2144,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:8
#: lib/block_scout_web/views/transaction_view.ex:357
#: lib/block_scout_web/views/transaction_view.ex:349
msgid "Success"
msgstr ""
@ -2365,8 +2365,8 @@ msgid "There are no transactions."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:28
#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:26
#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:28
#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:28 lib/block_scout_web/templates/tokens/transfer/index.html.eex:26
msgid "There are no transfers for this Token."
msgstr ""
@ -2477,13 +2477,12 @@ msgid "Token"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:8
#: lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex:6 lib/block_scout_web/views/transaction_view.ex:451
#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:3 lib/block_scout_web/views/transaction_view.ex:443
msgid "Token Burning"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:452
#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:7 lib/block_scout_web/views/transaction_view.ex:444
msgid "Token Creation"
msgstr ""
@ -2495,25 +2494,25 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:20
#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:16 lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:17
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:9 lib/block_scout_web/views/tokens/overview_view.ex:42
msgid "Token Holders"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:38
#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:11
#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:18 lib/block_scout_web/templates/tokens/inventory/_token.html.eex:37
msgid "Token ID"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:10
#: lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex:8 lib/block_scout_web/views/transaction_view.ex:450
#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:5 lib/block_scout_web/views/transaction_view.ex:442
msgid "Token Minting"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:12
#: lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex:10 lib/block_scout_web/views/transaction_view.ex:453
#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:9
#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:11 lib/block_scout_web/views/transaction_view.ex:445
msgid "Token Transfer"
msgstr ""
@ -2523,8 +2522,8 @@ msgstr ""
#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:16 lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:3
#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:14 lib/block_scout_web/templates/transaction/_tabs.html.eex:4
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:7 lib/block_scout_web/views/address_view.ex:348
#: lib/block_scout_web/views/tokens/instance/overview_view.ex:177 lib/block_scout_web/views/tokens/overview_view.ex:41
#: lib/block_scout_web/views/transaction_view.ex:514
#: lib/block_scout_web/views/tokens/instance/overview_view.ex:178 lib/block_scout_web/views/tokens/overview_view.ex:41
#: lib/block_scout_web/views/transaction_view.ex:506
msgid "Token Transfers"
msgstr ""
@ -2620,7 +2619,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_logs/_logs.html.eex:19
#: lib/block_scout_web/views/transaction_view.ex:463
#: lib/block_scout_web/views/transaction_view.ex:455
msgid "Transaction"
msgstr ""
@ -2750,12 +2749,12 @@ msgid "Uncles"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:348
#: lib/block_scout_web/views/transaction_view.ex:340
msgid "Unconfirmed"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:6
#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:9
msgid "Unique Token"
msgstr ""
@ -3191,3 +3190,13 @@ msgstr ""
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:18
msgid "validator"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:214
msgid "ERC-1155 "
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:11
msgid "Not unique Token"
msgstr ""

@ -81,7 +81,7 @@ msgid "%{subnetwork} Staking DApp - BlockScout"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:356
#: lib/block_scout_web/views/transaction_view.ex:348
msgid "(Awaiting internal transactions for status)"
msgstr ""
@ -348,7 +348,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/_link.html.eex:2
#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:28 lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:50
#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:28 lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:43
msgid "Block #%{number}"
msgstr ""
@ -580,7 +580,7 @@ msgid "Compiler version"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:349
#: lib/block_scout_web/views/transaction_view.ex:341
msgid "Confirmed"
msgstr ""
@ -654,12 +654,12 @@ msgid "Contract Address Pending"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:460
#: lib/block_scout_web/views/transaction_view.ex:452
msgid "Contract Call"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:457
#: lib/block_scout_web/views/transaction_view.ex:449
msgid "Contract Creation"
msgstr ""
@ -987,12 +987,12 @@ msgid "EIP-1167"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:221
#: lib/block_scout_web/views/transaction_view.ex:212
msgid "ERC-20 "
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:222
#: lib/block_scout_web/views/transaction_view.ex:213
msgid "ERC-721 "
msgstr ""
@ -1061,12 +1061,12 @@ msgid "Error trying to fetch balances."
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:360
#: lib/block_scout_web/views/transaction_view.ex:352
msgid "Error: %{reason}"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:358
#: lib/block_scout_web/views/transaction_view.ex:350
msgid "Error: (Awaiting internal transactions for reason)"
msgstr ""
@ -1313,7 +1313,7 @@ msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:28
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:21 lib/block_scout_web/templates/transaction/_tabs.html.eex:11
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:6 lib/block_scout_web/views/address_view.ex:346
#: lib/block_scout_web/views/transaction_view.ex:515
#: lib/block_scout_web/views/transaction_view.ex:507
msgid "Internal Transactions"
msgstr ""
@ -1440,7 +1440,7 @@ msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:41
#: lib/block_scout_web/templates/address_logs/index.html.eex:10 lib/block_scout_web/templates/transaction/_tabs.html.eex:17
#: lib/block_scout_web/templates/transaction_log/index.html.eex:8 lib/block_scout_web/views/address_view.ex:357
#: lib/block_scout_web/views/transaction_view.ex:516
#: lib/block_scout_web/views/transaction_view.ex:508
msgid "Logs"
msgstr ""
@ -1481,7 +1481,7 @@ msgid "Max Priority Fee per Gas"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:330
#: lib/block_scout_web/views/transaction_view.ex:322
msgid "Max of"
msgstr ""
@ -1497,7 +1497,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex:18
#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:10 lib/block_scout_web/views/tokens/instance/overview_view.ex:178
#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:10 lib/block_scout_web/views/tokens/instance/overview_view.ex:179
msgid "Metadata"
msgstr ""
@ -1684,7 +1684,7 @@ msgid "Other Explorers"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:18
#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:24
msgid "Owner Address"
msgstr ""
@ -1712,8 +1712,8 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:53
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:184 lib/block_scout_web/views/transaction_view.ex:355
#: lib/block_scout_web/views/transaction_view.ex:389
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:184 lib/block_scout_web/views/transaction_view.ex:347
#: lib/block_scout_web/views/transaction_view.ex:381
msgid "Pending"
msgstr ""
@ -1831,7 +1831,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:24
#: lib/block_scout_web/templates/transaction_raw_trace/index.html.eex:7 lib/block_scout_web/views/transaction_view.ex:517
#: lib/block_scout_web/templates/transaction_raw_trace/index.html.eex:7 lib/block_scout_web/views/transaction_view.ex:509
msgid "Raw Trace"
msgstr ""
@ -2037,9 +2037,9 @@ msgstr ""
#: lib/block_scout_web/templates/address_transaction/index.html.eex:54 lib/block_scout_web/templates/address_validation/index.html.eex:24
#: lib/block_scout_web/templates/block_transaction/index.html.eex:22 lib/block_scout_web/templates/chain/show.html.eex:180
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:22 lib/block_scout_web/templates/stakes/_table.html.eex:49
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:27 lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:23
#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:22 lib/block_scout_web/templates/tokens/transfer/index.html.eex:21
#: lib/block_scout_web/templates/transaction/index.html.eex:29
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:27 lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:23
#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:23 lib/block_scout_web/templates/tokens/inventory/index.html.eex:22
#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:21 lib/block_scout_web/templates/transaction/index.html.eex:29
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:13 lib/block_scout_web/templates/transaction_log/index.html.eex:15
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:14
msgid "Something went wrong, click to reload."
@ -2144,7 +2144,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:8
#: lib/block_scout_web/views/transaction_view.ex:357
#: lib/block_scout_web/views/transaction_view.ex:349
msgid "Success"
msgstr ""
@ -2365,8 +2365,8 @@ msgid "There are no transactions."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:28
#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:26
#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:28
#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:28 lib/block_scout_web/templates/tokens/transfer/index.html.eex:26
msgid "There are no transfers for this Token."
msgstr ""
@ -2477,13 +2477,12 @@ msgid "Token"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:8
#: lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex:6 lib/block_scout_web/views/transaction_view.ex:451
#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:3 lib/block_scout_web/views/transaction_view.ex:443
msgid "Token Burning"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:452
#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:7 lib/block_scout_web/views/transaction_view.ex:444
msgid "Token Creation"
msgstr ""
@ -2495,25 +2494,25 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:20
#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:16 lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:17
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:9 lib/block_scout_web/views/tokens/overview_view.ex:42
msgid "Token Holders"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:38
#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:11
#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:18 lib/block_scout_web/templates/tokens/inventory/_token.html.eex:37
msgid "Token ID"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:10
#: lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex:8 lib/block_scout_web/views/transaction_view.ex:450
#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:5 lib/block_scout_web/views/transaction_view.ex:442
msgid "Token Minting"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:12
#: lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex:10 lib/block_scout_web/views/transaction_view.ex:453
#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:9
#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:11 lib/block_scout_web/views/transaction_view.ex:445
msgid "Token Transfer"
msgstr ""
@ -2523,8 +2522,8 @@ msgstr ""
#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:16 lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:3
#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:14 lib/block_scout_web/templates/transaction/_tabs.html.eex:4
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:7 lib/block_scout_web/views/address_view.ex:348
#: lib/block_scout_web/views/tokens/instance/overview_view.ex:177 lib/block_scout_web/views/tokens/overview_view.ex:41
#: lib/block_scout_web/views/transaction_view.ex:514
#: lib/block_scout_web/views/tokens/instance/overview_view.ex:178 lib/block_scout_web/views/tokens/overview_view.ex:41
#: lib/block_scout_web/views/transaction_view.ex:506
msgid "Token Transfers"
msgstr ""
@ -2620,7 +2619,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_logs/_logs.html.eex:19
#: lib/block_scout_web/views/transaction_view.ex:463
#: lib/block_scout_web/views/transaction_view.ex:455
msgid "Transaction"
msgstr ""
@ -2750,12 +2749,12 @@ msgid "Uncles"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:348
#: lib/block_scout_web/views/transaction_view.ex:340
msgid "Unconfirmed"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:6
#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:9
msgid "Unique Token"
msgstr ""
@ -3191,3 +3190,13 @@ msgstr ""
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:18
msgid "validator"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:214
msgid "ERC-1155 "
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:11
msgid "Not unique Token"
msgstr ""

@ -56,19 +56,19 @@ defmodule BlockScoutWeb.Tokens.HolderViewTest do
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
token = build(:token, type: "ERC-20", decimals: Decimal.new(2))
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
test "returns the value when it's ERC-721" do
token = build(:token, type: "ERC-721")
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

@ -168,7 +168,11 @@ defmodule EthereumJSONRPC do
"""
@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
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
@doc """

@ -4539,7 +4539,7 @@ defmodule Explorer.Chain do
nft_tokens =
from(
token in Token,
where: token.type == ^"ERC-721",
where: token.type == ^"ERC-721" or token.type == ^"ERC-1155",
select: token.contract_address_hash
)
@ -5693,6 +5693,13 @@ defmodule Explorer.Chain do
|> Repo.one() || Decimal.new(0)
end
# @spec fetch_last_token_balance_1155(Hash.Address.t(), Hash.Address.t()) :: Decimal.t()
def fetch_last_token_balance_1155(address_hash, token_contract_address_hash, token_id) do
address_hash
|> CurrentTokenBalance.last_token_balance_1155(token_contract_address_hash, token_id)
|> Repo.one() || Decimal.new(0)
end
@spec address_to_coin_balances(Hash.Address.t(), [paging_options]) :: []
def address_to_coin_balances(address_hash, options) do
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
@ -5813,9 +5820,36 @@ defmodule Explorer.Chain do
|> Repo.all()
end
def fetch_token_holders_from_token_hash_and_token_id(contract_address_hash, token_id, options \\ []) do
contract_address_hash
|> CurrentTokenBalance.token_holders_1155_by_token_id(token_id, options)
|> Repo.all()
end
def token_id_1155_is_unique?(_, nil), do: false
def token_id_1155_is_unique?(contract_address_hash, token_id) do
result = contract_address_hash |> CurrentTokenBalance.token_balances_by_id_limit_2(token_id) |> Repo.all()
if length(result) == 1 do
Decimal.cmp(Enum.at(result, 0), 1) == :eq
else
false
end
end
def get_token_ids_1155(contract_address_hash) do
contract_address_hash
|> CurrentTokenBalance.token_ids_query()
|> Repo.all()
end
@spec count_token_holders_from_token_hash(Hash.Address.t()) :: non_neg_integer()
def count_token_holders_from_token_hash(contract_address_hash) do
query = from(ctb in CurrentTokenBalance.token_holders_query(contract_address_hash), select: fragment("COUNT(*)"))
query =
from(ctb in CurrentTokenBalance.token_holders_query_for_count(contract_address_hash),
select: fragment("COUNT(DISTINCT(address_hash))")
)
Repo.one!(query, timeout: :infinity)
end
@ -5853,7 +5887,7 @@ defmodule Explorer.Chain do
end
@spec transaction_token_transfer_type(Transaction.t()) ::
:erc20 | :erc721 | :token_transfer | nil
:erc20 | :erc721 | :erc1155 | :token_transfer | nil
def transaction_token_transfer_type(
%Transaction{
status: :ok,
@ -5900,10 +5934,24 @@ defmodule Explorer.Chain do
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} ->
: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} ->
types = [:address, {:uint, 256}]
@ -5911,7 +5959,7 @@ defmodule Explorer.Chain do
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
@ -5927,7 +5975,16 @@ defmodule Explorer.Chain do
if token_transfer, do: :erc721
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 =
Enum.find(token_transfers, fn token_transfer ->
token_transfer.to_address_hash.bytes == address && token_transfer.amount == decimal_value
@ -5937,6 +5994,7 @@ defmodule Explorer.Chain do
case token_transfer.token do
%Token{type: "ERC-20"} -> :erc20
%Token{type: "ERC-721"} -> :erc721
%Token{type: "ERC-1155"} -> :erc1155
_ -> nil
end
else

@ -23,6 +23,8 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do
* `token_contract_address_hash` - The contract address hash foreign key.
* `block_number` - The block's number that the transfer took place.
* `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__{
address: %Ecto.Association.NotLoaded{} | Address.t(),
@ -30,15 +32,21 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do
token: %Ecto.Association.NotLoaded{} | Token.t(),
token_contract_address_hash: Hash.Address,
block_number: Block.block_number(),
max_block_number: Block.block_number(),
inserted_at: DateTime.t(),
updated_at: DateTime.t(),
value: Decimal.t() | nil
value: Decimal.t() | nil,
token_id: non_neg_integer() | nil,
token_type: String.t()
}
schema "address_current_token_balances" do
field(:value, :decimal)
field(:block_number, :integer)
field(:max_block_number, :integer, virtual: true)
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
field(:old_value, :decimal)
@ -56,8 +64,8 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do
timestamps()
end
@optional_fields ~w(value value_fetched_at)a
@required_fields ~w(address_hash block_number token_contract_address_hash)a
@optional_fields ~w(value value_fetched_at token_id)a
@required_fields ~w(address_hash block_number token_contract_address_hash token_type)a
@allowed_fields @optional_fields ++ @required_fields
@doc false
@ -96,6 +104,57 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do
|> offset(^offset)
end
@doc """
Builds an `Ecto.Query` to fetch the token holders from the given token contract address hash and token_id.
The Token Holders are the addresses that own a positive amount of the Token. So this query is
considering the following conditions:
* The token balance from the last block.
* Balances greater than 0.
* Excluding the burn address (0x0000000000000000000000000000000000000000).
"""
def token_holders_1155_by_token_id(token_contract_address_hash, token_id, options \\ []) do
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
offset = (max(paging_options.page_number, 1) - 1) * paging_options.page_size
token_contract_address_hash
|> token_holders_by_token_id_query(token_id)
|> preload(:address)
|> order_by([tb], desc: :value, desc: :address_hash)
|> Chain.page_token_balances(paging_options)
|> limit(^paging_options.page_size)
|> offset(^offset)
end
@doc """
Builds an `Ecto.Query` to fetch all available token_ids
"""
def token_ids_query(token_contract_address_hash) do
from(
ctb in __MODULE__,
where: ctb.token_contract_address_hash == ^token_contract_address_hash,
where: ctb.address_hash != ^@burn_address_hash,
where: ctb.value > 0,
select: ctb.token_id,
distinct: ctb.token_id
)
end
@doc """
Builds an `Ecto.Query` to fetch all token holders, to count it
Used in `Explorer.Chain.count_token_holders_from_token_hash/1`
"""
def token_holders_query_for_count(token_contract_address_hash) do
from(
ctb in __MODULE__,
where: ctb.token_contract_address_hash == ^token_contract_address_hash,
where: ctb.address_hash != ^@burn_address_hash,
where: ctb.value > 0
)
end
@doc """
Builds an `t:Ecto.Query.t/0` to fetch the current token balances of the given address.
"""
@ -137,6 +196,48 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do
)
end
@doc """
Builds an `t:Ecto.Query.t/0` to fetch the current balance of the given address for the given token and token_id
"""
def last_token_balance_1155(address_hash, token_contract_address_hash, token_id) do
from(
ctb in __MODULE__,
where: ctb.token_contract_address_hash == ^token_contract_address_hash,
where: ctb.address_hash == ^address_hash,
where: ctb.token_id == ^token_id,
select: ctb.value
)
end
@doc """
Builds an `t:Ecto.Query.t/0` to check if the token_id corresponds to the unique token or not.
Used in `Explorer.Chain.token_id_1155_is_unique?/2`
"""
def token_balances_by_id_limit_2(token_contract_address_hash, token_id) do
from(
ctb in __MODULE__,
where: ctb.token_contract_address_hash == ^token_contract_address_hash,
where: ctb.token_id == ^token_id,
where: ctb.address_hash != ^@burn_address_hash,
where: ctb.value > 0,
select: ctb.value,
limit: 2
)
end
@doc """
Builds an `t:Ecto.Query.t/0` to fetch holders of the particular token_id in ERC-1155
"""
def token_holders_by_token_id_query(token_contract_address_hash, token_id) do
from(
ctb in __MODULE__,
where: ctb.token_contract_address_hash == ^token_contract_address_hash,
where: ctb.address_hash != ^@burn_address_hash,
where: ctb.value > 0,
where: ctb.token_id == ^token_id
)
end
@doc """
Builds an `t:Ecto.Query.t/0` to fetch addresses that hold the token.

@ -34,17 +34,16 @@ defmodule Explorer.Chain.Address.Token do
address_hash
|> join_with_last_balance()
|> order_filter_and_group()
|> filter_and_group()
|> order()
|> page_tokens(paging_options)
|> limit(^paging_options.page_size)
end
defp order_filter_and_group(query) do
defp filter_and_group(query) do
from(
[token, balance] in query,
order_by: fragment("? DESC, LOWER(?) ASC NULLS LAST", token.type, token.name),
where: balance.value > 0,
group_by: [token.name, token.symbol, balance.value, token.type, token.contract_address_hash],
select: %Address.Token{
contract_address_hash: token.contract_address_hash,
inserted_at: max(token.inserted_at),
@ -53,22 +52,40 @@ defmodule Explorer.Chain.Address.Token do
balance: balance.value,
decimals: max(token.decimals),
type: token.type
}
},
group_by: [token.name, token.symbol, balance.value, token.type, token.contract_address_hash, balance.block_number]
)
end
defp order(query) do
from(
token in subquery(query),
order_by: fragment("? DESC, ? ASC NULLS LAST", token.type, token.name)
)
end
defp join_with_last_balance(address_hash) do
last_balance_query =
from(
tb in CurrentTokenBalance,
where: tb.address_hash == ^address_hash,
select: %{value: tb.value, token_contract_address_hash: tb.token_contract_address_hash}
ctb in CurrentTokenBalance,
where: ctb.address_hash == ^address_hash,
select: %{
value: ctb.value,
token_contract_address_hash: ctb.token_contract_address_hash,
block_number: ctb.block_number,
max_block_number: over(max(ctb.block_number), :w)
},
windows: [
w: [partition_by: [ctb.token_contract_address_hash, ctb.address_hash]]
]
)
from(
t in Chain.Token,
join: tb in subquery(last_balance_query),
on: tb.token_contract_address_hash == t.contract_address_hash
on: tb.token_contract_address_hash == t.contract_address_hash,
where: tb.block_number == tb.max_block_number,
distinct: t.contract_address_hash
)
end

@ -23,6 +23,8 @@ defmodule Explorer.Chain.Address.TokenBalance do
* `token_contract_address_hash` - The contract address hash foreign key.
* `block_number` - The block's number that the transfer took place.
* `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__{
address: %Ecto.Association.NotLoaded{} | Address.t(),
@ -32,13 +34,17 @@ defmodule Explorer.Chain.Address.TokenBalance do
block_number: Block.block_number(),
inserted_at: DateTime.t(),
updated_at: DateTime.t(),
value: Decimal.t() | nil
value: Decimal.t() | nil,
token_id: non_neg_integer() | nil,
token_type: String.t()
}
schema "address_token_balances" do
field(:value, :decimal)
field(:block_number, :integer)
field(:value_fetched_at, :utc_datetime_usec)
field(:token_id, :decimal)
field(:token_type, :string)
belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Address)
@ -53,8 +59,8 @@ defmodule Explorer.Chain.Address.TokenBalance do
timestamps()
end
@optional_fields ~w(value value_fetched_at)a
@required_fields ~w(address_hash block_number token_contract_address_hash)a
@optional_fields ~w(value value_fetched_at token_id)a
@required_fields ~w(address_hash block_number token_contract_address_hash token_type)a
@allowed_fields @optional_fields ++ @required_fields
@doc false
@ -82,8 +88,9 @@ defmodule Explorer.Chain.Address.TokenBalance do
tb in TokenBalance,
join: t in Token,
on: tb.token_contract_address_hash == t.contract_address_hash,
where: is_nil(tb.value_fetched_at) or is_nil(tb.value),
where: (tb.address_hash != ^@burn_address_hash and t.type != "ERC-721") or t.type == "ERC-20"
where:
((tb.address_hash != ^@burn_address_hash and t.type != "ERC-721") or t.type == "ERC-20" or t.type == "ERC-1155") and
(is_nil(tb.value_fetched_at) or is_nil(tb.value))
)
end
end

@ -109,8 +109,16 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do
# Enforce ShareLocks tables order (see docs: sharelocks.md)
multi
|> Multi.run(:acquire_contract_address_tokens, fn repo, _ ->
contract_address_hashes = changes_list |> Enum.map(& &1.token_contract_address_hash) |> Enum.uniq()
Tokens.acquire_contract_address_tokens(repo, contract_address_hashes)
token_contract_address_hashes_and_ids =
changes_list
|> Enum.map(fn change ->
token_id = get_tokend_id(change)
{change.token_contract_address_hash, token_id}
end)
|> Enum.uniq()
Tokens.acquire_contract_address_tokens(repo, token_contract_address_hashes_and_ids)
end)
|> Multi.run(:address_current_token_balances, fn repo, _ ->
insert(repo, changes_list, insert_options)
@ -131,6 +139,10 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do
end)
end
defp get_tokend_id(change) do
if Map.has_key?(change, :token_id), do: change.token_id, else: nil
end
@impl Import.Runner
def timeout, do: @timeout
@ -198,21 +210,107 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do
| {:error, [Changeset.t()]}
defp insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options)
when is_atom(repo) and is_list(changes_list) do
inserted_changes_list =
insert_changes_list_with_and_without_token_id(changes_list, repo, timestamps, timeout, options)
{:ok, inserted_changes_list}
end
def insert_changes_list_with_and_without_token_id(changes_list, repo, timestamps, timeout, options) do
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0)
# Enforce CurrentTokenBalance ShareLocks order (see docs: sharelocks.md)
ordered_changes_list = Enum.sort_by(changes_list, &{&1.token_contract_address_hash, &1.address_hash})
Import.insert_changes_list(
repo,
ordered_changes_list,
conflict_target: ~w(address_hash token_contract_address_hash)a,
on_conflict: on_conflict,
for: CurrentTokenBalance,
returning: true,
timeout: timeout,
timestamps: timestamps
)
%{
changes_list_no_token_id: changes_list_no_token_id,
changes_list_with_token_id: changes_list_with_token_id
} =
changes_list
|> Enum.reduce(%{changes_list_no_token_id: [], changes_list_with_token_id: []}, fn change, acc ->
updated_change =
if Map.has_key?(change, :token_id) and Map.get(change, :token_type) == "ERC-1155" do
change
else
Map.put(change, :token_id, nil)
end
if updated_change.token_id do
changes_list_with_token_id = [updated_change | acc.changes_list_with_token_id]
%{
changes_list_no_token_id: acc.changes_list_no_token_id,
changes_list_with_token_id: changes_list_with_token_id
}
else
changes_list_no_token_id = [updated_change | acc.changes_list_no_token_id]
%{
changes_list_no_token_id: changes_list_no_token_id,
changes_list_with_token_id: acc.changes_list_with_token_id
}
end
end)
ordered_changes_list_no_token_id =
changes_list_no_token_id
|> 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})
ordered_changes_list_with_token_id =
changes_list_with_token_id
|> Enum.group_by(fn %{
address_hash: address_hash,
token_contract_address_hash: token_contract_address_hash,
token_id: token_id
} ->
{address_hash, token_contract_address_hash, token_id}
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.token_id, &1.address_hash})
{:ok, inserted_changes_list_no_token_id} =
if Enum.count(ordered_changes_list_no_token_id) > 0 do
Import.insert_changes_list(
repo,
ordered_changes_list_no_token_id,
conflict_target: {:unsafe_fragment, ~s<(address_hash, token_contract_address_hash) WHERE token_id IS NULL>},
on_conflict: on_conflict,
for: CurrentTokenBalance,
returning: true,
timeout: timeout,
timestamps: timestamps
)
else
{:ok, []}
end
{:ok, inserted_changes_list_with_token_id} =
if Enum.count(ordered_changes_list_with_token_id) > 0 do
Import.insert_changes_list(
repo,
ordered_changes_list_with_token_id,
conflict_target:
{:unsafe_fragment, ~s<(address_hash, token_contract_address_hash, token_id) WHERE token_id IS NOT NULL>},
on_conflict: on_conflict,
for: CurrentTokenBalance,
returning: true,
timeout: timeout,
timestamps: timestamps
)
else
{:ok, []}
end
inserted_changes_list_no_token_id ++ inserted_changes_list_with_token_id
end
defp default_on_conflict do

@ -60,20 +60,113 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalances do
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0)
# Enforce TokenBalance ShareLocks order (see docs: sharelocks.md)
ordered_changes_list =
Enum.sort_by(changes_list, &{&1.token_contract_address_hash, &1.address_hash, &1.block_number})
{:ok, _} =
Import.insert_changes_list(
repo,
ordered_changes_list,
conflict_target: ~w(address_hash token_contract_address_hash block_number)a,
on_conflict: on_conflict,
for: TokenBalance,
returning: true,
timeout: timeout,
timestamps: timestamps
)
%{
changes_list_no_token_id: changes_list_no_token_id,
changes_list_with_token_id: changes_list_with_token_id
} =
changes_list
|> Enum.reduce(%{changes_list_no_token_id: [], changes_list_with_token_id: []}, fn change, acc ->
updated_change =
if Map.has_key?(change, :token_id) and Map.get(change, :token_type) == "ERC-1155" do
change
else
Map.put(change, :token_id, nil)
end
if updated_change.token_id do
changes_list_with_token_id = [updated_change | acc.changes_list_with_token_id]
%{
changes_list_no_token_id: acc.changes_list_no_token_id,
changes_list_with_token_id: changes_list_with_token_id
}
else
changes_list_no_token_id = [updated_change | acc.changes_list_no_token_id]
%{
changes_list_no_token_id: changes_list_no_token_id,
changes_list_with_token_id: acc.changes_list_with_token_id
}
end
end)
ordered_changes_list_no_token_id =
changes_list_no_token_id
|> Enum.group_by(fn %{
address_hash: address_hash,
token_contract_address_hash: token_contract_address_hash,
block_number: block_number
} ->
{token_contract_address_hash, address_hash, block_number}
end)
|> Enum.map(fn {_, grouped_address_token_balances} ->
dedup = Enum.dedup(grouped_address_token_balances)
if Enum.count(dedup) > 1 do
Enum.max_by(dedup, fn %{value_fetched_at: value_fetched_at} -> value_fetched_at end)
else
Enum.at(dedup, 0)
end
end)
|> Enum.sort_by(&{&1.token_contract_address_hash, &1.address_hash, &1.block_number})
ordered_changes_list_with_token_id =
changes_list_with_token_id
|> Enum.group_by(fn %{
address_hash: address_hash,
token_contract_address_hash: token_contract_address_hash,
token_id: token_id,
block_number: block_number
} ->
{token_contract_address_hash, token_id, address_hash, block_number}
end)
|> Enum.map(fn {_, grouped_address_token_balances} ->
if Enum.count(grouped_address_token_balances) > 1 do
Enum.max_by(grouped_address_token_balances, fn %{value_fetched_at: value_fetched_at} -> value_fetched_at end)
else
Enum.at(grouped_address_token_balances, 0)
end
end)
|> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id, &1.address_hash, &1.block_number})
{:ok, inserted_changes_list_no_token_id} =
if Enum.count(ordered_changes_list_no_token_id) > 0 do
Import.insert_changes_list(
repo,
ordered_changes_list_no_token_id,
conflict_target:
{:unsafe_fragment, ~s<(address_hash, token_contract_address_hash, block_number) WHERE token_id IS NULL>},
on_conflict: on_conflict,
for: TokenBalance,
returning: true,
timeout: timeout,
timestamps: timestamps
)
else
{:ok, []}
end
{:ok, inserted_changes_list_with_token_id} =
if Enum.count(ordered_changes_list_with_token_id) > 0 do
Import.insert_changes_list(
repo,
ordered_changes_list_with_token_id,
conflict_target:
{:unsafe_fragment,
~s<(address_hash, token_contract_address_hash, token_id, block_number) WHERE token_id IS NOT NULL>},
on_conflict: on_conflict,
for: TokenBalance,
returning: true,
timeout: timeout,
timestamps: timestamps
)
else
{:ok, []}
end
inserted_changes_list = inserted_changes_list_no_token_id ++ inserted_changes_list_with_token_id
{:ok, inserted_changes_list}
end
defp default_on_conflict do

@ -119,13 +119,13 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
query =
from(ctb in Address.CurrentTokenBalance,
where: ctb.block_number in ^consensus_block_numbers,
select: ctb.token_contract_address_hash,
distinct: ctb.token_contract_address_hash
select: {ctb.token_contract_address_hash, ctb.token_id},
distinct: [ctb.token_contract_address_hash, ctb.token_id]
)
contract_address_hashes = repo.all(query)
contract_address_hashes_and_token_ids = repo.all(query)
Tokens.acquire_contract_address_tokens(repo, contract_address_hashes)
Tokens.acquire_contract_address_tokens(repo, contract_address_hashes_and_token_ids)
end
defp fork_transactions(%{
@ -341,10 +341,11 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
ordered_query =
from(tb in Address.TokenBalance,
where: tb.block_number in ^consensus_block_numbers,
select: map(tb, [:address_hash, :token_contract_address_hash, :block_number]),
select: map(tb, [:address_hash, :token_contract_address_hash, :token_id, :block_number]),
# Enforce TokenBalance ShareLocks order (see docs: sharelocks.md)
order_by: [
tb.token_contract_address_hash,
tb.token_id,
tb.address_hash,
tb.block_number
],
@ -359,6 +360,9 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
ordered_address_token_balance.address_hash == tb.address_hash and
ordered_address_token_balance.token_contract_address_hash ==
tb.token_contract_address_hash and
((is_nil(ordered_address_token_balance.token_id) and is_nil(tb.token_id)) or
(ordered_address_token_balance.token_id == tb.token_id and
not is_nil(ordered_address_token_balance.token_id) and not is_nil(tb.token_id))) and
ordered_address_token_balance.block_number == tb.block_number
)
@ -378,10 +382,11 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
ordered_query =
from(ctb in Address.CurrentTokenBalance,
where: ctb.block_number in ^consensus_block_numbers,
select: map(ctb, [:address_hash, :token_contract_address_hash]),
select: map(ctb, [:address_hash, :token_contract_address_hash, :token_id]),
# Enforce CurrentTokenBalance ShareLocks order (see docs: sharelocks.md)
order_by: [
ctb.token_contract_address_hash,
ctb.token_id,
ctb.address_hash
],
lock: "FOR UPDATE"
@ -393,6 +398,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
map(ctb, [
:address_hash,
:token_contract_address_hash,
:token_id,
# Used to determine if `address_hash` was a holder of `token_contract_address_hash` before
# `address_current_token_balance` is deleted in `update_tokens_holder_count`.
@ -401,8 +407,10 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
inner_join: ordered_address_current_token_balance in subquery(ordered_query),
on:
ordered_address_current_token_balance.address_hash == ctb.address_hash and
ordered_address_current_token_balance.token_contract_address_hash ==
ctb.token_contract_address_hash
ordered_address_current_token_balance.token_contract_address_hash == ctb.token_contract_address_hash and
((is_nil(ordered_address_current_token_balance.token_id) and is_nil(ctb.token_id)) or
(ordered_address_current_token_balance.token_id == ctb.token_id and
not is_nil(ordered_address_current_token_balance.token_id) and not is_nil(ctb.token_id)))
)
try do
@ -417,31 +425,13 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
defp derive_address_current_token_balances(_, [], _), do: {:ok, []}
defp derive_address_current_token_balances(repo, deleted_address_current_token_balances, %{timeout: timeout})
defp derive_address_current_token_balances(
repo,
deleted_address_current_token_balances,
%{timeout: timeout} = options
)
when is_list(deleted_address_current_token_balances) do
initial_query =
from(tb in Address.TokenBalance,
select: %{
address_hash: tb.address_hash,
token_contract_address_hash: tb.token_contract_address_hash,
block_number: max(tb.block_number)
},
group_by: [tb.address_hash, tb.token_contract_address_hash]
)
final_query =
Enum.reduce(deleted_address_current_token_balances, initial_query, fn %{
address_hash: address_hash,
token_contract_address_hash:
token_contract_address_hash
},
acc_query ->
from(tb in acc_query,
or_where:
tb.address_hash == ^address_hash and
tb.token_contract_address_hash == ^token_contract_address_hash
)
end)
final_query = derive_address_current_token_balances_grouped_query(deleted_address_current_token_balances)
new_current_token_balance_query =
from(new_current_token_balance in subquery(final_query),
@ -449,42 +439,82 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
on:
tb.address_hash == new_current_token_balance.address_hash and
tb.token_contract_address_hash == new_current_token_balance.token_contract_address_hash and
((is_nil(tb.token_id) and is_nil(new_current_token_balance.token_id)) or
(tb.token_id == new_current_token_balance.token_id and
not is_nil(tb.token_id) and not is_nil(new_current_token_balance.token_id))) and
tb.block_number == new_current_token_balance.block_number,
select: %{
address_hash: new_current_token_balance.address_hash,
token_contract_address_hash: new_current_token_balance.token_contract_address_hash,
token_id: new_current_token_balance.token_id,
block_number: new_current_token_balance.block_number,
value: tb.value,
inserted_at: over(min(tb.inserted_at), :w),
updated_at: over(max(tb.updated_at), :w)
},
windows: [
w: [partition_by: [tb.address_hash, tb.token_contract_address_hash]]
w: [partition_by: [tb.address_hash, tb.token_contract_address_hash, tb.token_id]]
]
)
ordered_current_token_balance =
current_token_balance =
new_current_token_balance_query
|> repo.all()
# Enforce CurrentTokenBalance ShareLocks order (see docs: sharelocks.md)
|> Enum.sort_by(&{&1.token_contract_address_hash, &1.address_hash})
{_total, result} =
repo.insert_all(
Address.CurrentTokenBalance,
ordered_current_token_balance,
# No `ON CONFLICT` because `delete_address_current_token_balances`
# should have removed any conflicts.
returning: [:address_hash, :token_contract_address_hash, :block_number, :value],
timeout: timeout
timestamps = Import.timestamps()
result =
CurrentTokenBalances.insert_changes_list_with_and_without_token_id(
current_token_balance,
repo,
timestamps,
timeout,
options
)
derived_address_current_token_balances =
Enum.map(result, &Map.take(&1, [:address_hash, :token_contract_address_hash, :block_number, :value]))
Enum.map(result, &Map.take(&1, [:address_hash, :token_contract_address_hash, :token_id, :block_number, :value]))
{:ok, derived_address_current_token_balances}
end
defp derive_address_current_token_balances_grouped_query(deleted_address_current_token_balances) do
initial_query =
from(tb in Address.TokenBalance,
select: %{
address_hash: tb.address_hash,
token_contract_address_hash: tb.token_contract_address_hash,
token_id: tb.token_id,
block_number: max(tb.block_number)
},
group_by: [tb.address_hash, tb.token_contract_address_hash, tb.token_id]
)
Enum.reduce(deleted_address_current_token_balances, initial_query, fn %{
address_hash: address_hash,
token_contract_address_hash:
token_contract_address_hash,
token_id: token_id
},
acc_query ->
if token_id do
from(tb in acc_query,
or_where:
tb.address_hash == ^address_hash and
tb.token_contract_address_hash == ^token_contract_address_hash and
tb.token_id == ^token_id
)
else
from(tb in acc_query,
or_where:
tb.address_hash == ^address_hash and
tb.token_contract_address_hash == ^token_contract_address_hash and
is_nil(tb.token_id)
)
end
end)
end
# `block_rewards` are linked to `blocks.hash`, but fetched by `blocks.number`, so when a block with the same number is
# inserted, the old block rewards need to be deleted, so that the old and new rewards aren't combined.
defp delete_rewards(repo, blocks_changes, %{timeout: timeout}) do

@ -21,17 +21,70 @@ defmodule Explorer.Chain.Import.Runner.Tokens do
@type holder_count :: non_neg_integer()
@type token_holder_count :: %{contract_address_hash: Hash.Address.t(), count: holder_count()}
def acquire_contract_address_tokens(repo, contract_address_hashes) do
token_query =
from(
token in Token,
where: token.contract_address_hash in ^contract_address_hashes,
# Enforce Token ShareLocks order (see docs: sharelocks.md)
order_by: token.contract_address_hash,
lock: "FOR UPDATE"
def acquire_contract_address_tokens(repo, contract_address_hashes_and_token_ids) do
initial_query_no_token_id =
from(token in Token,
select: token
)
tokens = repo.all(token_query)
initial_query_with_token_id =
from(token in Token,
left_join: instance in Token.Instance,
on: token.contract_address_hash == instance.token_contract_address_hash,
select: token
)
{query_no_token_id, query_with_token_id} =
contract_address_hashes_and_token_ids
|> Enum.reduce({initial_query_no_token_id, initial_query_with_token_id}, fn {contract_address_hash, token_id},
{query_no_token_id,
query_with_token_id} ->
if is_nil(token_id) do
{from(
token in query_no_token_id,
or_where: token.contract_address_hash == ^contract_address_hash
), query_with_token_id}
else
{query_no_token_id,
from(
[token, instance] in query_with_token_id,
or_where: token.contract_address_hash == ^contract_address_hash and instance.token_id == ^token_id
)}
end
end)
final_query_no_token_id =
if query_no_token_id == initial_query_no_token_id do
nil
else
from(
token in query_no_token_id,
# Enforce Token ShareLocks order (see docs: sharelocks.md)
order_by: [
token.contract_address_hash
],
lock: "FOR UPDATE"
)
end
final_query_with_token_id =
if query_with_token_id == initial_query_with_token_id do
nil
else
from(
[token, instance] in query_with_token_id,
# Enforce Token ShareLocks order (see docs: sharelocks.md)
order_by: [
token.contract_address_hash,
instance.token_id
],
lock: "FOR UPDATE"
)
end
tokens_no_token_id = (final_query_no_token_id && repo.all(final_query_no_token_id)) || []
tokens_with_token_id = (final_query_with_token_id && repo.all(final_query_with_token_id)) || []
tokens = tokens_no_token_id ++ tokens_with_token_id
{:ok, tokens}
end

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

@ -47,9 +47,11 @@ defmodule Explorer.Chain.TokenTransfer do
* `:transaction` - The `t:Explorer.Chain.Transaction.t/0` ledger
* `:transaction_hash` - Transaction foreign key
* `:log_index` - Index of the corresponding `t:Explorer.Chain.Log.t/0` in the transaction.
* `:amounts` - Tokens transferred amounts in case of batched transfer in ERC-1155
* `:token_ids` - IDs of the tokens (applicable to ERC-1155 tokens)
"""
@type t :: %TokenTransfer{
amount: Decimal.t(),
amount: Decimal.t() | nil,
block_number: non_neg_integer() | nil,
block_hash: Hash.Full.t(),
from_address: %Ecto.Association.NotLoaded{} | Address.t(),
@ -61,12 +63,16 @@ defmodule Explorer.Chain.TokenTransfer do
token_id: non_neg_integer() | nil,
transaction: %Ecto.Association.NotLoaded{} | Transaction.t(),
transaction_hash: Hash.Full.t(),
log_index: non_neg_integer()
log_index: non_neg_integer(),
amounts: [Decimal.t()] | nil,
token_ids: [non_neg_integer()] | nil
}
@typep paging_options :: {:paging_options, PagingOptions.t()}
@constant "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
@erc1155_single_transfer_signature "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62"
@erc1155_batch_transfer_signature "0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb"
@transfer_function_signature "0xa9059cbb"
@ -76,6 +82,8 @@ defmodule Explorer.Chain.TokenTransfer do
field(:block_number, :integer)
field(:log_index, :integer, primary_key: true)
field(:token_id, :decimal)
field(:amounts, {:array, :decimal})
field(:token_ids, {:array, :decimal})
belongs_to(:from_address, Address, foreign_key: :from_address_hash, references: :hash, type: Hash.Address)
belongs_to(:to_address, Address, foreign_key: :to_address_hash, references: :hash, type: Hash.Address)
@ -115,7 +123,7 @@ defmodule Explorer.Chain.TokenTransfer do
end
@required_attrs ~w(block_number log_index from_address_hash to_address_hash token_contract_address_hash transaction_hash block_hash)a
@optional_attrs ~w(amount token_id)a
@optional_attrs ~w(amount token_id amounts token_ids)a
@doc false
def changeset(%TokenTransfer{} = struct, params \\ %{}) do
@ -134,6 +142,10 @@ defmodule Explorer.Chain.TokenTransfer do
"""
def constant, do: @constant
def erc1155_single_transfer_signature, do: @erc1155_single_transfer_signature
def erc1155_batch_transfer_signature, do: @erc1155_batch_transfer_signature
@doc """
ERC 20's transfer(address,uint256) function signature
"""
@ -294,6 +306,7 @@ defmodule Explorer.Chain.TokenTransfer do
left_join: instance in Instance,
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.to_address_hash != ^"0x0000000000000000000000000000000000000000",
order_by: [desc: tt.block_number],
distinct: [desc: tt.token_id],
preload: [:to_address],

@ -27,6 +27,18 @@ defmodule Explorer.Token.BalanceReader do
}
]
@erc1155_balance_function_abi [
%{
"constant" => true,
"inputs" => [%{"name" => "_owner", "type" => "address"}, %{"name" => "_id", "type" => "uint256"}],
"name" => "balanceOf",
"outputs" => [%{"name" => "", "type" => "uint256"}],
"payable" => false,
"stateMutability" => "view",
"type" => "function"
}
]
@spec get_balances_of([
%{token_contract_address_hash: String.t(), address_hash: String.t(), block_number: non_neg_integer()}
]) :: [{:ok, non_neg_integer()} | {:error, String.t()}]
@ -37,6 +49,31 @@ defmodule Explorer.Token.BalanceReader do
|> Enum.map(&format_balance_result/1)
end
@spec get_balances_of_with_abi(
[
%{token_contract_address_hash: String.t(), address_hash: String.t(), block_number: non_neg_integer()}
],
[%{}]
) :: [{:ok, non_neg_integer()} | {:error, String.t()}]
def get_balances_of_with_abi(token_balance_requests, abi) do
formatted_balances_requests =
if abi == @erc1155_balance_function_abi do
token_balance_requests
|> Enum.map(&format_erc_1155_balance_request/1)
else
token_balance_requests
|> Enum.map(&format_balance_request/1)
end
if Enum.count(formatted_balances_requests) > 0 do
formatted_balances_requests
|> Reader.query_contracts(abi)
|> Enum.map(&format_balance_result/1)
else
[]
end
end
defp format_balance_request(%{
address_hash: address_hash,
block_number: block_number,
@ -50,6 +87,20 @@ defmodule Explorer.Token.BalanceReader do
}
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
{:ok, balance}
end

@ -29,9 +29,36 @@ defmodule Explorer.Token.InstanceMetadataRetriever do
}
]
@uri "0e89341c"
@abi_uri [
%{
"type" => "function",
"stateMutability" => "view",
"payable" => false,
"outputs" => [
%{
"type" => "string",
"name" => "",
"internalType" => "string"
}
],
"name" => "uri",
"inputs" => [
%{
"type" => "uint256",
"name" => "_id",
"internalType" => "uint256"
}
],
"constant" => true
}
]
@cryptokitties_address_hash "0x06012c8cf97bead5deae237070f9587f8e7a266d"
@no_uri_error "no uri"
@vm_execution_error "VM execution error"
def fetch_metadata(unquote(@cryptokitties_address_hash), token_id) do
%{"tokenURI" => {:ok, ["https://api.cryptokitties.co/kitties/#{token_id}"]}}
@ -42,31 +69,62 @@ defmodule Explorer.Token.InstanceMetadataRetriever do
# c87b56dd = keccak256(tokenURI(uint256))
contract_functions = %{@token_uri => [token_id]}
contract_address_hash
|> query_contract(contract_functions)
|> fetch_json()
res =
contract_address_hash
|> query_contract(contract_functions, @abi)
|> fetch_json()
if res == {:ok, %{error: @vm_execution_error}} do
contract_functions_uri = %{@uri => [token_id]}
contract_address_hash
|> query_contract(contract_functions_uri, @abi_uri)
|> fetch_json()
else
res
end
end
def query_contract(contract_address_hash, contract_functions) do
Reader.query_contract(contract_address_hash, @abi, contract_functions)
def query_contract(contract_address_hash, contract_functions, abi) do
Reader.query_contract(contract_address_hash, abi, contract_functions)
end
def fetch_json(%{@token_uri => {:ok, [""]}}) do
def fetch_json(uri) when uri in [%{@token_uri => {:ok, [""]}}, %{@uri => {:ok, [""]}}] do
{:ok, %{error: @no_uri_error}}
end
def fetch_json(%{@token_uri => {:error, "(-32015) VM execution error."}}) do
{:ok, %{error: @no_uri_error}}
def fetch_json(uri)
when uri in [
%{@token_uri => {:error, "(-32015) VM execution error."}},
%{@uri => {:error, "(-32015) VM execution error."}}
] do
{:ok, %{error: @vm_execution_error}}
end
def fetch_json(%{@token_uri => {:error, "(-32015) VM execution error." <> _}}) do
{:ok, %{error: @vm_execution_error}}
end
def fetch_json(%{@uri => {:error, "(-32015) VM execution error." <> _}}) do
{:ok, %{error: @vm_execution_error}}
end
def fetch_json(%{@token_uri => {:ok, ["http://" <> _ = token_uri]}}) do
fetch_metadata(token_uri)
end
def fetch_json(%{@uri => {:ok, ["http://" <> _ = token_uri]}}) do
fetch_metadata(token_uri)
end
def fetch_json(%{@token_uri => {:ok, ["https://" <> _ = token_uri]}}) do
fetch_metadata(token_uri)
end
def fetch_json(%{@uri => {:ok, ["https://" <> _ = token_uri]}}) do
fetch_metadata(token_uri)
end
def fetch_json(%{@token_uri => {:ok, ["data:application/json," <> json]}}) do
decoded_json = URI.decode(json)
@ -80,16 +138,39 @@ defmodule Explorer.Token.InstanceMetadataRetriever do
{:error, json}
end
def fetch_json(%{@uri => {:ok, ["data:application/json," <> json]}}) do
decoded_json = URI.decode(json)
fetch_json(%{@token_uri => {:ok, [decoded_json]}})
rescue
e ->
Logger.debug(["Unknown metadata format #{inspect(json)}. error #{inspect(e)}"],
fetcher: :token_instances
)
{:error, json}
end
def fetch_json(%{@token_uri => {:ok, ["ipfs://ipfs/" <> ipfs_uid]}}) do
ipfs_url = "https://ipfs.io/ipfs/" <> ipfs_uid
fetch_metadata(ipfs_url)
end
def fetch_json(%{@uri => {:ok, ["ipfs://ipfs/" <> ipfs_uid]}}) do
ipfs_url = "https://ipfs.io/ipfs/" <> ipfs_uid
fetch_metadata(ipfs_url)
end
def fetch_json(%{@token_uri => {:ok, ["ipfs://" <> ipfs_uid]}}) do
ipfs_url = "https://ipfs.io/ipfs/" <> ipfs_uid
fetch_metadata(ipfs_url)
end
def fetch_json(%{@uri => {:ok, ["ipfs://" <> ipfs_uid]}}) do
ipfs_url = "https://ipfs.io/ipfs/" <> ipfs_uid
fetch_metadata(ipfs_url)
end
def fetch_json(%{@token_uri => {:ok, [json]}}) do
{:ok, json} = decode_json(json)
@ -103,17 +184,30 @@ defmodule Explorer.Token.InstanceMetadataRetriever do
{:error, json}
end
def fetch_json(%{@uri => {:ok, [json]}}) do
{:ok, json} = decode_json(json)
check_type(json)
rescue
e ->
Logger.debug(["Unknown metadata format #{inspect(json)}. error #{inspect(e)}"],
fetcher: :token_instances
)
{:error, json}
end
def fetch_json(result) do
Logger.debug(["Unknown metadata format #{inspect(result)}."], fetcher: :token_instances)
{:error, result}
end
defp fetch_metadata(token_uri) do
case HTTPoison.get(token_uri) do
defp fetch_metadata(uri) do
case HTTPoison.get(uri) do
{:ok, %Response{body: body, status_code: 200, headers: headers}} ->
if Enum.member?(headers, {"Content-Type", "image/png"}) do
json = %{"image" => token_uri}
json = %{"image" => uri}
check_type(json)
else
@ -135,7 +229,7 @@ defmodule Explorer.Token.InstanceMetadataRetriever do
end
rescue
e ->
Logger.debug(["Could not send request to token uri #{inspect(token_uri)}. error #{inspect(e)}"],
Logger.debug(["Could not send request to token uri #{inspect(uri)}. error #{inspect(e)}"],
fetcher: :token_instances
)

@ -0,0 +1,12 @@
defmodule Explorer.Repo.Migrations.AddTokenIdToTokenBalances do
use Ecto.Migration
def change do
alter table(:address_token_balances) do
add(:token_id, :numeric, precision: 78, scale: 0, null: true)
add(:token_type, :string, null: true)
end
create(index(:address_token_balances, [:token_id]))
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

@ -0,0 +1,25 @@
defmodule Explorer.Repo.Migrations.AddressCurrentTokenBalancesAddTokenIdToUniqueIndex do
use Ecto.Migration
def change do
drop(unique_index(:address_current_token_balances, ~w(address_hash token_contract_address_hash)a))
create(
unique_index(
:address_current_token_balances,
~w(address_hash token_contract_address_hash token_id)a,
name: :fetched_current_token_balances_with_token_id,
where: "token_id IS NOT NULL"
)
)
create(
unique_index(
:address_current_token_balances,
~w(address_hash token_contract_address_hash)a,
name: :fetched_current_token_balances,
where: "token_id IS NULL"
)
)
end
end

@ -0,0 +1,25 @@
defmodule Explorer.Repo.Migrations.AddressTokenBalancesAddTokenIdToUniqueIndex do
use Ecto.Migration
def change do
drop(unique_index(:address_token_balances, ~w(address_hash token_contract_address_hash block_number)a))
create(
unique_index(
:address_token_balances,
~w(address_hash token_contract_address_hash token_id block_number)a,
name: :fetched_token_balances_with_token_id,
where: "token_id IS NOT NULL"
)
)
create(
unique_index(
:address_token_balances,
~w(address_hash token_contract_address_hash block_number)a,
name: :fetched_token_balances,
where: "token_id IS NULL"
)
)
end
end

@ -0,0 +1,32 @@
defmodule Explorer.Repo.Migrations.AddressTokenBalancesChangeUnfetchedTokenBalancesUniqueIndex do
use Ecto.Migration
def change do
drop(
unique_index(
:address_token_balances,
~w(address_hash token_contract_address_hash block_number)a,
name: :unfetched_token_balances,
where: "value_fetched_at IS NULL"
)
)
create(
unique_index(
:address_token_balances,
~w(address_hash token_contract_address_hash block_number)a,
name: :unfetched_token_balances,
where: "value_fetched_at IS NULL and token_id IS NULL"
)
)
create(
unique_index(
:address_token_balances,
~w(address_hash token_contract_address_hash token_id block_number)a,
name: :unfetched_token_balances_with_token_id,
where: "value_fetched_at IS NULL and token_id IS NOT NULL"
)
)
end
end

@ -0,0 +1,10 @@
defmodule Explorer.Repo.Migrations.ExtendTokenTransfersForErc1155 do
use Ecto.Migration
def change do
alter table(:token_transfers) do
add(:amounts, {:array, :decimal}, null: true)
add(:token_ids, {:array, :numeric}, precision: 78, scale: 0, null: true)
end
end
end

@ -0,0 +1,55 @@
defmodule Explorer.Repo.Migrations.RemoveDuplicatesOfCurrentTokenBalances do
use Ecto.Migration
def change do
execute("""
DELETE FROM address_current_token_balances
WHERE id in (
SELECT a.id FROM (SELECT actb.*
FROM address_current_token_balances actb
INNER JOIN tokens t
ON actb.token_contract_address_hash = t.contract_address_hash
WHERE t.type='ERC-721') AS a
LEFT JOIN
(SELECT actb.address_hash, actb.token_contract_address_hash, MAX(actb.value_fetched_at) AS max_value_fetched_at
FROM address_current_token_balances actb
INNER JOIN tokens t
ON actb.token_contract_address_hash = t.contract_address_hash
WHERE t.type='ERC-721'
GROUP BY token_contract_address_hash, address_hash) c
ON a.address_hash=c.address_hash AND a.token_contract_address_hash = c.token_contract_address_hash AND a.value_fetched_at = c.max_value_fetched_at
WHERE c.address_hash IS NULL
);
""")
execute("""
UPDATE address_current_token_balances
SET token_id = NULL
WHERE id in (
SELECT a.id FROM (SELECT actb.*
FROM address_current_token_balances actb
INNER JOIN tokens t
ON actb.token_contract_address_hash = t.contract_address_hash
WHERE t.type='ERC-721'
AND actb.token_id IS NOT NULL
) a
);
""")
execute("""
UPDATE address_current_token_balances
SET token_type = t.type
FROM tokens t
WHERE address_current_token_balances.token_type IS NULL
AND t.contract_address_hash = address_current_token_balances.token_contract_address_hash;
""")
execute("""
UPDATE address_token_balances
SET token_type = t.type
FROM tokens t
WHERE address_token_balances.token_type IS NULL
AND t.contract_address_hash = address_token_balances.token_contract_address_hash;
""")
end
end

@ -65,6 +65,141 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do
assert current_token_balances == 1
end
test "inserts values for multiple token IDs in the current token balances", %{
address: %Address{hash: address_hash},
token: %Token{contract_address_hash: token_contract_address_hash},
options: options
} do
value_1 = Decimal.new(111)
token_id_1 = Decimal.new(1)
value_2 = Decimal.new(222)
token_id_2 = Decimal.new(2)
token_erc_20 = insert(:token, holder_count: 0)
token_erc_20_contract_address_hash = token_erc_20.contract_address_hash
value_3 = Decimal.new(333)
token_id_3 = nil
token_erc_721 = insert(:token, holder_count: 0)
token_erc_721_contract_address_hash = token_erc_721.contract_address_hash
value_4 = Decimal.new(1)
token_id_4 = Decimal.new(1)
value_5 = Decimal.new(2)
token_id_5 = Decimal.new(555)
block_number = 1
assert {:ok,
%{
address_current_token_balances: [
%Explorer.Chain.Address.CurrentTokenBalance{
address_hash: ^address_hash,
block_number: ^block_number,
token_contract_address_hash: ^token_erc_20_contract_address_hash,
value: ^value_3,
token_id: ^token_id_3
},
%Explorer.Chain.Address.CurrentTokenBalance{
address_hash: ^address_hash,
block_number: ^block_number,
token_contract_address_hash: ^token_erc_721_contract_address_hash,
value: ^value_5,
token_id: nil
},
%Explorer.Chain.Address.CurrentTokenBalance{
address_hash: ^address_hash,
block_number: ^block_number,
token_contract_address_hash: ^token_contract_address_hash,
value: ^value_1,
token_id: ^token_id_1
},
%Explorer.Chain.Address.CurrentTokenBalance{
address_hash: ^address_hash,
block_number: ^block_number,
token_contract_address_hash: ^token_contract_address_hash,
value: ^value_2,
token_id: ^token_id_2
}
],
address_current_token_balances_update_token_holder_counts: [
%{
contract_address_hash: ^token_contract_address_hash,
holder_count: 2
},
%{
contract_address_hash: ^token_erc_20_contract_address_hash,
holder_count: 1
},
%{
contract_address_hash: ^token_erc_721_contract_address_hash,
holder_count: 1
}
]
}} =
run_changes_list(
[
%{
address_hash: address_hash,
block_number: block_number,
token_contract_address_hash: token_contract_address_hash,
value: value_1,
value_fetched_at: DateTime.utc_now(),
token_id: token_id_1,
token_type: "ERC-1155"
},
%{
address_hash: address_hash,
block_number: block_number,
token_contract_address_hash: token_contract_address_hash,
value: value_2,
value_fetched_at: DateTime.utc_now(),
token_id: token_id_2,
token_type: "ERC-1155"
},
%{
address_hash: address_hash,
block_number: block_number,
token_contract_address_hash: token_erc_20.contract_address_hash,
value: value_3,
value_fetched_at: DateTime.utc_now(),
token_id: token_id_3,
token_type: "ERC-20"
},
%{
address_hash: address_hash,
block_number: block_number,
token_contract_address_hash: token_erc_721.contract_address_hash,
value: value_4,
value_fetched_at: DateTime.utc_now(),
token_id: token_id_4,
token_type: "ERC-721"
},
%{
address_hash: address_hash,
block_number: block_number,
token_contract_address_hash: token_erc_721.contract_address_hash,
value: value_5,
value_fetched_at: DateTime.utc_now(),
token_id: token_id_5,
token_type: "ERC-721"
}
],
options
)
current_token_balances =
CurrentTokenBalance
|> Repo.all()
current_token_balances_count =
current_token_balances
|> Enum.count()
assert current_token_balances_count == 4
end
test "updates when the new block number is greater", %{
address: address,
token: token,

@ -29,7 +29,9 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalancesTest do
block_number: block_number,
token_contract_address_hash: token_contract_address_hash,
value: value,
value_fetched_at: value_fetched_at
value_fetched_at: value_fetched_at,
token_id: 11,
token_type: "ERC-20"
}
assert {:ok,
@ -69,7 +71,9 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalancesTest do
block_number: block_number,
token_contract_address_hash: token_contract_address_hash,
value: nil,
value_fetched_at: value_fetched_at
value_fetched_at: value_fetched_at,
token_id: nil,
token_type: "ERC-20"
}
assert {:ok,
@ -97,6 +101,58 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalancesTest do
end
end
test "does not nillifies existing value ERC-1155" do
address = insert(:address)
token = insert(:token)
options = %{
timeout: :infinity,
timestamps: %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()}
}
value_fetched_at = DateTime.utc_now()
block_number = 1
value = Decimal.new(100)
token_contract_address_hash = token.contract_address_hash
address_hash = address.hash
changes = %{
address_hash: address_hash,
block_number: block_number,
token_contract_address_hash: token_contract_address_hash,
value: nil,
value_fetched_at: value_fetched_at,
token_id: 11,
token_type: "ERC-1155"
}
assert {:ok,
%{
address_token_balances: [
%TokenBalance{
address_hash: address_hash,
block_number: ^block_number,
token_contract_address_hash: ^token_contract_address_hash,
value: nil,
value_fetched_at: ^value_fetched_at
}
]
}} = run_changes(changes, options)
new_changes = %{
address_hash: address_hash,
block_number: block_number,
token_contract_address_hash: token_contract_address_hash,
value: value,
value_fetched_at: DateTime.utc_now()
}
run_changes(new_changes, options)
end
defp run_changes(changes, options) when is_map(changes) do
run_changes_list([changes], options)
end

@ -375,17 +375,20 @@ defmodule Explorer.Chain.ImportTest do
%{
address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
block_number: "37"
block_number: "37",
token_type: "ERC-20"
},
%{
address_hash: "0x515c09c5bba1ed566b02a5b0599ec5d5d0aee73d",
token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
block_number: "37"
block_number: "37",
token_type: "ERC-20"
},
%{
address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
block_number: "37"
block_number: "37",
token_type: "ERC-20"
}
],
timeout: 5
@ -425,13 +428,15 @@ defmodule Explorer.Chain.ImportTest do
address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
block_number: "37",
value: 200
value: 200,
token_type: "ERC-20"
},
%{
address_hash: "0x515c09c5bba1ed566b02a5b0599ec5d5d0aee73d",
token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
block_number: "37",
value: 100
value: 100,
token_type: "ERC-20"
}
],
timeout: 5
@ -1564,8 +1569,8 @@ defmodule Explorer.Chain.ImportTest do
},
address_coin_balances: %{
params: [
%{address_hash: miner_hash, block_number: block_number, value: nil},
%{address_hash: uncle_miner_hash, block_number: block_number, value: nil}
%{address_hash: miner_hash, block_number: block_number, value: nil, token_type: "ERC-20"},
%{address_hash: uncle_miner_hash, block_number: block_number, value: nil, token_type: "ERC-20"}
],
timeout: 1
},
@ -2250,7 +2255,8 @@ defmodule Explorer.Chain.ImportTest do
address_hash: address_hash,
token_contract_address_hash: token_contract_address_hash,
block_number: block_number,
value: value_after
value: value_after,
token_type: "ERC-20"
}
]
},
@ -2260,7 +2266,8 @@ defmodule Explorer.Chain.ImportTest do
address_hash: address_hash,
token_contract_address_hash: token_contract_address_hash,
block_number: block_number,
value: value_after
value: value_after,
token_type: "ERC-20"
}
]
},

@ -32,7 +32,8 @@ defmodule Explorer.Token.BalanceReaderTest do
%{
token_contract_address_hash: token_contract_address_hash,
address_hash: address_hash,
block_number: block_number
block_number: block_number,
token_type: "ERC-20"
}
])
@ -51,7 +52,8 @@ defmodule Explorer.Token.BalanceReaderTest do
%{
token_contract_address_hash: token_contract_address_hash,
address_hash: address_hash,
block_number: block_number
block_number: block_number,
token_type: "ERC-20"
}
])

@ -9,6 +9,49 @@ defmodule Explorer.Token.InstanceMetadataRetrieverTest do
setup :verify_on_exit!
setup :set_mox_global
@abi [
%{
"type" => "function",
"stateMutability" => "view",
"payable" => false,
"outputs" => [
%{"type" => "string", "name" => ""}
],
"name" => "tokenURI",
"inputs" => [
%{
"type" => "uint256",
"name" => "_tokenId"
}
],
"constant" => true
}
]
@abi_uri [
%{
"type" => "function",
"stateMutability" => "view",
"payable" => false,
"outputs" => [
%{
"type" => "string",
"name" => "",
"internalType" => "string"
}
],
"name" => "uri",
"inputs" => [
%{
"type" => "uint256",
"name" => "_id",
"internalType" => "uint256"
}
],
"constant" => true
}
]
describe "fetch_metadata/2" do
@tag :no_parity
@tag :no_geth
@ -46,9 +89,56 @@ defmodule Explorer.Token.InstanceMetadataRetrieverTest do
assert %{
"c87b56dd" => {:ok, ["https://vault.warriders.com/18290729947667102496.json"]}
} ==
InstanceMetadataRetriever.query_contract("0x5caebd3b32e210e85ce3e9d51638b9c445481567", %{
"c87b56dd" => [18_290_729_947_667_102_496]
})
InstanceMetadataRetriever.query_contract(
"0x5caebd3b32e210e85ce3e9d51638b9c445481567",
%{
"c87b56dd" => [18_290_729_947_667_102_496]
},
@abi
)
end
test "fetches json metadata for ERC-1155 token", %{json_rpc_named_arguments: json_rpc_named_arguments} do
if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do
EthereumJSONRPC.Mox
|> expect(:json_rpc, fn [
%{
id: 0,
jsonrpc: "2.0",
method: "eth_call",
params: [
%{
data:
"0x0e89341c000000000000000000000000000000000000000000000000fdd5b9fa9d4bfb20",
to: "0x5caebd3b32e210e85ce3e9d51638b9c445481567"
},
"latest"
]
}
],
_options ->
{:ok,
[
%{
id: 0,
jsonrpc: "2.0",
result:
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003568747470733a2f2f7661756c742e7761727269646572732e636f6d2f31383239303732393934373636373130323439362e6a736f6e0000000000000000000000"
}
]}
end)
end
assert %{
"0e89341c" => {:ok, ["https://vault.warriders.com/18290729947667102496.json"]}
} ==
InstanceMetadataRetriever.query_contract(
"0x5caebd3b32e210e85ce3e9d51638b9c445481567",
%{
"0e89341c" => [18_290_729_947_667_102_496]
},
@abi_uri
)
end
end

@ -586,7 +586,8 @@ defmodule Explorer.Factory do
token_contract_address_hash: insert(:token).contract_address_hash,
block_number: block_number(),
value: Enum.random(1..100_000),
value_fetched_at: DateTime.utc_now()
value_fetched_at: DateTime.utc_now(),
token_type: "ERC-20"
}
end

@ -96,7 +96,7 @@ defmodule Indexer.Fetcher.TokenBalance do
retryable_params_list =
params_list
|> Enum.filter(&(&1.retries_count <= @max_retries))
|> Enum.uniq_by(&Map.take(&1, [:token_contract_address_hash, :address_hash, :block_number]))
|> Enum.uniq_by(&Map.take(&1, [:token_contract_address_hash, :token_id, :address_hash, :block_number]))
Logger.metadata(count: Enum.count(retryable_params_list))
@ -138,15 +138,26 @@ defmodule Indexer.Fetcher.TokenBalance do
%{
token_contract_address_hash: token_contract_address_hash,
address_hash: address_hash,
block_number: block_number
block_number: block_number,
token_type: token_type,
token_id: token_id
} = token_balance
) do
retries_count = Map.get(token_balance, :retries_count, 0)
{address_hash.bytes, token_contract_address_hash.bytes, block_number, retries_count}
token_id_int =
case token_id do
%Decimal{} -> Decimal.to_integer(token_id)
id_int when is_integer(id_int) -> id_int
_ -> token_id
end
{address_hash.bytes, token_contract_address_hash.bytes, block_number, token_type, token_id_int, retries_count}
end
defp format_params({address_hash_bytes, token_contract_address_hash_bytes, block_number, retries_count}) do
defp format_params(
{address_hash_bytes, token_contract_address_hash_bytes, block_number, token_type, token_id, retries_count}
) do
{:ok, token_contract_address_hash} = Hash.Address.cast(token_contract_address_hash_bytes)
{:ok, address_hash} = Hash.Address.cast(address_hash_bytes)
@ -154,7 +165,9 @@ defmodule Indexer.Fetcher.TokenBalance do
token_contract_address_hash: to_string(token_contract_address_hash),
address_hash: to_string(address_hash),
block_number: block_number,
retries_count: retries_count
retries_count: retries_count,
token_type: token_type,
token_id: token_id
}
end
end

@ -13,6 +13,18 @@ defmodule Indexer.TokenBalances do
alias Indexer.Fetcher.TokenBalance
alias Indexer.Tracer
@erc1155_balance_function_abi [
%{
"constant" => true,
"inputs" => [%{"name" => "_owner", "type" => "address"}, %{"name" => "_id", "type" => "uint256"}],
"name" => "balanceOf",
"outputs" => [%{"name" => "", "type" => "uint256"}],
"payable" => false,
"stateMutability" => "view",
"type" => "function"
}
]
@doc """
Fetches TokenBalances from specific Addresses and Blocks in the Blockchain
@ -26,6 +38,8 @@ defmodule Indexer.TokenBalances do
* `token_contract_address_hash` - The contract address that represents the Token in the blockchain.
* `address_hash` - The address_hash that we want to know the balance.
* `block_number` - The block number that the address_hash has the balance.
* `token_type` - type of the token that balance belongs to
* `token_id` - token id for ERC-1155 tokens
"""
def fetch_token_balances_from_blockchain([]), do: {:ok, []}
@ -33,12 +47,39 @@ defmodule Indexer.TokenBalances do
def fetch_token_balances_from_blockchain(token_balances) do
Logger.debug("fetching token balances", count: Enum.count(token_balances))
requested_token_balances =
regular_token_balances =
token_balances
|> Enum.filter(fn request ->
if Map.has_key?(request, :token_type) do
request.token_type !== "ERC-1155"
else
true
end
end)
erc1155_token_balances =
token_balances
|> Enum.filter(fn request ->
if Map.has_key?(request, :token_type) do
request.token_type == "ERC-1155"
else
false
end
end)
requested_regular_token_balances =
regular_token_balances
|> BalanceReader.get_balances_of()
|> Stream.zip(token_balances)
|> Stream.zip(regular_token_balances)
|> Enum.map(fn {result, token_balance} -> set_token_balance_value(result, token_balance) end)
requested_erc1155_token_balances =
erc1155_token_balances
|> BalanceReader.get_balances_of_with_abi(@erc1155_balance_function_abi)
|> Stream.zip(erc1155_token_balances)
|> Enum.map(fn {result, token_balance} -> set_token_balance_value(result, token_balance) end)
requested_token_balances = requested_regular_token_balances ++ requested_erc1155_token_balances
fetched_token_balances = Enum.filter(requested_token_balances, &ignore_request_with_errors/1)
requested_token_balances
@ -51,13 +92,17 @@ defmodule Indexer.TokenBalances do
def to_address_current_token_balances(address_token_balances) when is_list(address_token_balances) do
address_token_balances
|> Enum.group_by(fn %{address_hash: address_hash, token_contract_address_hash: token_contract_address_hash} ->
{address_hash, token_contract_address_hash}
|> Enum.group_by(fn %{
address_hash: address_hash,
token_contract_address_hash: token_contract_address_hash,
token_id: token_id
} ->
{address_hash, token_contract_address_hash, token_id}
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})
|> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id, &1.address_hash})
end
defp set_token_balance_value({:ok, balance}, token_balance) do
@ -137,10 +182,20 @@ defmodule Indexer.TokenBalances do
end
defp present?(list, token_balance) do
Enum.any?(list, fn item ->
token_balance.address_hash == item.address_hash &&
token_balance.token_contract_address_hash == item.token_contract_address_hash &&
token_balance.block_number == item.block_number
end)
if token_balance.token_id do
Enum.any?(list, fn item ->
token_balance.address_hash == item.address_hash &&
token_balance.token_contract_address_hash == item.token_contract_address_hash &&
token_balance.token_id == item.token_id &&
token_balance.block_number == item.block_number
end)
else
Enum.any?(list, fn item ->
token_balance.address_hash == item.address_hash &&
token_balance.token_contract_address_hash == item.token_contract_address_hash &&
is_nil(item.token_id) &&
token_balance.block_number == item.block_number
end)
end
end
end

@ -16,14 +16,25 @@ defmodule Indexer.Transform.AddressTokenBalances do
block_number: block_number,
from_address_hash: from_address_hash,
to_address_hash: to_address_hash,
token_contract_address_hash: token_contract_address_hash
},
token_contract_address_hash: token_contract_address_hash,
token_id: token_id,
token_type: token_type
} = params,
acc
when is_integer(block_number) and is_binary(from_address_hash) and
is_binary(to_address_hash) and is_binary(token_contract_address_hash) ->
acc
|> add_token_balance_address(from_address_hash, token_contract_address_hash, block_number)
|> add_token_balance_address(to_address_hash, token_contract_address_hash, block_number)
if params[:token_ids] && token_type == "ERC-1155" do
params[:token_ids]
|> Enum.reduce(acc, fn id, sub_acc ->
sub_acc
|> add_token_balance_address(from_address_hash, token_contract_address_hash, id, token_type, block_number)
|> add_token_balance_address(to_address_hash, token_contract_address_hash, id, token_type, block_number)
end)
else
acc
|> add_token_balance_address(from_address_hash, token_contract_address_hash, token_id, token_type, block_number)
|> add_token_balance_address(to_address_hash, token_contract_address_hash, token_id, token_type, block_number)
end
end)
end
@ -31,13 +42,15 @@ defmodule Indexer.Transform.AddressTokenBalances do
Enum.filter(token_transfers_params, &do_filter_burn_address/1)
end
defp add_token_balance_address(map_set, unquote(@burn_address), _, _), do: map_set
defp add_token_balance_address(map_set, unquote(@burn_address), _, _, _, _), do: map_set
defp add_token_balance_address(map_set, address, token_contract_address, block_number) do
defp add_token_balance_address(map_set, address, token_contract_address, token_id, token_type, block_number) do
MapSet.put(map_set, %{
address_hash: address,
token_contract_address_hash: token_contract_address,
block_number: block_number
block_number: block_number,
token_id: token_id,
token_type: token_type
})
end

@ -18,12 +18,21 @@ defmodule Indexer.Transform.TokenTransfers do
def parse(logs) do
initial_acc = %{tokens: [], token_transfers: []}
token_transfers_from_logs =
erc20_and_erc721_token_transfers =
logs
|> Enum.filter(&(&1.first_topic == unquote(TokenTransfer.constant())))
|> Enum.reduce(initial_acc, &do_parse/2)
token_transfers = token_transfers_from_logs.token_transfers
erc1155_token_transfers =
logs
|> Enum.filter(fn log ->
log.first_topic == TokenTransfer.erc1155_single_transfer_signature() ||
log.first_topic == TokenTransfer.erc1155_batch_transfer_signature()
end)
|> Enum.reduce(initial_acc, &do_parse(&1, &2, :erc1155))
tokens = erc1155_token_transfers.tokens ++ erc20_and_erc721_token_transfers.tokens
token_transfers = erc1155_token_transfers.token_transfers ++ erc20_and_erc721_token_transfers.token_transfers
token_transfers
|> Enum.filter(fn token_transfer ->
@ -35,18 +44,23 @@ defmodule Indexer.Transform.TokenTransfers do
|> Enum.dedup()
|> Enum.each(&update_token/1)
tokens_dedup = token_transfers_from_logs.tokens |> Enum.dedup()
tokens_dedup = tokens |> Enum.dedup()
token_transfers_from_logs_dedup = %{
tokens: tokens_dedup,
token_transfers: token_transfers_from_logs.token_transfers
token_transfers: token_transfers
}
token_transfers_from_logs_dedup
end
defp do_parse(log, %{tokens: tokens, token_transfers: token_transfers} = acc) do
{token, token_transfer} = parse_params(log)
defp do_parse(log, %{tokens: tokens, token_transfers: token_transfers} = acc, type \\ :erc20_erc721) do
{token, token_transfer} =
if type != :erc1155 do
parse_params(log)
else
parse_erc1155_params(log)
end
%{
tokens: [token | tokens],
@ -72,6 +86,7 @@ defmodule Indexer.Transform.TokenTransfers do
to_address_hash: truncate_address_hash(log.third_topic),
token_contract_address_hash: log.address_hash,
transaction_hash: log.transaction_hash,
token_id: nil,
token_type: "ERC-20"
}
@ -109,7 +124,14 @@ defmodule Indexer.Transform.TokenTransfers do
end
# ERC-721 token transfer with info in data field instead of in log topics
defp parse_params(%{second_topic: nil, third_topic: nil, fourth_topic: nil, data: data} = log)
defp parse_params(
%{
second_topic: nil,
third_topic: nil,
fourth_topic: nil,
data: data
} = log
)
when not is_nil(data) do
[from_address_hash, to_address_hash, token_id] = decode_data(data, [:address, :address, {:uint, 256}])
@ -157,6 +179,62 @@ defmodule Indexer.Transform.TokenTransfers do
:ok
end
def parse_erc1155_params(
%{
first_topic: unquote(TokenTransfer.erc1155_batch_transfer_signature()),
third_topic: third_topic,
fourth_topic: fourth_topic,
data: data
} = log
) do
[token_ids, values] = decode_data(data, [{:array, {:uint, 256}}, {:array, {:uint, 256}}])
token_transfer = %{
block_number: log.block_number,
block_hash: log.block_hash,
log_index: log.index,
from_address_hash: truncate_address_hash(third_topic),
to_address_hash: truncate_address_hash(fourth_topic),
token_contract_address_hash: log.address_hash,
transaction_hash: log.transaction_hash,
token_type: "ERC-1155",
token_ids: token_ids,
token_id: nil,
amounts: values
}
token = %{
contract_address_hash: log.address_hash,
type: "ERC-1155"
}
{token, token_transfer}
end
def parse_erc1155_params(%{third_topic: third_topic, fourth_topic: fourth_topic, data: data} = log) do
[token_id, value] = decode_data(data, [{:uint, 256}, {:uint, 256}])
token_transfer = %{
amount: value,
block_number: log.block_number,
block_hash: log.block_hash,
log_index: log.index,
from_address_hash: truncate_address_hash(third_topic),
to_address_hash: truncate_address_hash(fourth_topic),
token_contract_address_hash: log.address_hash,
transaction_hash: log.transaction_hash,
token_type: "ERC-1155",
token_id: token_id
}
token = %{
contract_address_hash: log.address_hash,
type: "ERC-1155"
}
{token, token_transfer}
end
defp truncate_address_hash(nil), do: "0x0000000000000000000000000000000000000000"
defp truncate_address_hash("0x000000000000000000000000" <> truncated_hash) do

@ -17,13 +17,13 @@ defmodule Indexer.Fetcher.TokenBalanceTest do
%Address.TokenBalance{
address_hash: %Hash{bytes: address_hash_bytes},
token_contract_address_hash: %Hash{bytes: token_contract_address_hash_bytes},
block_number: block_number
block_number: _block_number
} = insert(:token_balance, block_number: 1_000, value_fetched_at: nil)
insert(:token_balance, value_fetched_at: DateTime.utc_now())
assert TokenBalance.init([], &[&1 | &2], nil) == [
{address_hash_bytes, token_contract_address_hash_bytes, block_number, 0}
{address_hash_bytes, token_contract_address_hash_bytes, 1000, "ERC-20", nil, 0}
]
end
end
@ -58,7 +58,7 @@ defmodule Indexer.Fetcher.TokenBalanceTest do
)
assert TokenBalance.run(
[{address_hash_bytes, token_contract_address_hash_bytes, block_number, 0}],
[{address_hash_bytes, token_contract_address_hash_bytes, block_number, "ERC-20", nil, 0}],
nil
) == :ok
@ -76,26 +76,12 @@ defmodule Indexer.Fetcher.TokenBalanceTest do
token_balance_a = 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_balance_a.address_hash.bytes,
token_balance_a.token_contract_address_hash.bytes,
"ERC-20",
nil,
token_balance_a.block_number,
# this token balance must be ignored
max_retries
@ -103,6 +89,8 @@ defmodule Indexer.Fetcher.TokenBalanceTest do
{
token_balance_b.address_hash.bytes,
token_balance_b.token_contract_address_hash.bytes,
"ERC-20",
nil,
token_balance_b.block_number,
# this token balance still have to be retried
max_retries - 2
@ -136,8 +124,8 @@ defmodule Indexer.Fetcher.TokenBalanceTest do
assert TokenBalance.run(
[
{address_hash_bytes, token_contract_address_hash_bytes, block_number, 0},
{address_hash_bytes, token_contract_address_hash_bytes, block_number, 0}
{address_hash_bytes, token_contract_address_hash_bytes, block_number, "ERC-20", nil, 0},
{address_hash_bytes, token_contract_address_hash_bytes, block_number, "ERC-20", nil, 0}
],
nil
) == :ok
@ -161,6 +149,7 @@ defmodule Indexer.Fetcher.TokenBalanceTest do
address_hash: nil,
block_number: nil,
token_contract_address_hash: to_string(token_balance.token_contract_address_hash),
token_id: nil,
value: nil,
value_fetched_at: nil
}
@ -177,7 +166,9 @@ defmodule Indexer.Fetcher.TokenBalanceTest do
%{
address_hash: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
block_number: 19999,
token_contract_address_hash: to_string(contract.contract_address_hash)
token_contract_address_hash: to_string(contract.contract_address_hash),
token_type: "ERC-20",
token_id: nil
}
]
@ -186,7 +177,32 @@ defmodule Indexer.Fetcher.TokenBalanceTest do
assert {:ok, _} = Explorer.Chain.hash_to_address(address_hash)
end
test "import the token balances and return :ok when there are multiple balances for the same address on the batch" do
test "import the token balances and return :ok when there are multiple balances for the same address on the batch (ERC-20)" do
contract = insert(:token)
contract2 = insert(:token)
insert(:block, number: 19999)
token_balances_params = [
%{
address_hash: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
block_number: 19999,
token_contract_address_hash: to_string(contract.contract_address_hash),
token_id: nil,
token_type: "ERC-20"
},
%{
address_hash: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
block_number: 19999,
token_contract_address_hash: to_string(contract2.contract_address_hash),
token_id: nil,
token_type: "ERC-20"
}
]
assert TokenBalance.import_token_balances(token_balances_params) == :ok
end
test "import the token balances and return :ok when there are multiple balances for the same address on the batch (ERC-1155)" do
contract = insert(:token)
contract2 = insert(:token)
insert(:block, number: 19999)
@ -195,12 +211,16 @@ defmodule Indexer.Fetcher.TokenBalanceTest do
%{
address_hash: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
block_number: 19999,
token_contract_address_hash: to_string(contract.contract_address_hash)
token_contract_address_hash: to_string(contract.contract_address_hash),
token_id: 11,
token_type: "ERC-20"
},
%{
address_hash: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
block_number: 19999,
token_contract_address_hash: to_string(contract2.contract_address_hash)
token_contract_address_hash: to_string(contract2.contract_address_hash),
token_id: 11,
token_type: "ERC-1155"
}
]

@ -26,10 +26,14 @@ defmodule Indexer.TokenBalancesTest do
token = insert(:token, contract_address: build(:contract_address))
address_hash_string = Hash.to_string(address.hash)
token_contract_address_hash = Hash.to_string(token.contract_address_hash)
data = %{
token_contract_address_hash: Hash.to_string(token.contract_address_hash),
token_contract_address_hash: token_contract_address_hash,
address_hash: address_hash_string,
block_number: 1_000
block_number: 1_000,
token_id: 11,
token_type: "ERC-20"
}
get_balance_from_blockchain()
@ -38,13 +42,183 @@ defmodule Indexer.TokenBalancesTest do
assert %{
value: 1_000_000_000_000_000_000_000_000,
token_contract_address_hash: token_contract_address_hash,
address_hash: address_hash,
token_contract_address_hash: ^token_contract_address_hash,
address_hash: ^address_hash_string,
block_number: 1_000,
value_fetched_at: _
} = List.first(result)
end
test "fetches balances of ERC-1155 tokens" do
address = insert(:address, hash: "0x609991ca0ae39bc4eaf2669976237296d40c2f31")
address_hash_string = Hash.to_string(address.hash)
token_contract_address_hash = "0xf7f79032fd395978acb7069c74d21e5a53206559"
contract_address = insert(:address, hash: token_contract_address_hash)
token = insert(:token, contract_address: contract_address)
data = [
%{
token_contract_address_hash: Hash.to_string(token.contract_address_hash),
address_hash: address_hash_string,
block_number: 1_000,
token_id: 5,
token_type: "ERC-1155"
}
]
get_erc1155_balance_from_blockchain()
{:ok, result} = TokenBalances.fetch_token_balances_from_blockchain(data)
assert [
%{
value: 2,
token_contract_address_hash: ^token_contract_address_hash,
address_hash: ^address_hash_string,
block_number: 1_000,
value_fetched_at: _
}
] = result
end
test "fetches multiple balances of tokens" do
address_1 = insert(:address, hash: "0xecba3c9ea993b0e0594e0b0a0d361a1f9596e310")
address_2 = insert(:address, hash: "0x609991ca0ae39bc4eaf2669976237296d40c2f31")
address_3 = insert(:address, hash: "0xf712a82dd8e2ac923299193e9d6daeda2d5a32fd")
address_1_hash_string = Hash.to_string(address_1.hash)
address_2_hash_string = Hash.to_string(address_2.hash)
address_3_hash_string = Hash.to_string(address_3.hash)
token_1_contract_address_hash = "0x57e93bb58268de818b42e3795c97bad58afcd3fe"
token_2_contract_address_hash = "0xe0d0b1dbbcf3dd5cac67edaf9243863fd70745da"
token_3_contract_address_hash = "0x22c1f6050e56d2876009903609a2cc3fef83b415"
token_4_contract_address_hash = "0xf7f79032fd395978acb7069c74d21e5a53206559"
contract_address_1 = insert(:address, hash: token_1_contract_address_hash)
contract_address_2 = insert(:address, hash: token_2_contract_address_hash)
contract_address_3 = insert(:address, hash: token_3_contract_address_hash)
contract_address_4 = insert(:address, hash: token_4_contract_address_hash)
token_1 = insert(:token, contract_address: contract_address_1)
token_2 = insert(:token, contract_address: contract_address_2)
token_3 = insert(:token, contract_address: contract_address_3)
token_4 = insert(:token, contract_address: contract_address_4)
data = [
%{
token_contract_address_hash: Hash.to_string(token_1.contract_address_hash),
address_hash: address_1_hash_string,
block_number: 1_000,
token_id: nil,
token_type: "ERC-20"
},
%{
token_contract_address_hash: Hash.to_string(token_2.contract_address_hash),
address_hash: address_2_hash_string,
block_number: 1_000,
token_id: nil,
token_type: "ERC-20"
},
%{
token_contract_address_hash: Hash.to_string(token_3.contract_address_hash),
address_hash: address_2_hash_string,
block_number: 1_000,
token_id: 42,
token_type: "ERC-721"
},
%{
token_contract_address_hash: Hash.to_string(token_4.contract_address_hash),
address_hash: address_2_hash_string,
block_number: 1_000,
token_id: 5,
token_type: "ERC-1155"
},
%{
token_contract_address_hash: Hash.to_string(token_2.contract_address_hash),
address_hash: Hash.to_string(token_2.contract_address_hash),
block_number: 1_000,
token_id: nil,
token_type: "ERC-20"
},
%{
token_contract_address_hash: Hash.to_string(token_2.contract_address_hash),
address_hash: address_3_hash_string,
block_number: 1_000,
token_id: nil,
token_type: "ERC-20"
},
%{
token_contract_address_hash: Hash.to_string(token_2.contract_address_hash),
address_hash: Hash.to_string(token_2.contract_address_hash),
block_number: 1_000,
token_id: nil,
token_type: "ERC-20"
}
]
get_multiple_balances_from_blockchain()
get_erc1155_balance_from_blockchain()
{:ok, result} = TokenBalances.fetch_token_balances_from_blockchain(data)
assert [
%{
value: 1_000_000_000_000_000_000_000_000,
token_contract_address_hash: ^token_1_contract_address_hash,
address_hash: ^address_1_hash_string,
block_number: 1_000,
value_fetched_at: _
},
%{
value: 3_000_000_000_000_000_000_000_000_000,
token_contract_address_hash: ^token_2_contract_address_hash,
address_hash: ^address_2_hash_string,
block_number: 1_000,
value_fetched_at: _
},
%{
value: 1,
token_contract_address_hash: ^token_3_contract_address_hash,
address_hash: ^address_2_hash_string,
block_number: 1_000,
value_fetched_at: _
},
%{
value: 6_000_000_000_000_000_000_000_000_000,
token_contract_address_hash: ^token_2_contract_address_hash,
address_hash: ^token_2_contract_address_hash,
block_number: 1_000,
value_fetched_at: _
},
%{
value: 5_000_000_000_000_000_000_000_000_000,
token_contract_address_hash: ^token_2_contract_address_hash,
address_hash: ^address_3_hash_string,
block_number: 1_000,
value_fetched_at: _
},
%{
value: 6_000_000_000_000_000_000_000_000_000,
token_contract_address_hash: ^token_2_contract_address_hash,
address_hash: ^token_2_contract_address_hash,
block_number: 1_000,
value_fetched_at: _
},
%{
value: 2,
token_contract_address_hash: ^token_4_contract_address_hash,
address_hash: ^address_2_hash_string,
block_number: 1_000,
value_fetched_at: _
}
] = result
end
test "ignores calls that gave errors to try fetch they again later" do
address = insert(:address, hash: "0x7113ffcb9c18a97da1b9cfc43e6cb44ed9165509")
token = insert(:token, contract_address: build(:contract_address))
@ -54,7 +228,9 @@ defmodule Indexer.TokenBalancesTest do
address_hash: to_string(address.hash),
block_number: 1_000,
token_contract_address_hash: to_string(token.contract_address_hash),
retries_count: 1
retries_count: 1,
token_id: 11,
token_type: "ERC-20"
}
]
@ -128,12 +304,14 @@ defmodule Indexer.TokenBalancesTest do
token_balance_a = %{
token_contract_address_hash: Hash.to_string(token.contract_address_hash),
token_id: nil,
address_hash: address_hash_string,
block_number: 1_000
}
token_balance_b = %{
token_contract_address_hash: Hash.to_string(token.contract_address_hash),
token_id: nil,
address_hash: address_hash_string,
block_number: 1_001
}
@ -162,6 +340,129 @@ defmodule Indexer.TokenBalancesTest do
)
end
defp get_erc1155_balance_from_blockchain() do
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn requests, _options ->
{:ok,
requests
|> Enum.map(fn
%{
id: id,
method: "eth_call",
params: [
%{
data:
"0x00fdd58e000000000000000000000000609991ca0ae39bc4eaf2669976237296d40c2f310000000000000000000000000000000000000000000000000000000000000005",
to: "0xf7f79032fd395978acb7069c74d21e5a53206559"
},
_
]
} ->
%{
id: id,
jsonrpc: "2.0",
result: "0x0000000000000000000000000000000000000000000000000000000000000002"
}
req ->
IO.inspect("Gimme req")
IO.inspect(req)
end)
|> Enum.shuffle()}
end
)
end
defp get_multiple_balances_from_blockchain() do
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn requests, _options ->
{:ok,
requests
|> Enum.map(fn
%{id: id, method: "eth_call", params: [%{data: _, to: "0x57e93bb58268de818b42e3795c97bad58afcd3fe"}, _]} ->
%{
id: id,
jsonrpc: "2.0",
result: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000"
}
%{
id: id,
method: "eth_call",
params: [
%{
data: "0x70a08231000000000000000000000000609991ca0ae39bc4eaf2669976237296d40c2f31",
to: "0xe0d0b1dbbcf3dd5cac67edaf9243863fd70745da"
},
_
]
} ->
%{
id: id,
jsonrpc: "2.0",
result: "0x000000000000000000000000000000000000000009b18ab5df7180b6b8000000"
}
%{
id: id,
method: "eth_call",
params: [
%{
data: "0x70a08231000000000000000000000000609991ca0ae39bc4eaf2669976237296d40c2f31",
to: "0x22c1f6050e56d2876009903609a2cc3fef83b415"
},
_
]
} ->
%{
id: id,
jsonrpc: "2.0",
result: "0x0000000000000000000000000000000000000000000000000000000000000001"
}
%{
id: id,
method: "eth_call",
params: [
%{
data: "0x70a08231000000000000000000000000f712a82dd8e2ac923299193e9d6daeda2d5a32fd",
to: "0xe0d0b1dbbcf3dd5cac67edaf9243863fd70745da"
},
_
]
} ->
%{
id: id,
jsonrpc: "2.0",
result: "0x00000000000000000000000000000000000000001027e72f1f12813088000000"
}
%{
id: id,
method: "eth_call",
params: [
%{
data: "0x70a08231000000000000000000000000e0d0b1dbbcf3dd5cac67edaf9243863fd70745da",
to: "0xe0d0b1dbbcf3dd5cac67edaf9243863fd70745da"
},
_
]
} ->
%{
id: id,
jsonrpc: "2.0",
result: "0x00000000000000000000000000000000000000001363156bbee3016d70000000"
}
end)
|> Enum.shuffle()}
end
)
end
defp get_balance_from_blockchain_with_error() do
expect(
EthereumJSONRPC.Mox,

@ -24,7 +24,9 @@ defmodule Indexer.Transform.AddressTokenBalancesTest do
block_number: block_number,
from_address_hash: from_address_hash,
to_address_hash: to_address_hash,
token_contract_address_hash: token_contract_address_hash
token_contract_address_hash: token_contract_address_hash,
token_id: nil,
token_type: "ERC-20"
}
params_set = AddressTokenBalances.params_set(%{token_transfers_params: [token_transfer_params]})
@ -46,7 +48,8 @@ defmodule Indexer.Transform.AddressTokenBalancesTest do
from_address_hash: from_address_hash,
to_address_hash: to_address_hash,
token_contract_address_hash: token_contract_address_hash,
token_type: "ERC-721"
token_type: "ERC-721",
token_id: nil
}
params_set = AddressTokenBalances.params_set(%{token_transfers_params: [token_transfer_params]})
@ -56,7 +59,9 @@ defmodule Indexer.Transform.AddressTokenBalancesTest do
%{
address_hash: "0x5b8410f67eb8040bb1cd1e8a4ff9d5f6ce678a15",
block_number: 1,
token_contract_address_hash: "0xe18035bf8712672935fdb4e5e431b1a0183d2dfc"
token_contract_address_hash: "0xe18035bf8712672935fdb4e5e431b1a0183d2dfc",
token_id: nil,
token_type: "ERC-721"
}
])
end

@ -74,6 +74,7 @@ defmodule Indexer.Transform.TokenTransfersTest do
block_hash: log_3.block_hash
},
%{
token_id: nil,
amount: Decimal.new(17_000_000_000_000_000_000),
block_number: log_1.block_number,
log_index: log_1.index,
@ -131,6 +132,83 @@ defmodule Indexer.Transform.TokenTransfersTest do
assert TokenTransfers.parse([log]) == expected
end
test "parses erc1155 token transfer" do
log = %{
address_hash: "0x58Ab73CB79c8275628E0213742a85B163fE0A9Fb",
block_number: 8_683_457,
data:
"0x1000000000000c520000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001",
first_topic: "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62",
secon_topic: "0x0000000000000000000000009c978f4cfa1fe13406bcc05baf26a35716f881dd",
third_topic: "0x0000000000000000000000009c978f4cfa1fe13406bcc05baf26a35716f881dd",
fourth_topic: "0x0000000000000000000000009c978f4cfa1fe13406bcc05baf26a35716f881dd",
index: 2,
transaction_hash: "0x6d2dd62c178e55a13b65601f227c4ffdd8aa4e3bcb1f24731363b4f7619e92c8",
block_hash: "0x79594150677f083756a37eee7b97ed99ab071f502104332cb3835bac345711ca",
type: "mined"
}
assert TokenTransfers.parse([log]) == %{
token_transfers: [
%{
amount: 1,
block_hash: "0x79594150677f083756a37eee7b97ed99ab071f502104332cb3835bac345711ca",
block_number: 8_683_457,
from_address_hash: "0x9c978f4cfa1fe13406bcc05baf26a35716f881dd",
log_index: 2,
to_address_hash: "0x9c978f4cfa1fe13406bcc05baf26a35716f881dd",
token_contract_address_hash: "0x58Ab73CB79c8275628E0213742a85B163fE0A9Fb",
token_id:
7_237_005_577_332_282_011_952_059_972_634_123_378_909_214_838_582_411_639_295_170_840_059_424_276_480,
token_type: "ERC-1155",
transaction_hash: "0x6d2dd62c178e55a13b65601f227c4ffdd8aa4e3bcb1f24731363b4f7619e92c8"
}
],
tokens: [
%{
contract_address_hash: "0x58Ab73CB79c8275628E0213742a85B163fE0A9Fb",
type: "ERC-1155"
}
]
}
end
test "parses erc1155 batch token transfer" do
log = %{
address_hash: "0x58Ab73CB79c8275628E0213742a85B163fE0A9Fb",
block_number: 8_683_457,
data:
"0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000001388",
first_topic: "0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb",
secon_topic: "0x0000000000000000000000006c943470780461b00783ad530a53913bd2c104d3",
third_topic: "0x0000000000000000000000006c943470780461b00783ad530a53913bd2c104d3",
fourth_topic: "0x0000000000000000000000006c943470780461b00783ad530a53913bd2c104d3",
index: 2,
transaction_hash: "0x6d2dd62c178e55a13b65601f227c4ffdd8aa4e3bcb1f24731363b4f7619e92c8",
block_hash: "0x79594150677f083756a37eee7b97ed99ab071f502104332cb3835bac345711ca",
type: "mined"
}
assert TokenTransfers.parse([log]) == %{
token_transfers: [
%{
block_hash: "0x79594150677f083756a37eee7b97ed99ab071f502104332cb3835bac345711ca",
block_number: 8_683_457,
from_address_hash: "0x6c943470780461b00783ad530a53913bd2c104d3",
log_index: 2,
to_address_hash: "0x6c943470780461b00783ad530a53913bd2c104d3",
token_contract_address_hash: "0x58Ab73CB79c8275628E0213742a85B163fE0A9Fb",
token_id: nil,
token_ids: [680_564_733_841_876_926_926_749_214_863_536_422_912],
token_type: "ERC-1155",
transaction_hash: "0x6d2dd62c178e55a13b65601f227c4ffdd8aa4e3bcb1f24731363b4f7619e92c8",
amounts: [5000]
}
],
tokens: [%{contract_address_hash: "0x58Ab73CB79c8275628E0213742a85B163fE0A9Fb", type: "ERC-1155"}]
}
end
test "logs error with unrecognized token transfer format" do
log = %{
address_hash: "0x58Ab73CB79c8275628E0213742a85B163fE0A9Fb",

Loading…
Cancel
Save