ERC-404 basic support (#9407)

* ERC-404 basic support

* rename nft_token_ to nft_

* ERC-404 support additions

* Cover with token transfer parsing tests

* Cover ERC-404 with token balance tests

* Cover ERC-404 with current token balance tests

* Notification summary tests

* Some more tests

* Update apps/block_scout_web/lib/block_scout_web/views/tokens/helper.ex

Co-authored-by: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com>

* Process review comments

* Process review comment

* Format changes

---------

Co-authored-by: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com>
pull/9264/merge
Victor Baranov 8 months ago committed by GitHub
parent dea361d56f
commit ee3b9c2c28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 1
      apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex
  3. 22
      apps/block_scout_web/lib/block_scout_web/controllers/account/api/v1/user_controller.ex
  4. 10
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex
  5. 6
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex
  6. 2
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex
  7. 2
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/holder_controller.ex
  8. 2
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/metadata_controller.ex
  9. 2
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/transfer_controller.ex
  10. 12
      apps/block_scout_web/lib/block_scout_web/paging_helper.ex
  11. 9
      apps/block_scout_web/lib/block_scout_web/views/account/api/v1/user_view.ex
  12. 11
      apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex
  13. 2
      apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex
  14. 31
      apps/block_scout_web/lib/block_scout_web/views/tokens/helper.ex
  15. 10
      apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex
  16. 1
      apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex
  17. 1
      apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex
  18. 125
      apps/block_scout_web/priv/gettext/default.pot
  19. 142
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  20. 4
      apps/block_scout_web/test/block_scout_web/controllers/account/api/v1/user_controller_test.exs
  21. 31
      apps/block_scout_web/test/block_scout_web/controllers/address_token_controller_test.exs
  22. 36
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs
  23. 85
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs
  24. 3
      apps/explorer/lib/explorer/account/notifier/email.ex
  25. 3
      apps/explorer/lib/explorer/account/notifier/notify.ex
  26. 16
      apps/explorer/lib/explorer/account/notifier/summary.ex
  27. 4
      apps/explorer/lib/explorer/account/watchlist_address.ex
  28. 39
      apps/explorer/lib/explorer/chain.ex
  29. 6
      apps/explorer/lib/explorer/chain/address/token_balance.ex
  30. 3
      apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex
  31. 9
      apps/explorer/lib/explorer/chain/import/runner/address/token_balances.ex
  32. 2
      apps/explorer/lib/explorer/chain/shibarium/bridge.ex
  33. 2
      apps/explorer/lib/explorer/chain/token.ex
  34. 83
      apps/explorer/lib/explorer/chain/token/instance.ex
  35. 6
      apps/explorer/lib/explorer/chain/token_transfer.ex
  36. 14
      apps/explorer/lib/explorer/etherscan.ex
  37. 6
      apps/explorer/lib/explorer/token/balance_reader.ex
  38. 10
      apps/explorer/priv/account/migrations/20240219152220_add_account_watchlist_addresses_erc_404_fields.exs
  39. 108
      apps/explorer/test/explorer/account/notifier/summary_test.exs
  40. 45
      apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs
  41. 6
      apps/explorer/test/explorer/chain_test.exs
  42. 17
      apps/explorer/test/support/factory.ex
  43. 14
      apps/indexer/lib/indexer/fetcher/token_balance_on_demand.ex
  44. 2
      apps/indexer/lib/indexer/fetcher/token_instance/metadata_retriever.ex
  45. 36
      apps/indexer/lib/indexer/token_balances.ex
  46. 5
      apps/indexer/lib/indexer/transform/address_token_balances.ex
  47. 17
      apps/indexer/lib/indexer/transform/token_instances.ex
  48. 112
      apps/indexer/lib/indexer/transform/token_transfers.ex
  49. 28
      apps/indexer/test/indexer/fetcher/token_balance_test.exs
  50. 118
      apps/indexer/test/indexer/token_balances_test.exs
  51. 86
      apps/indexer/test/indexer/transform/token_transfers_test.exs

@ -112,6 +112,7 @@
- [#9441](https://github.com/blockscout/blockscout/pull/9441) - Update BENS integration: change endpoint for resolving address in search
- [#9437](https://github.com/blockscout/blockscout/pull/9437) - Add Enum.uniq before sanitizing token transfers
- [#9407](https://github.com/blockscout/blockscout/pull/9407) - ERC-404 basic support
- [#9403](https://github.com/blockscout/blockscout/pull/9403) - Null round handling
- [#9401](https://github.com/blockscout/blockscout/pull/9401) - Eliminate incorrect token transfers with empty token_ids
- [#9396](https://github.com/blockscout/blockscout/pull/9396) - More-Minimal Proxy support

@ -241,6 +241,7 @@ defmodule BlockScoutWeb.AddressChannel do
push_current_token_balances(socket, address_current_token_balances, "erc_20", "ERC-20")
push_current_token_balances(socket, address_current_token_balances, "erc_721", "ERC-721")
push_current_token_balances(socket, address_current_token_balances, "erc_1155", "ERC-1155")
push_current_token_balances(socket, address_current_token_balances, "erc_404", "ERC-404")
{:noreply, socket}
end

@ -146,12 +146,15 @@ defmodule BlockScoutWeb.Account.Api.V1.UserController do
"ERC-721" => %{
"incoming" => watch_erc_721_input,
"outcoming" => watch_erc_721_output
}
# ,
},
# "ERC-1155" => %{
# "incoming" => watch_erc_1155_input,
# "outcoming" => watch_erc_1155_output
# }
# },
"ERC-404" => %{
"incoming" => watch_erc_404_input,
"outcoming" => watch_erc_404_output
}
},
"notification_methods" => %{
"email" => notify_email
@ -167,6 +170,8 @@ defmodule BlockScoutWeb.Account.Api.V1.UserController do
watch_erc_721_output: watch_erc_721_output,
watch_erc_1155_input: watch_erc_721_input,
watch_erc_1155_output: watch_erc_721_output,
watch_erc_404_input: watch_erc_404_input,
watch_erc_404_output: watch_erc_404_output,
notify_email: notify_email,
address_hash: address_hash
}
@ -202,12 +207,15 @@ defmodule BlockScoutWeb.Account.Api.V1.UserController do
"ERC-721" => %{
"incoming" => watch_erc_721_input,
"outcoming" => watch_erc_721_output
}
# ,
},
# "ERC-1155" => %{
# "incoming" => watch_erc_1155_input,
# "outcoming" => watch_erc_1155_output
# }
# },
"ERC-404" => %{
"incoming" => watch_erc_404_input,
"outcoming" => watch_erc_404_output
}
},
"notification_methods" => %{
"email" => notify_email
@ -224,6 +232,8 @@ defmodule BlockScoutWeb.Account.Api.V1.UserController do
watch_erc_721_output: watch_erc_721_output,
watch_erc_1155_input: watch_erc_721_input,
watch_erc_1155_output: watch_erc_721_output,
watch_erc_404_input: watch_erc_404_input,
watch_erc_404_output: watch_erc_404_output,
notify_email: notify_email,
address_hash: address_hash
}

@ -200,7 +200,7 @@ defmodule BlockScoutWeb.API.RPC.AddressController do
{:contract_address, to_address_hash_optional(params["contractaddress"])},
true <- !is_nil(address_hash) or !is_nil(contract_address_hash),
{:ok, token_transfers, max_block_number} <-
list_nft_token_transfers(address_hash, contract_address_hash, options) do
list_nft_transfers(address_hash, contract_address_hash, options) do
render(conn, :tokennfttx, %{token_transfers: token_transfers, max_block_number: max_block_number})
else
false ->
@ -531,10 +531,10 @@ defmodule BlockScoutWeb.API.RPC.AddressController do
end
end
defp list_nft_token_transfers(nil, contract_address_hash, options) do
defp list_nft_transfers(nil, contract_address_hash, options) do
with {:ok, max_block_number} <- Chain.max_consensus_block_number(),
token_transfers when token_transfers != [] <-
Etherscan.list_nft_token_transfers_by_token(contract_address_hash, options) do
Etherscan.list_nft_transfers_by_token(contract_address_hash, options) do
{:ok, token_transfers, max_block_number}
else
_ ->
@ -542,11 +542,11 @@ defmodule BlockScoutWeb.API.RPC.AddressController do
end
end
defp list_nft_token_transfers(address_hash, contract_address_hash, options) do
defp list_nft_transfers(address_hash, contract_address_hash, options) do
with {:address, :ok} <- {:address, Address.check_address_exists(address_hash, @api_true)},
{:ok, max_block_number} <- Chain.max_consensus_block_number(),
token_transfers when token_transfers != [] <-
Etherscan.list_nft_token_transfers(address_hash, contract_address_hash, options) do
Etherscan.list_nft_transfers(address_hash, contract_address_hash, options) do
{:ok, token_transfers, max_block_number}
else
_ ->

@ -17,7 +17,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do
delete_parameters_from_next_page_params: 1,
token_transfers_types_options: 1,
address_transactions_sorting: 1,
nft_token_types_options: 1
nft_types_options: 1
]
import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens: 1, maybe_preload_ens_to_address: 1]
@ -449,7 +449,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do
address_hash,
params
|> paging_options()
|> Keyword.merge(nft_token_types_options(params))
|> Keyword.merge(nft_types_options(params))
|> Keyword.merge(@api_true)
|> Keyword.merge(@nft_necessity_by_association)
)
@ -477,7 +477,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do
address_hash,
params
|> paging_options()
|> Keyword.merge(nft_token_types_options(params))
|> Keyword.merge(nft_types_options(params))
|> Keyword.merge(@api_true)
|> Keyword.merge(@nft_necessity_by_association)
)

@ -189,7 +189,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do
{:not_found, false} <- {:not_found, Chain.erc_20_token?(token)},
{:format, {token_id, ""}} <- {:format, Integer.parse(token_id_str)} do
token_instance =
case Chain.erc721_or_erc1155_token_instance_from_token_id_and_token_address(token_id, address_hash, @api_true) do
case Chain.nft_instance_from_token_id_and_token_address(token_id, address_hash, @api_true) do
{:ok, token_instance} ->
token_instance
|> Chain.select_repo(@api_true).preload(:owner)

@ -60,7 +60,7 @@ defmodule BlockScoutWeb.Tokens.Instance.HolderController do
{:ok, token} <- Chain.token_from_address_hash(hash, options),
false <- Chain.erc_20_token?(token),
{token_id, ""} <- Integer.parse(token_id_str) do
case Chain.erc721_or_erc1155_token_instance_from_token_id_and_token_address(token_id, hash) do
case Chain.nft_instance_from_token_id_and_token_address(token_id, hash) do
{:ok, token_instance} -> Helper.render(conn, token_instance, hash, token_id, token)
{:error, :not_found} -> Helper.render(conn, nil, hash, token_id, token)
end

@ -12,7 +12,7 @@ defmodule BlockScoutWeb.Tokens.Instance.MetadataController do
false <- Chain.erc_20_token?(token),
{token_id, ""} <- Integer.parse(token_id_str),
{:ok, token_instance} <-
Chain.erc721_or_erc1155_token_instance_from_token_id_and_token_address(token_id, hash) do
Chain.nft_instance_from_token_id_and_token_address(token_id, hash) do
if token_instance.metadata do
Helper.render(conn, token_instance, hash, token_id, token)
else

@ -63,7 +63,7 @@ defmodule BlockScoutWeb.Tokens.Instance.TransferController do
{:ok, token} <- Chain.token_from_address_hash(hash, options),
false <- Chain.erc_20_token?(token),
{token_id, ""} <- Integer.parse(token_id_str) do
case Chain.erc721_or_erc1155_token_instance_from_token_id_and_token_address(token_id, hash) do
case Chain.nft_instance_from_token_id_and_token_address(token_id, hash) do
{:ok, token_instance} -> Helper.render(conn, token_instance, hash, token_id, token)
{:error, :not_found} -> Helper.render(conn, nil, hash, token_id, token)
end

@ -32,8 +32,8 @@ defmodule BlockScoutWeb.PagingHelper do
]
end
@allowed_token_transfer_type_labels ["ERC-20", "ERC-721", "ERC-1155"]
@allowed_nft_token_type_labels ["ERC-721", "ERC-1155"]
@allowed_token_transfer_type_labels ["ERC-20", "ERC-721", "ERC-1155", "ERC-404"]
@allowed_nft_type_labels ["ERC-721", "ERC-1155", "ERC-404"]
@allowed_chain_id [1, 56, 99]
@allowed_stability_validators_states ["active", "probation", "inactive"]
@ -80,14 +80,14 @@ defmodule BlockScoutWeb.PagingHelper do
@doc """
Parse 'type' query parameter from request option map
"""
@spec nft_token_types_options(map()) :: [{:token_type, list}]
def nft_token_types_options(%{"type" => filters}) do
@spec nft_types_options(map()) :: [{:token_type, list}]
def nft_types_options(%{"type" => filters}) do
[
token_type: filters_to_list(filters, @allowed_nft_token_type_labels)
token_type: filters_to_list(filters, @allowed_nft_type_labels)
]
end
def nft_token_types_options(_), do: [token_type: []]
def nft_types_options(_), do: [token_type: []]
defp filters_to_list(filters, allowed, variant \\ :upcase)
defp filters_to_list(filters, allowed, :downcase), do: filters |> String.downcase() |> parse_filter(allowed)

@ -112,12 +112,15 @@ defmodule BlockScoutWeb.Account.Api.V1.UserView do
"ERC-721" => %{
"incoming" => watchlist.watch_erc_721_input,
"outcoming" => watchlist.watch_erc_721_output
}
# ,
},
# "ERC-1155" => %{
# "incoming" => watchlist.watch_erc_1155_input,
# "outcoming" => watchlist.watch_erc_1155_output
# }
# },
"ERC-404" => %{
"incoming" => watchlist.watch_erc_404_input,
"outcoming" => watchlist.watch_erc_404_output
}
},
"notification_methods" => %{
"email" => watchlist.notify_email

@ -44,7 +44,7 @@ defmodule BlockScoutWeb.API.RPC.AddressView do
end
def render("tokennfttx.json", %{token_transfers: token_transfers, max_block_number: max_block_number}) do
data = Enum.map(token_transfers, &prepare_nft_token_transfer(&1, max_block_number))
data = Enum.map(token_transfers, &prepare_nft_transfer(&1, max_block_number))
RPCView.render("show.json", data: data)
end
@ -192,6 +192,13 @@ defmodule BlockScoutWeb.API.RPC.AddressView do
|> Map.put_new(:values, token_transfer.amounts)
end
defp prepare_token_transfer(%{token_type: "ERC-404"} = token_transfer) do
token_transfer
|> prepare_common_token_transfer()
|> Map.put_new(:tokenIDs, token_transfer.token_ids)
|> Map.put_new(:values, token_transfer.amounts)
end
defp prepare_token_transfer(%{token_type: "ERC-20"} = token_transfer) do
token_transfer
|> prepare_common_token_transfer()
@ -202,7 +209,7 @@ defmodule BlockScoutWeb.API.RPC.AddressView do
prepare_common_token_transfer(token_transfer)
end
defp prepare_nft_token_transfer(token_transfer, max_block_number) do
defp prepare_nft_transfer(token_transfer, max_block_number) do
%{
"blockNumber" => to_string(token_transfer.block_number),
"timeStamp" => to_string(DateTime.to_unix(token_transfer.block.timestamp)),

@ -230,7 +230,7 @@ defmodule BlockScoutWeb.API.V2.AddressView do
) :: map()
def fetch_and_render_token_instance(token_id, token, address_hash, token_balance) do
token_instance =
case Chain.erc721_or_erc1155_token_instance_from_token_id_and_token_address(
case Chain.nft_instance_from_token_id_and_token_address(
token_id,
token.contract_address_hash,
@api_true

@ -31,29 +31,33 @@ defmodule BlockScoutWeb.Tokens.Helper do
end
# TODO: remove this clause along with token transfer denormalization
defp do_token_transfer_amount(%Token{type: "ERC-20"}, nil, nil, nil, _token_ids) do
defp do_token_transfer_amount(%Token{type: type}, nil, nil, nil, _token_ids) when type in ["ERC-20", "ERC-404"] do
{:ok, "--"}
end
defp do_token_transfer_amount(_token, "ERC-20", nil, nil, _token_ids) do
defp do_token_transfer_amount(_token, type, nil, nil, _token_ids) when type in ["ERC-20", "ERC-404"] do
{:ok, "--"}
end
# TODO: remove this clause along with token transfer denormalization
defp do_token_transfer_amount(%Token{type: "ERC-20", decimals: nil}, nil, amount, _amounts, _token_ids) do
defp do_token_transfer_amount(%Token{type: type, decimals: nil}, nil, amount, _amounts, _token_ids)
when type in ["ERC-20", "ERC-404"] do
{:ok, CurrencyHelper.format_according_to_decimals(amount, Decimal.new(0))}
end
defp do_token_transfer_amount(%Token{decimals: nil}, "ERC-20", amount, _amounts, _token_ids) do
defp do_token_transfer_amount(%Token{decimals: nil}, type, amount, _amounts, _token_ids)
when type in ["ERC-20", "ERC-404"] do
{:ok, CurrencyHelper.format_according_to_decimals(amount, Decimal.new(0))}
end
# TODO: remove this clause along with token transfer denormalization
defp do_token_transfer_amount(%Token{type: "ERC-20", decimals: decimals}, nil, amount, _amounts, _token_ids) do
defp do_token_transfer_amount(%Token{type: type, decimals: decimals}, nil, amount, _amounts, _token_ids)
when type in ["ERC-20", "ERC-404"] do
{:ok, CurrencyHelper.format_according_to_decimals(amount, decimals)}
end
defp do_token_transfer_amount(%Token{decimals: decimals}, "ERC-20", amount, _amounts, _token_ids) do
defp do_token_transfer_amount(%Token{decimals: decimals}, type, amount, _amounts, _token_ids)
when type in ["ERC-20", "ERC-404"] do
{:ok, CurrencyHelper.format_according_to_decimals(amount, decimals)}
end
@ -102,32 +106,35 @@ defmodule BlockScoutWeb.Tokens.Helper do
end
# TODO: remove this clause along with token transfer denormalization
defp do_token_transfer_amount_for_api(%Token{type: "ERC-20"}, nil, nil, nil, _token_ids) do
defp do_token_transfer_amount_for_api(%Token{type: type}, nil, nil, nil, _token_ids)
when type in ["ERC-20", "ERC-404"] do
{:ok, nil}
end
defp do_token_transfer_amount_for_api(_token, "ERC-20", nil, nil, _token_ids) do
defp do_token_transfer_amount_for_api(_token, type, nil, nil, _token_ids) when type in ["ERC-20", "ERC-404"] do
{:ok, nil}
end
# TODO: remove this clause along with token transfer denormalization
defp do_token_transfer_amount_for_api(
%Token{type: "ERC-20", decimals: decimals},
%Token{type: type, decimals: decimals},
nil,
amount,
_amounts,
_token_ids
) do
)
when type in ["ERC-20", "ERC-404"] do
{:ok, amount, decimals}
end
defp do_token_transfer_amount_for_api(
%Token{decimals: decimals},
"ERC-20",
type,
amount,
_amounts,
_token_ids
) do
)
when type in ["ERC-20", "ERC-404"] do
{:ok, amount, decimals}
end

@ -70,6 +70,16 @@ defmodule BlockScoutWeb.Tokens.HolderView do
to_string(format_according_to_decimals(value, decimals)) <> " TokenID " <> to_string(id)
end
def format_token_balance_value(value, id, %Token{type: "ERC-404", decimals: decimals}) do
base = to_string(format_according_to_decimals(value, decimals))
if id do
base <> " TokenID " <> to_string(id)
else
base
end
end
def format_token_balance_value(value, _id, _token) do
value
end

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

@ -223,6 +223,7 @@ defmodule BlockScoutWeb.TransactionView do
:erc20 -> gettext("ERC-20 ")
:erc721 -> gettext("ERC-721 ")
:erc1155 -> gettext("ERC-1155 ")
:erc404 -> gettext("ERC-404 ")
_ -> ""
end
end

@ -1,16 +1,15 @@
#: lib/block_scout_web/views/address_token_balance_view.ex:10
#, elixir-autogen, elixir-format
msgid "%{count} token"
msgid_plural "%{count} tokens"
msgstr[0] ""
msgstr[1] ""
#: lib/block_scout_web/templates/block/_tile.html.eex:29
#, elixir-autogen, elixir-format
msgid "%{count} transaction"
msgid_plural "%{count} transactions"
msgstr[0] ""
msgstr[1] ""
## This file is a PO Template file.
##
## "msgid"s here are often extracted from source code.
## Add new messages manually only if they're dynamic
## messages that can't be statically extracted.
##
## Run "mix gettext.extract" to bring this file up to
## date. Leave "msgstr"s empty as changing them here has no
## effect: edit them in PO (.po) files instead.
#
msgid ""
msgstr ""
#: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:9
#, elixir-autogen, elixir-format
@ -53,6 +52,20 @@ msgstr ""
msgid "%{count} Transactions"
msgstr ""
#: lib/block_scout_web/views/address_token_balance_view.ex:10
#, elixir-autogen, elixir-format
msgid "%{count} token"
msgid_plural "%{count} tokens"
msgstr[0] ""
msgstr[1] ""
#: lib/block_scout_web/templates/block/_tile.html.eex:29
#, elixir-autogen, elixir-format
msgid "%{count} transaction"
msgid_plural "%{count} transactions"
msgstr[0] ""
msgstr[1] ""
#: lib/block_scout_web/templates/transaction/_actions.html.eex:101
#, elixir-autogen, elixir-format
msgid "%{qty} of <span class=\"text-muted\">Token ID [%{link_to_id}]</span>"
@ -68,7 +81,7 @@ msgstr ""
msgid "%{subnetwork} Explorer - BlockScout"
msgstr ""
#: lib/block_scout_web/views/transaction_view.ex:374
#: lib/block_scout_web/views/transaction_view.ex:375
#, elixir-autogen, elixir-format
msgid "(Awaiting internal transactions for status)"
msgstr ""
@ -671,7 +684,7 @@ msgstr ""
msgid "Compiler version"
msgstr ""
#: lib/block_scout_web/views/transaction_view.ex:367
#: lib/block_scout_web/views/transaction_view.ex:368
#, elixir-autogen, elixir-format
msgid "Confirmed"
msgstr ""
@ -1265,12 +1278,12 @@ msgstr ""
msgid "Error trying to fetch balances."
msgstr ""
#: lib/block_scout_web/views/transaction_view.ex:378
#: lib/block_scout_web/views/transaction_view.ex:379
#, elixir-autogen, elixir-format
msgid "Error: %{reason}"
msgstr ""
#: lib/block_scout_web/views/transaction_view.ex:376
#: lib/block_scout_web/views/transaction_view.ex:377
#, elixir-autogen, elixir-format
msgid "Error: (Awaiting internal transactions for reason)"
msgstr ""
@ -1302,6 +1315,11 @@ msgstr ""
msgid "Expand"
msgstr ""
#: lib/block_scout_web/templates/csv_export/index.html.eex:14
#, elixir-autogen, elixir-format
msgid "Export"
msgstr ""
#: lib/block_scout_web/templates/csv_export/index.html.eex:10
#, elixir-autogen, elixir-format
msgid "Export Data"
@ -1708,7 +1726,7 @@ msgstr ""
msgid "Max Priority Fee per Gas"
msgstr ""
#: lib/block_scout_web/views/transaction_view.ex:330
#: lib/block_scout_web/views/transaction_view.ex:331
#, elixir-autogen, elixir-format
msgid "Max of"
msgstr ""
@ -1855,6 +1873,16 @@ msgstr ""
msgid "New Smart Contract Verification"
msgstr ""
#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:9
#, elixir-autogen, elixir-format
msgid "New Smart Contract Verification via Standard input JSON"
msgstr ""
#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:5
#, elixir-autogen, elixir-format
msgid "New Smart Contract Verification via metadata JSON"
msgstr ""
#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:9
#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:7
#, elixir-autogen, elixir-format
@ -2487,7 +2515,7 @@ msgid "Submit an Issue"
msgstr ""
#: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:8
#: lib/block_scout_web/views/transaction_view.ex:375
#: lib/block_scout_web/views/transaction_view.ex:376
#, elixir-autogen, elixir-format
msgid "Success"
msgstr ""
@ -3114,7 +3142,7 @@ msgstr ""
msgid "Uncles"
msgstr ""
#: lib/block_scout_web/views/transaction_view.ex:366
#: lib/block_scout_web/views/transaction_view.ex:367
#, elixir-autogen, elixir-format
msgid "Unconfirmed"
msgstr ""
@ -3461,6 +3489,12 @@ msgstr ""
msgid "Your request contained an error, perhaps a mistyped tx/block/address hash. Try again, and check the developer tools console for more info."
msgstr ""
#: lib/block_scout_web/templates/verified_contracts/index.html.eex:38
#: lib/block_scout_web/views/verified_contracts_view.ex:12
#, elixir-autogen, elixir-format
msgid "Yul"
msgstr ""
#: lib/block_scout_web/templates/address/overview.html.eex:111
#, elixir-autogen, elixir-format
msgid "at"
@ -3471,6 +3505,16 @@ msgstr ""
msgid "balance of the address"
msgstr ""
#: lib/block_scout_web/templates/transaction/overview.html.eex:437
#, elixir-autogen, elixir-format
msgid "burnt for this transaction. Equals Block Base Fee per Gas * Gas Used."
msgstr ""
#: lib/block_scout_web/templates/block/overview.html.eex:215
#, elixir-autogen, elixir-format
msgid "burnt from transactions included in the block (Base fee (per unit of gas) * Gas Used)."
msgstr ""
#: lib/block_scout_web/templates/address_contract/index.html.eex:27
#, elixir-autogen, elixir-format
msgid "button"
@ -3506,6 +3550,11 @@ msgstr ""
msgid "false"
msgstr ""
#: lib/block_scout_web/templates/csv_export/index.html.eex:14
#, elixir-autogen, elixir-format
msgid "for address"
msgstr ""
#: lib/block_scout_web/templates/address_logs/_logs.html.eex:10
#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:12
#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:10
@ -3557,6 +3606,11 @@ msgstr ""
msgid "string"
msgstr ""
#: lib/block_scout_web/templates/csv_export/index.html.eex:17
#, elixir-autogen, elixir-format
msgid "to CSV file"
msgstr ""
#: lib/block_scout_web/views/address_contract_view.ex:29
#, elixir-autogen, elixir-format
msgid "true"
@ -3681,37 +3735,6 @@ msgstr ""
msgid "%{withdrawals_count} withdrawals processed and %{withdrawals_sum} withdrawn."
msgstr ""
#: lib/block_scout_web/templates/csv_export/index.html.eex:14
#, elixir-autogen, elixir-format
msgid "Export"
msgstr ""
#: lib/block_scout_web/templates/csv_export/index.html.eex:14
#, elixir-autogen, elixir-format
msgid "for address"
msgstr ""
#: lib/block_scout_web/templates/csv_export/index.html.eex:17
#, elixir-autogen, elixir-format
msgid "to CSV file"
msgstr ""
#: lib/block_scout_web/templates/verified_contracts/index.html.eex:38
#: lib/block_scout_web/views/verified_contracts_view.ex:12
#, elixir-autogen, elixir-format
msgid "Yul"
msgstr ""
#: lib/block_scout_web/templates/transaction/overview.html.eex:469
#, elixir-autogen, elixir-format
msgid "burnt for this transaction. Equals Block Base Fee per Gas * Gas Used."
msgstr ""
#: lib/block_scout_web/templates/block/overview.html.eex:215
#, elixir-autogen, elixir-format
msgid "burnt from transactions included in the block (Base fee (per unit of gas) * Gas Used)."
msgstr ""
#: lib/block_scout_web/templates/transaction/overview.html.eex:488
#, elixir-autogen, elixir-format
msgid "Actual gas amount used by the transaction."

@ -1,16 +1,15 @@
#: lib/block_scout_web/views/address_token_balance_view.ex:10
#, elixir-autogen, elixir-format
msgid "%{count} token"
msgid_plural "%{count} tokens"
msgstr[0] ""
msgstr[1] ""
#: lib/block_scout_web/templates/block/_tile.html.eex:29
#, elixir-autogen, elixir-format
msgid "%{count} transaction"
msgid_plural "%{count} transactions"
msgstr[0] ""
msgstr[1] ""
## "msgid"s in this file come from POT (.pot) files.
###
### Do not add, change, or remove "msgid"s manually here as
### they're tied to the ones in the corresponding POT file
### (with the same domain).
###
### Use "mix gettext.extract --merge" or "mix gettext.merge"
### to merge POT files into PO files.
msgid ""
msgstr ""
"Language: en\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:9
#, elixir-autogen, elixir-format
@ -53,6 +52,20 @@ msgstr ""
msgid "%{count} Transactions"
msgstr ""
#: lib/block_scout_web/views/address_token_balance_view.ex:10
#, elixir-autogen, elixir-format
msgid "%{count} token"
msgid_plural "%{count} tokens"
msgstr[0] ""
msgstr[1] ""
#: lib/block_scout_web/templates/block/_tile.html.eex:29
#, elixir-autogen, elixir-format
msgid "%{count} transaction"
msgid_plural "%{count} transactions"
msgstr[0] ""
msgstr[1] ""
#: lib/block_scout_web/templates/transaction/_actions.html.eex:101
#, elixir-autogen, elixir-format
msgid "%{qty} of <span class=\"text-muted\">Token ID [%{link_to_id}]</span>"
@ -68,7 +81,7 @@ msgstr ""
msgid "%{subnetwork} Explorer - BlockScout"
msgstr ""
#: lib/block_scout_web/views/transaction_view.ex:374
#: lib/block_scout_web/views/transaction_view.ex:375
#, elixir-autogen, elixir-format
msgid "(Awaiting internal transactions for status)"
msgstr ""
@ -671,7 +684,7 @@ msgstr ""
msgid "Compiler version"
msgstr ""
#: lib/block_scout_web/views/transaction_view.ex:367
#: lib/block_scout_web/views/transaction_view.ex:368
#, elixir-autogen, elixir-format
msgid "Confirmed"
msgstr ""
@ -1265,12 +1278,12 @@ msgstr ""
msgid "Error trying to fetch balances."
msgstr ""
#: lib/block_scout_web/views/transaction_view.ex:378
#: lib/block_scout_web/views/transaction_view.ex:379
#, elixir-autogen, elixir-format
msgid "Error: %{reason}"
msgstr ""
#: lib/block_scout_web/views/transaction_view.ex:376
#: lib/block_scout_web/views/transaction_view.ex:377
#, elixir-autogen, elixir-format
msgid "Error: (Awaiting internal transactions for reason)"
msgstr ""
@ -1302,6 +1315,11 @@ msgstr ""
msgid "Expand"
msgstr ""
#: lib/block_scout_web/templates/csv_export/index.html.eex:14
#, elixir-autogen, elixir-format
msgid "Export"
msgstr ""
#: lib/block_scout_web/templates/csv_export/index.html.eex:10
#, elixir-autogen, elixir-format
msgid "Export Data"
@ -1708,7 +1726,7 @@ msgstr ""
msgid "Max Priority Fee per Gas"
msgstr ""
#: lib/block_scout_web/views/transaction_view.ex:330
#: lib/block_scout_web/views/transaction_view.ex:331
#, elixir-autogen, elixir-format
msgid "Max of"
msgstr ""
@ -1855,6 +1873,16 @@ msgstr ""
msgid "New Smart Contract Verification"
msgstr ""
#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:9
#, elixir-autogen, elixir-format
msgid "New Smart Contract Verification via Standard input JSON"
msgstr ""
#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:5
#, elixir-autogen, elixir-format
msgid "New Smart Contract Verification via metadata JSON"
msgstr ""
#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:9
#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:7
#, elixir-autogen, elixir-format
@ -2487,7 +2515,7 @@ msgid "Submit an Issue"
msgstr ""
#: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:8
#: lib/block_scout_web/views/transaction_view.ex:375
#: lib/block_scout_web/views/transaction_view.ex:376
#, elixir-autogen, elixir-format
msgid "Success"
msgstr ""
@ -3114,7 +3142,7 @@ msgstr ""
msgid "Uncles"
msgstr ""
#: lib/block_scout_web/views/transaction_view.ex:366
#: lib/block_scout_web/views/transaction_view.ex:367
#, elixir-autogen, elixir-format
msgid "Unconfirmed"
msgstr ""
@ -3461,6 +3489,12 @@ msgstr ""
msgid "Your request contained an error, perhaps a mistyped tx/block/address hash. Try again, and check the developer tools console for more info."
msgstr ""
#: lib/block_scout_web/templates/verified_contracts/index.html.eex:38
#: lib/block_scout_web/views/verified_contracts_view.ex:12
#, elixir-autogen, elixir-format
msgid "Yul"
msgstr ""
#: lib/block_scout_web/templates/address/overview.html.eex:111
#, elixir-autogen, elixir-format
msgid "at"
@ -3471,6 +3505,16 @@ msgstr ""
msgid "balance of the address"
msgstr ""
#: lib/block_scout_web/templates/transaction/overview.html.eex:437
#, elixir-autogen, elixir-format
msgid "burnt for this transaction. Equals Block Base Fee per Gas * Gas Used."
msgstr ""
#: lib/block_scout_web/templates/block/overview.html.eex:215
#, elixir-autogen, elixir-format
msgid "burnt from transactions included in the block (Base fee (per unit of gas) * Gas Used)."
msgstr ""
#: lib/block_scout_web/templates/address_contract/index.html.eex:27
#, elixir-autogen, elixir-format
msgid "button"
@ -3506,6 +3550,11 @@ msgstr ""
msgid "false"
msgstr ""
#: lib/block_scout_web/templates/csv_export/index.html.eex:14
#, elixir-autogen, elixir-format
msgid "for address"
msgstr ""
#: lib/block_scout_web/templates/address_logs/_logs.html.eex:10
#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:12
#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:10
@ -3557,6 +3606,11 @@ msgstr ""
msgid "string"
msgstr ""
#: lib/block_scout_web/templates/csv_export/index.html.eex:17
#, elixir-autogen, elixir-format
msgid "to CSV file"
msgstr ""
#: lib/block_scout_web/views/address_contract_view.ex:29
#, elixir-autogen, elixir-format
msgid "true"
@ -3642,23 +3696,6 @@ msgstr ""
msgid "Beacon chain, Withdrawals, %{subnetwork}, %{coin}"
msgstr ""
#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:29
#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:23
#: lib/block_scout_web/templates/withdrawal/index.html.eex:23
#, elixir-autogen, elixir-format, fuzzy
msgid "Index"
msgstr ""
#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:9
#, elixir-autogen, elixir-format, fuzzy
msgid "New Smart Contract Verification via Standard input JSON"
msgstr ""
#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:5
#, elixir-autogen, elixir-format, fuzzy
msgid "New Smart Contract Verification via metadata JSON"
msgstr ""
#: lib/block_scout_web/templates/layout/_footer.html.eex:31
#, elixir-autogen, elixir-format
msgid "Telegram"
@ -3681,37 +3718,6 @@ msgstr ""
msgid "%{withdrawals_count} withdrawals processed and %{withdrawals_sum} withdrawn."
msgstr ""
#: lib/block_scout_web/templates/csv_export/index.html.eex:14
#, elixir-autogen, elixir-format, fuzzy
msgid "Export"
msgstr ""
#: lib/block_scout_web/templates/csv_export/index.html.eex:14
#, elixir-autogen, elixir-format, fuzzy
msgid "for address"
msgstr ""
#: lib/block_scout_web/templates/csv_export/index.html.eex:17
#, elixir-autogen, elixir-format
msgid "to CSV file"
msgstr ""
#: lib/block_scout_web/templates/verified_contracts/index.html.eex:38
#: lib/block_scout_web/views/verified_contracts_view.ex:12
#, elixir-autogen, elixir-format
msgid "Yul"
msgstr ""
#: lib/block_scout_web/templates/transaction/overview.html.eex:469
#, elixir-autogen, elixir-format, fuzzy
msgid "burnt for this transaction. Equals Block Base Fee per Gas * Gas Used."
msgstr ""
#: lib/block_scout_web/templates/block/overview.html.eex:215
#, elixir-autogen, elixir-format, fuzzy
msgid "burnt from transactions included in the block (Base fee (per unit of gas) * Gas Used)."
msgstr ""
#: lib/block_scout_web/templates/transaction/overview.html.eex:488
#, elixir-autogen, elixir-format, fuzzy
msgid "Actual gas amount used by the transaction."

@ -1218,6 +1218,10 @@ defmodule BlockScoutWeb.Account.Api.V1.UserControllerTest do
"ERC-721" => %{
"incoming" => watchlist.watch_erc_721_input,
"outcoming" => watchlist.watch_erc_721_output
},
"ERC-404" => %{
"incoming" => watchlist.watch_erc_404_input,
"outcoming" => watchlist.watch_erc_404_output
}
}

@ -161,6 +161,37 @@ defmodule BlockScoutWeb.AddressTokenControllerTest do
assert 1 = length(response_2nd_page["items"])
end
test "returns next page of results based on last seen token for erc-404", %{conn: conn} do
address = insert(:address)
1..51
|> Enum.reduce([], fn _i, acc ->
token = insert(:token, name: "FN2 Token", type: "ERC-404")
insert(
:address_current_token_balance,
token_contract_address_hash: token.contract_address_hash,
address: address,
value: 3
)
acc ++ [token.name]
end)
conn =
get(conn, address_token_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash)), %{
"type" => "JSON"
})
assert response = json_response(conn, 200)
request_2nd_page = get(conn, response["next_page_path"], %{"type" => "JSON"})
assert response_2nd_page = json_response(request_2nd_page, 200)
assert 1 = length(response_2nd_page["items"])
end
test "next_page_params exists if not on last page", %{conn: conn} do
address = insert(:address)

@ -2565,6 +2565,42 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
check_paginated_response(response, response_2nd_page, token_instances)
end
test "get paginated ERC-404 nft", %{conn: conn, endpoint: endpoint} do
address = insert(:address)
insert_list(51, :address_current_token_balance_with_token_id)
token_instances =
for _ <- 0..50 do
token = insert(:token, type: "ERC-404")
ti =
insert(:token_instance,
token_contract_address_hash: token.contract_address_hash
)
|> Repo.preload([:token])
current_token_balance =
insert(:address_current_token_balance_with_token_id_and_fixed_token_type,
address: address,
token_type: "ERC-404",
token_id: ti.token_id,
token_contract_address_hash: token.contract_address_hash
)
%Instance{ti | current_token_balance: current_token_balance}
end
|> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id}, :desc)
request = get(conn, endpoint.(address.hash))
assert response = json_response(request, 200)
request_2nd_page = get(conn, endpoint.(address.hash), response["next_page_params"])
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, token_instances)
end
test "test filters", %{conn: conn, endpoint: endpoint} do
address = insert(:address)

@ -532,6 +532,8 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do
tokens_ordered_by_holders_asc
)
:timer.sleep(200)
# by circulating_market_cap
tokens_ordered_by_circulating_market_cap =
Enum.sort(tokens, &(&1.circulating_market_cap <= &2.circulating_market_cap))
@ -634,9 +636,15 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do
insert(:token, type: "ERC-1155")
end
erc_404_tokens =
for _i <- 0..50 do
insert(:token, type: "ERC-404")
end
check_tokens_pagination(erc_20_tokens, conn, %{"type" => "ERC-20"})
check_tokens_pagination(erc_721_tokens |> Enum.reverse(), conn, %{"type" => "ERC-721"})
check_tokens_pagination(erc_1155_tokens |> Enum.reverse(), conn, %{"type" => "ERC-1155"})
check_tokens_pagination(erc_404_tokens |> Enum.reverse(), conn, %{"type" => "ERC-404"})
end
test "tokens are filtered by multiple type", %{conn: conn} do
@ -655,6 +663,11 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do
insert(:token, type: "ERC-1155")
end
erc_404_tokens =
for _i <- 0..24 do
insert(:token, type: "ERC-404")
end
check_tokens_pagination(
erc_721_tokens |> Kernel.++(erc_1155_tokens) |> Enum.reverse(),
conn,
@ -670,6 +683,14 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do
"type" => "[erc-20,ERC-1155]"
}
)
check_tokens_pagination(
erc_404_tokens |> Enum.reverse() |> Kernel.++(erc_20_tokens),
conn,
%{
"type" => "[erc-20,ERC-404]"
}
)
end
test "sorting by fiat_value", %{conn: conn} do
@ -1001,7 +1022,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do
instance = insert(:token_instance, token_id: 0, token_contract_address_hash: token.contract_address_hash)
transfer =
_transfer =
insert(:token_transfer,
token_contract_address: token.contract_address,
transaction: transaction,
@ -1118,6 +1139,68 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do
check_paginated_response(response, response_2nd_page, transfers_0 ++ transfers_1)
end
test "check that pagination works for 404 tokens", %{conn: conn} do
token = insert(:token, type: "ERC-404")
for _ <- 0..50 do
insert(:token_instance, token_id: 0)
end
id = :rand.uniform(1_000_000)
transaction =
:transaction
|> insert(input: "0xabcd010203040506")
|> with_block()
insert(:token_instance, token_id: id, token_contract_address_hash: token.contract_address_hash)
insert_list(100, :token_transfer,
token_contract_address: token.contract_address,
transaction: transaction,
token_ids: [id + 1],
token_type: "ERC-404",
amounts: [1]
)
transfers_0 =
insert_list(26, :token_transfer,
token_contract_address: token.contract_address,
transaction: transaction,
token_ids: [id, id + 1],
token_type: "ERC-404",
amounts: [1, 2]
)
transfers_1 =
for _ <- 26..50 do
transaction =
:transaction
|> insert(input: "0xabcd010203040506")
|> with_block()
insert(:token_transfer,
token_contract_address: token.contract_address,
transaction: transaction,
token_ids: [id],
token_type: "ERC-404"
)
end
request = get(conn, "/api/v2/tokens/#{token.contract_address_hash}/instances/#{id}/transfers")
assert response = json_response(request, 200)
request_2nd_page =
get(
conn,
"/api/v2/tokens/#{token.contract_address_hash}/instances/#{id}/transfers",
response["next_page_params"]
)
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, transfers_0 ++ transfers_1)
end
test "check that pagination works for 721 tokens", %{conn: conn} do
token = insert(:token, type: "ERC-721")
id = 0

@ -59,6 +59,9 @@ defmodule Explorer.Account.Notifier.Email do
"ERC-1155" ->
"Token ID: " <> subject <> " of "
"ERC-404" ->
"Token ID: " <> subject <> " of "
end
end

@ -140,6 +140,7 @@ defmodule Explorer.Account.Notifier.Notify do
end
end
# credo:disable-for-next-line
defp watched?(%WatchlistAddress{} = address, %{type: type}, direction) do
case {type, direction} do
{"COIN", :incoming} -> address.watch_coin_input
@ -150,6 +151,8 @@ defmodule Explorer.Account.Notifier.Notify do
{"ERC-721", :outgoing} -> address.watch_erc_721_output
{"ERC-1155", :incoming} -> address.watch_erc_1155_input
{"ERC-1155", :outgoing} -> address.watch_erc_1155_output
{"ERC-404", :incoming} -> address.watch_erc_404_input
{"ERC-404", :outgoing} -> address.watch_erc_404_output
end
end

@ -151,6 +151,22 @@ defmodule Explorer.Account.Notifier.Summary do
name: transfer.token.name,
type: transfer.token.type
}
"ERC-404" ->
token_ids_string = token_ids(transfer)
%Summary{
amount: amount(transfer),
transaction_hash: transaction.hash,
method: method(transfer),
from_address_hash: transfer.from_address_hash,
to_address_hash: transfer.to_address_hash,
block_number: transfer.block_number,
subject: if(token_ids_string == "", do: transfer.token.type, else: token_ids_string),
tx_fee: fee(transaction),
name: transfer.token.name,
type: transfer.token.type
}
end
end

@ -30,6 +30,8 @@ defmodule Explorer.Account.WatchlistAddress do
field(:watch_erc_721_output, :boolean, default: true, null: false)
field(:watch_erc_1155_input, :boolean, default: true, null: false)
field(:watch_erc_1155_output, :boolean, default: true, null: false)
field(:watch_erc_404_input, :boolean, default: true, null: false)
field(:watch_erc_404_output, :boolean, default: true, null: false)
field(:notify_email, :boolean, default: true, null: false)
field(:notify_epns, :boolean)
field(:notify_feed, :boolean)
@ -43,7 +45,7 @@ defmodule Explorer.Account.WatchlistAddress do
timestamps()
end
@attrs ~w(name address_hash watch_coin_input watch_coin_output watch_erc_20_input watch_erc_20_output watch_erc_721_input watch_erc_721_output watch_erc_1155_input watch_erc_1155_output notify_email notify_epns notify_feed notify_inapp watchlist_id)a
@attrs ~w(name address_hash watch_coin_input watch_coin_output watch_erc_20_input watch_erc_20_output watch_erc_721_input watch_erc_721_output watch_erc_1155_input watch_erc_1155_output watch_erc_404_input watch_erc_404_output notify_email notify_epns notify_feed notify_inapp watchlist_id)a
def changeset do
%__MODULE__{}

@ -3818,13 +3818,13 @@ defmodule Explorer.Chain do
|> select_repo(options).all()
end
@spec erc721_or_erc1155_token_instance_from_token_id_and_token_address(
@spec nft_instance_from_token_id_and_token_address(
Decimal.t() | non_neg_integer(),
Hash.Address.t(),
[api?]
) ::
{:ok, Instance.t()} | {:error, :not_found}
def erc721_or_erc1155_token_instance_from_token_id_and_token_address(token_id, token_contract_address, options \\ []) do
def nft_instance_from_token_id_and_token_address(token_id, token_contract_address, options \\ []) do
query = Instance.token_instance_query(token_id, token_contract_address)
case select_repo(options).one(query) do
@ -4129,9 +4129,10 @@ defmodule Explorer.Chain do
def put_owner_to_token_instance(
%Instance{owner: nil, is_unique: true} = token_instance,
%Token{type: "ERC-1155"},
%Token{type: type},
options
) do
)
when type in ["ERC-1155", "ERC-404"] do
owner_address_hash =
token_instance
|> Instance.owner_query()
@ -4146,7 +4147,7 @@ defmodule Explorer.Chain do
def data, do: DataloaderEcto.new(Repo)
@spec transaction_token_transfer_type(Transaction.t()) ::
:erc20 | :erc721 | :erc1155 | :token_transfer | nil
:erc20 | :erc721 | :erc1155 | :erc404 | :token_transfer | nil
def transaction_token_transfer_type(
%Transaction{
status: :ok,
@ -4207,10 +4208,7 @@ defmodule Explorer.Chain do
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, ERC-1155 token versions
# check for ERC-20 or for old ERC-721, ERC-1155, ERC-404 token versions
{unquote(TokenTransfer.transfer_function_signature()) <> params, ^zero_wei} ->
types = [:address, {:uint, 256}]
@ -4218,7 +4216,7 @@ defmodule Explorer.Chain do
decimal_value = Decimal.new(value)
find_erc721_or_erc20_or_erc1155_token_transfer(transaction.token_transfers, {address, decimal_value})
find_known_token_transfer(transaction.token_transfers, {address, decimal_value})
_ ->
nil
@ -4243,7 +4241,7 @@ defmodule Explorer.Chain do
if token_transfer, do: :erc1155
end
defp find_erc721_or_erc20_or_erc1155_token_transfer(token_transfers, {address, decimal_value}) do
defp find_known_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
@ -4254,6 +4252,7 @@ defmodule Explorer.Chain do
%Token{type: "ERC-20"} -> :erc20
%Token{type: "ERC-721"} -> :erc721
%Token{type: "ERC-1155"} -> :erc1155
%Token{type: "ERC-404"} -> :erc404
_ -> nil
end
else
@ -4504,20 +4503,20 @@ defmodule Explorer.Chain do
...> token_contract_address_hash: token.contract_address_hash,
...> token_id: token_id
...> )
iex> Explorer.Chain.check_erc721_or_erc1155_token_instance_exists(token_id, token.contract_address_hash)
iex> Explorer.Chain.check_nft_instance_exists(token_id, token.contract_address_hash)
:ok
Returns `:not_found` if not found
iex> {:ok, hash} = Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed")
iex> Explorer.Chain.check_erc721_or_erc1155_token_instance_exists(10, hash)
iex> Explorer.Chain.check_nft_instance_exists(10, hash)
:not_found
"""
@spec check_erc721_or_erc1155_token_instance_exists(binary() | non_neg_integer(), Hash.Address.t()) ::
@spec check_nft_instance_exists(binary() | non_neg_integer(), Hash.Address.t()) ::
:ok | :not_found
def check_erc721_or_erc1155_token_instance_exists(token_id, hash) do
def check_nft_instance_exists(token_id, hash) do
token_id
|> erc721_or_erc1155_token_instance_exist?(hash)
|> nft_instance_exist?(hash)
|> boolean_to_check_result()
end
@ -4532,17 +4531,17 @@ defmodule Explorer.Chain do
...> token_contract_address_hash: token.contract_address_hash,
...> token_id: token_id
...> )
iex> Explorer.Chain.erc721_or_erc1155_token_instance_exist?(token_id, token.contract_address_hash)
iex> Explorer.Chain.nft_instance_exist?(token_id, token.contract_address_hash)
true
Returns `false` if not found
iex> {:ok, hash} = Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed")
iex> Explorer.Chain.erc721_or_erc1155_token_instance_exist?(10, hash)
iex> Explorer.Chain.nft_instance_exist?(10, hash)
false
"""
@spec erc721_or_erc1155_token_instance_exist?(binary() | non_neg_integer(), Hash.Address.t()) :: boolean()
def erc721_or_erc1155_token_instance_exist?(token_id, hash) do
@spec nft_instance_exist?(binary() | non_neg_integer(), Hash.Address.t()) :: boolean()
def nft_instance_exist?(token_id, hash) do
query =
from(i in Instance,
where: i.token_contract_address_hash == ^hash and i.token_id == ^Decimal.new(token_id)

@ -23,7 +23,7 @@ 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_id` - The token_id of the transferred token (applicable for ERC-1155, ERC-721 and ERC-404 tokens)
* `token_type` - The type of the token
"""
typed_schema "address_token_balances" do
@ -76,7 +76,7 @@ defmodule Explorer.Chain.Address.TokenBalance do
tb in TokenBalance,
where:
((tb.address_hash != ^@burn_address_hash and tb.token_type == "ERC-721") or tb.token_type == "ERC-20" or
tb.token_type == "ERC-1155") and
tb.token_type == "ERC-1155" or tb.token_type == "ERC-404") and
(is_nil(tb.value_fetched_at) or is_nil(tb.value))
)
else
@ -86,7 +86,7 @@ defmodule Explorer.Chain.Address.TokenBalance do
on: tb.token_contract_address_hash == t.contract_address_hash,
where:
((tb.address_hash != ^@burn_address_hash and t.type == "ERC-721") or t.type == "ERC-20" or
t.type == "ERC-1155") and
t.type == "ERC-1155" or t.type == "ERC-404") and
(is_nil(tb.value_fetched_at) or is_nil(tb.value))
)
end

@ -219,7 +219,8 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do
ordered_changes_list =
changes_list
|> Enum.map(fn change ->
if Map.has_key?(change, :token_id) and Map.get(change, :token_type) == "ERC-1155" do
if Map.has_key?(change, :token_id) and
(Map.get(change, :token_type) == "ERC-1155" || Map.get(change, :token_type) == "ERC-404") do
change
else
Map.put(change, :token_id, nil)

@ -69,10 +69,11 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalances do
ordered_changes_list =
changes_list
|> Enum.map(fn 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)
cond do
Map.has_key?(change, :token_id) and Map.get(change, :token_type) == "ERC-1155" -> change
Map.get(change, :token_type) == "ERC-404" and Map.has_key?(change, :token_id) -> Map.put(change, :value, nil)
Map.get(change, :token_type) == "ERC-404" and Map.has_key?(change, :value) -> Map.put(change, :token_id, nil)
true -> Map.put(change, :token_id, nil)
end
end)
|> Enum.group_by(fn %{

@ -19,7 +19,7 @@ defmodule Explorer.Chain.Shibarium.Bridge do
@typedoc """
* `user_address` - address of the user that initiated operation
* `user` - foreign key of `user_address`
* `amount_or_id` - amount of the operation or NTF id (in case of ERC-721 token)
* `amount_or_id` - amount of the operation or NFT id (in case of ERC-721 token)
* `erc1155_ids` - an array of ERC-1155 token ids (when batch ERC-1155 token transfer)
* `erc1155_amounts` - an array of corresponding ERC-1155 token amounts (when batch ERC-1155 token transfer)
* `l1_transaction_hash` - transaction hash for L1 side

@ -61,6 +61,7 @@ defmodule Explorer.Chain.Token do
* ERC-20
* ERC-721
* ERC-1155
* ERC-404
## Token Specifications
@ -68,6 +69,7 @@ defmodule Explorer.Chain.Token do
* [ERC-721](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md)
* [ERC-777](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-777.md)
* [ERC-1155](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1155.md)
* [ERC-404](https://github.com/Pandora-Labs-Org/erc404)
"""
use Explorer.Schema

@ -1,6 +1,6 @@
defmodule Explorer.Chain.Token.Instance do
@moduledoc """
Represents an ERC-721/ERC-1155 token instance and stores metadata defined in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md.
Represents an ERC-721/ERC-1155/ERC-404 token instance and stores metadata defined in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md.
"""
use Explorer.Schema
@ -113,6 +113,10 @@ defmodule Explorer.Chain.Token.Instance do
erc_1155_token_instances_by_address_hash(address_hash, options)
end
defp nft_list(address_hash, ["ERC-404"], options) do
erc_404_token_instances_by_address_hash(address_hash, options)
end
defp nft_list(address_hash, _, options) do
paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options())
@ -120,6 +124,9 @@ defmodule Explorer.Chain.Token.Instance do
%PagingOptions{key: {_contract_address_hash, _token_id, "ERC-1155"}} ->
erc_1155_token_instances_by_address_hash(address_hash, options)
%PagingOptions{key: {_contract_address_hash, _token_id, "ERC-404"}} ->
erc_404_token_instances_by_address_hash(address_hash, options)
_ ->
erc_721 = erc_721_token_instances_by_owner_address_hash(address_hash, options)
@ -127,8 +134,9 @@ defmodule Explorer.Chain.Token.Instance do
erc_721
else
erc_1155 = erc_1155_token_instances_by_address_hash(address_hash, options)
erc_404 = erc_404_token_instances_by_address_hash(address_hash, options)
(erc_721 ++ erc_1155) |> Enum.take(paging_options.page_size)
(erc_721 ++ erc_1155 ++ erc_404) |> Enum.take(paging_options.page_size)
end
end
end
@ -183,6 +191,33 @@ defmodule Explorer.Chain.Token.Instance do
defp page_erc_1155_token_instances(query, _), do: query
@spec erc_404_token_instances_by_address_hash(binary() | Hash.Address.t(), keyword) :: [Instance.t()]
def erc_404_token_instances_by_address_hash(address_hash, options \\ []) do
paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options())
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
__MODULE__
|> join(:inner, [ti], ctb in CurrentTokenBalance,
as: :ctb,
on:
ctb.token_contract_address_hash == ti.token_contract_address_hash and ctb.token_id == ti.token_id and
ctb.address_hash == ^address_hash
)
|> where([ctb: ctb], ctb.value > 0 and ctb.token_type == "ERC-404")
|> order_by([ti], asc: ti.token_contract_address_hash, desc: ti.token_id)
|> limit(^paging_options.page_size)
|> page_erc_404_token_instances(paging_options)
|> select_merge([ctb: ctb], %{current_token_balance: ctb})
|> Chain.join_associations(necessity_by_association)
|> Chain.select_repo(options).all()
end
defp page_erc_404_token_instances(query, %PagingOptions{key: {contract_address_hash, token_id, "ERC-404"}}) do
page_token_instance(query, contract_address_hash, token_id)
end
defp page_erc_404_token_instances(query, _), do: query
defp page_token_instance(query, contract_address_hash, token_id) do
query
|> where(
@ -199,9 +234,10 @@ defmodule Explorer.Chain.Token.Instance do
def nft_list_next_page_params(%__MODULE__{
current_token_balance: %CurrentTokenBalance{},
token_contract_address_hash: token_contract_address_hash,
token_id: token_id
token_id: token_id,
token: token
}) do
%{"token_contract_address_hash" => token_contract_address_hash, "token_id" => token_id, "token_type" => "ERC-1155"}
%{"token_contract_address_hash" => token_contract_address_hash, "token_id" => token_id, "token_type" => token.type}
end
def nft_list_next_page_params(%__MODULE__{
@ -228,6 +264,10 @@ defmodule Explorer.Chain.Token.Instance do
erc_1155_collections_by_address_hash(address_hash, options)
end
defp nft_collections(address_hash, ["ERC-404"], options) do
erc_404_collections_by_address_hash(address_hash, options)
end
defp nft_collections(address_hash, _, options) do
paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options())
@ -242,8 +282,9 @@ defmodule Explorer.Chain.Token.Instance do
erc_721
else
erc_1155 = erc_1155_collections_by_address_hash(address_hash, options)
erc_404 = erc_404_collections_by_address_hash(address_hash, options)
(erc_721 ++ erc_1155) |> Enum.take(paging_options.page_size)
(erc_721 ++ erc_1155 ++ erc_404) |> Enum.take(paging_options.page_size)
end
end
end
@ -301,6 +342,38 @@ defmodule Explorer.Chain.Token.Instance do
defp page_erc_1155_nft_collections(query, _), do: query
@spec erc_404_collections_by_address_hash(binary() | Hash.Address.t(), keyword) :: [
%{
token_contract_address_hash: Hash.Address.t(),
distinct_token_instances_count: integer(),
token_ids: [integer()]
}
]
def erc_404_collections_by_address_hash(address_hash, options) do
paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options())
CurrentTokenBalance
|> where([ctb], ctb.address_hash == ^address_hash and ctb.value > 0 and ctb.token_type == "ERC-404")
|> group_by([ctb], ctb.token_contract_address_hash)
|> order_by([ctb], asc: ctb.token_contract_address_hash)
|> select([ctb], %{
token_contract_address_hash: ctb.token_contract_address_hash,
distinct_token_instances_count: fragment("COUNT(*)"),
token_ids: fragment("array_agg(?)", ctb.token_id)
})
|> page_erc_404_nft_collections(paging_options)
|> limit(^paging_options.page_size)
|> Chain.select_repo(options).all()
|> Enum.map(&erc_1155_preload_nft(&1, address_hash, options))
|> Helper.custom_preload(options, Token, :token_contract_address_hash, :contract_address_hash, :token)
end
defp page_erc_404_nft_collections(query, %PagingOptions{key: {contract_address_hash, "ERC-404"}}) do
page_nft_collections(query, contract_address_hash)
end
defp page_erc_404_nft_collections(query, _), do: query
defp page_nft_collections(query, token_contract_address_hash) do
query
|> where([ctb], ctb.token_contract_address_hash > ^token_contract_address_hash)

@ -41,6 +41,8 @@ defmodule Explorer.Chain.TokenTransfer do
@weth_withdrawal_signature "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65"
@erc1155_single_transfer_signature "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62"
@erc1155_batch_transfer_signature "0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb"
@erc404_erc20_transfer_event "0xe59fdd36d0d223c0c7d996db7ad796880f45e1936cb0bb7ac102e7082e031487"
@erc404_erc721_transfer_event "0xe5f815dc84b8cecdfd4beedfc3f91ab5be7af100eca4e8fb11552b867995394f"
@transfer_function_signature "0xa9059cbb"
@ -143,6 +145,10 @@ defmodule Explorer.Chain.TokenTransfer do
def erc1155_batch_transfer_signature, do: @erc1155_batch_transfer_signature
def erc404_erc20_transfer_event, do: @erc404_erc20_transfer_event
def erc404_erc721_transfer_event, do: @erc404_erc721_transfer_event
@doc """
ERC 20's transfer(address,uint256) function signature
"""

@ -293,14 +293,14 @@ defmodule Explorer.Etherscan do
@doc """
Gets a list of ERC-721 token transfers for a given address_hash. If contract_address_hash is not nil, transfers will be filtered by contract.
"""
@spec list_nft_token_transfers(Hash.Address.t(), Hash.Address.t() | nil, map()) :: [TokenTransfer.t()]
def list_nft_token_transfers(
@spec list_nft_transfers(Hash.Address.t(), Hash.Address.t() | nil, map()) :: [TokenTransfer.t()]
def list_nft_transfers(
%Hash{byte_count: unquote(Hash.Address.byte_count())} = address_hash,
contract_address_hash,
options \\ @default_options
) do
options
|> base_nft_token_transfers_query(contract_address_hash)
|> base_nft_transfers_query(contract_address_hash)
|> where([tt], tt.from_address_hash == ^address_hash or tt.to_address_hash == ^address_hash)
|> Repo.replica().all()
end
@ -308,17 +308,17 @@ defmodule Explorer.Etherscan do
@doc """
Gets a list of ERC-721 token transfers for a given token contract_address_hash.
"""
@spec list_nft_token_transfers_by_token(Hash.Address.t(), map()) :: [TokenTransfer.t()]
def list_nft_token_transfers_by_token(
@spec list_nft_transfers_by_token(Hash.Address.t(), map()) :: [TokenTransfer.t()]
def list_nft_transfers_by_token(
%Hash{byte_count: unquote(Hash.Address.byte_count())} = contract_address_hash,
options \\ @default_options
) do
options
|> base_nft_token_transfers_query(contract_address_hash)
|> base_nft_transfers_query(contract_address_hash)
|> Repo.replica().all()
end
defp base_nft_token_transfers_query(options, contract_address_hash) do
defp base_nft_transfers_query(options, contract_address_hash) do
options = Map.merge(@default_options, options)
TokenTransfer.erc_721_token_transfers_query()

@ -27,7 +27,7 @@ defmodule Explorer.Token.BalanceReader do
}
]
@erc1155_balance_function_abi [
@nft_balance_function_abi [
%{
"constant" => true,
"inputs" => [%{"name" => "_owner", "type" => "address"}, %{"name" => "_id", "type" => "uint256"}],
@ -67,7 +67,7 @@ defmodule Explorer.Token.BalanceReader do
) :: [{: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
if abi == @nft_balance_function_abi do
token_balance_requests
|> Enum.map(&format_erc_1155_balance_request/1)
else
@ -93,7 +93,7 @@ defmodule Explorer.Token.BalanceReader do
}
]) :: [{:ok, non_neg_integer()} | {:error, String.t()}]
def get_balances_of_erc_1155(token_balance_requests) do
get_balances_of_with_abi(token_balance_requests, @erc1155_balance_function_abi)
get_balances_of_with_abi(token_balance_requests, @nft_balance_function_abi)
end
defp format_balance_request(%{

@ -0,0 +1,10 @@
defmodule Explorer.Repo.Account.Migrations.AddAccountWatchlistAddressesErc404Fields do
use Ecto.Migration
def change do
alter table(:account_watchlist_addresses) do
add(:watch_erc_404_input, :boolean, default: true)
add(:watch_erc_404_output, :boolean, default: true)
end
end
end

@ -4,7 +4,6 @@ defmodule Explorer.Account.Notifier.SummaryTest do
import Explorer.Factory
alias Explorer.Account.Notifier.Summary
alias Explorer.Chain
alias Explorer.Chain.{TokenTransfer, Transaction, Wei}
alias Explorer.Repo
@ -268,5 +267,112 @@ defmodule Explorer.Account.Notifier.SummaryTest do
}
]
end
test "ERC-404 Token transfer with token id" do
token = insert(:token, type: "ERC-404")
tx =
%Transaction{
from_address: _from_address,
to_address: _to_address,
block_number: _block_number,
hash: _tx_hash
} = with_block(insert(:transaction))
transfer =
%TokenTransfer{
amount: _amount,
block_number: block_number,
from_address: from_address,
to_address: to_address
} =
:token_transfer
|> insert(
transaction: tx,
token_ids: [42],
token_contract_address: token.contract_address
)
|> Repo.preload([
:token
])
{_, fee} = Transaction.fee(tx, :gwei)
token_decimals = Decimal.to_integer(token.decimals)
decimals = Decimal.new(Integer.pow(10, token_decimals))
amount = Decimal.div(transfer.amount, decimals)
assert Summary.process(transfer) == [
%Summary{
amount: amount,
block_number: block_number,
from_address_hash: from_address.hash,
method: "transfer",
name: "Infinite Token",
subject: "42",
to_address_hash: to_address.hash,
transaction_hash: tx.hash,
tx_fee: fee,
type: "ERC-404"
}
]
end
test "ERC-404 Token transfer without token id" do
token = insert(:token, type: "ERC-404")
tx =
%Transaction{
from_address: _from_address,
to_address: _to_address,
block_number: _block_number,
hash: _tx_hash
} = with_block(insert(:transaction))
transfer =
%TokenTransfer{
amount: _amount,
block_number: block_number,
from_address: from_address,
to_address: to_address
} =
:token_transfer
|> insert(
transaction: tx,
token_ids: [],
token_contract_address: token.contract_address
)
|> Repo.preload([
:token
])
{_, fee} = Transaction.fee(tx, :gwei)
token_decimals = Decimal.to_integer(token.decimals)
decimals = Decimal.new(Integer.pow(10, token_decimals))
amount = Decimal.div(transfer.amount, decimals)
IO.inspect("Gimme")
IO.inspect(Summary.process(transfer))
assert Summary.process(transfer) == [
%Summary{
amount: amount,
block_number: block_number,
from_address_hash: from_address.hash,
method: "transfer",
name: "Infinite Token",
subject: "ERC-404",
to_address_hash: to_address.hash,
transaction_hash: tx.hash,
tx_fee: fee,
type: "ERC-404"
}
]
end
end
end

@ -89,6 +89,13 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do
value_5 = Decimal.new(2)
token_id_5 = Decimal.new(555)
token_erc_404 = insert(:token, holder_count: 0)
token_erc_404_contract_address_hash = token_erc_404.contract_address_hash
value_6 = Decimal.new(10)
token_id_6 = Decimal.new(333)
value_7 = Decimal.new(25)
block_number = 1
assert {:ok,
@ -121,6 +128,20 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do
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_erc_404_contract_address_hash,
value: ^value_7,
token_id: nil
},
%Explorer.Chain.Address.CurrentTokenBalance{
address_hash: ^address_hash,
block_number: ^block_number,
token_contract_address_hash: ^token_erc_404_contract_address_hash,
value: ^value_6,
token_id: ^token_id_6
}
],
address_current_token_balances_update_token_holder_counts: [
@ -135,6 +156,10 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do
%{
contract_address_hash: ^token_erc_721_contract_address_hash,
holder_count: 1
},
%{
contract_address_hash: ^token_erc_404_contract_address_hash,
holder_count: 2
}
]
}} =
@ -184,6 +209,24 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do
value_fetched_at: DateTime.utc_now(),
token_id: token_id_5,
token_type: "ERC-721"
},
%{
address_hash: address_hash,
block_number: block_number,
token_contract_address_hash: token_erc_404_contract_address_hash,
value: value_6,
value_fetched_at: DateTime.utc_now(),
token_id: token_id_6,
token_type: "ERC-404"
},
%{
address_hash: address_hash,
block_number: block_number,
token_contract_address_hash: token_erc_404_contract_address_hash,
value: value_7,
value_fetched_at: DateTime.utc_now(),
token_id: nil,
token_type: "ERC-404"
}
],
options
@ -197,7 +240,7 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do
current_token_balances
|> Enum.count()
assert current_token_balances_count == 4
assert current_token_balances_count == 6
end
test "updates when the new block number is greater", %{

@ -148,8 +148,8 @@ defmodule Explorer.ChainTest do
end
end
describe "ERC721_or_ERC1155_token_instance_from_token_id_and_token_address/2" do
test "return ERC721 token instance" do
describe "nft_instance_from_token_id_and_token_address/2" do
test "return NFT instance" do
token = insert(:token)
token_id = 10
@ -160,7 +160,7 @@ defmodule Explorer.ChainTest do
)
assert {:ok, result} =
Chain.erc721_or_erc1155_token_instance_from_token_id_and_token_address(
Chain.nft_instance_from_token_id_and_token_address(
token_id,
token.contract_address_hash
)

@ -106,6 +106,10 @@ defmodule Explorer.Factory do
"ERC-721" => %{
"incoming" => random_bool(),
"outcoming" => random_bool()
},
"ERC-404" => %{
"incoming" => random_bool(),
"outcoming" => random_bool()
}
},
"notification_methods" => %{
@ -130,6 +134,8 @@ defmodule Explorer.Factory do
watch_erc_721_output: random_bool(),
watch_erc_1155_input: random_bool(),
watch_erc_1155_output: random_bool(),
watch_erc_404_input: random_bool(),
watch_erc_404_output: random_bool(),
notify_email: random_bool()
}
end
@ -205,6 +211,8 @@ defmodule Explorer.Factory do
watch_erc_721_output: random_bool(),
watch_erc_1155_input: random_bool(),
watch_erc_1155_output: random_bool(),
watch_erc_404_input: random_bool(),
watch_erc_404_output: random_bool(),
notify_email: random_bool()
}
end
@ -951,7 +959,14 @@ defmodule Explorer.Factory do
end
def address_current_token_balance_with_token_id_factory do
{token_type, token_id} = Enum.random([{"ERC-20", nil}, {"ERC-721", nil}, {"ERC-1155", Enum.random(1..100_000)}])
{token_type, token_id} =
Enum.random([
{"ERC-20", nil},
{"ERC-721", nil},
{"ERC-1155", Enum.random(1..100_000)},
{"ERC-404", nil},
{"ERC-404", Enum.random(1..100_000)}
])
%CurrentTokenBalance{
address: build(:address),

@ -209,8 +209,18 @@ defmodule Indexer.Fetcher.TokenBalanceOnDemand do
balance_response =
case token_type do
"ERC-1155" -> BalanceReader.get_balances_of_erc_1155([request])
_ -> BalanceReader.get_balances_of([request])
"ERC-404" ->
if token_id do
BalanceReader.get_balances_of_erc_1155([request])
else
BalanceReader.get_balances_of([request])
end
"ERC-1155" ->
BalanceReader.get_balances_of_erc_1155([request])
_ ->
BalanceReader.get_balances_of([request])
end
balance = balance_response[:ok]

@ -1,6 +1,6 @@
defmodule Indexer.Fetcher.TokenInstance.MetadataRetriever do
@moduledoc """
Fetches ERC-721 & ERC-1155 token instance metadata.
Fetches ERC-721/ERC-1155/ERC-404 token instance metadata.
"""
require Logger

@ -13,7 +13,7 @@ defmodule Indexer.TokenBalances do
alias Indexer.Fetcher.TokenBalance
alias Indexer.Tracer
@erc1155_balance_function_abi [
@nft_balance_function_abi [
%{
"constant" => true,
"inputs" => [%{"name" => "_owner", "type" => "address"}, %{"name" => "_id", "type" => "uint256"}],
@ -39,7 +39,7 @@ defmodule Indexer.TokenBalances do
* `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
* `token_id` - token id for ERC-1155/ERC-404 tokens
"""
def fetch_token_balances_from_blockchain([]), do: {:ok, []}
@ -47,39 +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))
regular_token_balances =
ft_token_balances =
token_balances
|> Enum.filter(fn request ->
if Map.has_key?(request, :token_type) do
request.token_type !== "ERC-1155"
|> Enum.filter(fn token_balance ->
if Map.has_key?(token_balance, :token_type) do
token_balance.token_type !== "ERC-1155" && !(token_balance.token_type == "ERC-404" && token_balance.token_id)
else
true
end
end)
erc1155_token_balances =
nft_token_balances =
token_balances
|> Enum.filter(fn request ->
if Map.has_key?(request, :token_type) do
request.token_type == "ERC-1155"
|> Enum.filter(fn token_balance ->
if Map.has_key?(token_balance, :token_type) do
token_balance.token_type == "ERC-1155" || (token_balance.token_type == "ERC-404" && token_balance.token_id)
else
false
end
end)
requested_regular_token_balances =
regular_token_balances
requested_ft_token_balances =
ft_token_balances
|> BalanceReader.get_balances_of()
|> Stream.zip(regular_token_balances)
|> Stream.zip(ft_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)
requested_nft_token_balances =
nft_token_balances
|> BalanceReader.get_balances_of_with_abi(@nft_balance_function_abi)
|> Stream.zip(nft_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
requested_token_balances = requested_ft_token_balances ++ requested_nft_token_balances
fetched_token_balances = Enum.filter(requested_token_balances, &ignore_request_with_errors/1)
requested_token_balances

@ -22,7 +22,10 @@ defmodule Indexer.Transform.AddressTokenBalances do
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) ->
Enum.reduce(token_ids || [nil], acc, fn id, sub_acc ->
sanitized_token_ids =
if is_nil(token_ids) || (is_list(token_ids) && Enum.empty?(token_ids)), do: [nil], else: token_ids
Enum.reduce(sanitized_token_ids, 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)

@ -66,6 +66,23 @@ defmodule Indexer.Transform.TokenInstances do
)
end
defp transfer_to_instances(
%{
token_type: "ERC-404" = token_type,
token_ids: [_ | _] = token_ids,
token_contract_address_hash: token_contract_address_hash
},
acc
) do
Enum.reduce(token_ids, acc, fn id, sub_acc ->
Map.put(sub_acc, {token_contract_address_hash, id}, %{
token_contract_address_hash: token_contract_address_hash,
token_id: id,
token_type: token_type
})
end)
end
defp transfer_to_instances(
%{
token_type: _token_type,

@ -1,6 +1,6 @@
defmodule Indexer.Transform.TokenTransfers do
@moduledoc """
Helper functions for transforming data for ERC-20 and ERC-721 token transfers.
Helper functions for transforming data for known token standards (ERC-20, ERC-721, ERC-1155, ERC-404) transfers.
"""
require Logger
@ -8,7 +8,7 @@ defmodule Indexer.Transform.TokenTransfers do
import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0]
alias Explorer.{Helper, Repo}
alias Explorer.Chain.{Token, TokenTransfer}
alias Explorer.Chain.{Hash, Token, TokenTransfer}
alias Indexer.Fetcher.TokenTotalSupplyUpdater
@doc """
@ -38,11 +38,21 @@ defmodule Indexer.Transform.TokenTransfers do
end)
|> Enum.reduce(initial_acc, &do_parse(&1, &2, :erc1155))
erc404_token_transfers =
logs
|> Enum.filter(fn log ->
log.first_topic == TokenTransfer.erc404_erc20_transfer_event() ||
log.first_topic == TokenTransfer.erc404_erc721_transfer_event()
end)
|> Enum.reduce(initial_acc, &do_parse(&1, &2, :erc404))
rough_tokens =
erc404_token_transfers.tokens ++
erc1155_token_transfers.tokens ++
erc20_and_erc721_token_transfers.tokens ++ weth_transfers.tokens
rough_token_transfers =
erc404_token_transfers.token_transfers ++
erc1155_token_transfers.token_transfers ++
erc20_and_erc721_token_transfers.token_transfers ++ weth_transfers.token_transfers
@ -141,17 +151,17 @@ defmodule Indexer.Transform.TokenTransfers do
defp token_type_priority(nil), do: -1
@token_types_priority_order ["ERC-20", "ERC-721", "ERC-1155"]
@token_types_priority_order ["ERC-20", "ERC-721", "ERC-1155", "ERC-404"]
defp token_type_priority(token_type) do
Enum.find_index(@token_types_priority_order, &(&1 == token_type))
end
defp do_parse(log, %{tokens: tokens, token_transfers: token_transfers} = acc, type \\ :erc20_erc721) do
parse_result =
if type != :erc1155 do
parse_params(log)
else
parse_erc1155_params(log)
case type do
:erc1155 -> parse_erc1155_params(log)
:erc404 -> parse_erc404_params(log)
_ -> parse_params(log)
end
case parse_result do
@ -295,7 +305,13 @@ defmodule Indexer.Transform.TokenTransfers do
{token, token_transfer}
end
def parse_erc1155_params(
@spec parse_erc1155_params(map()) ::
nil
| {%{
contract_address_hash: Hash.Address.t(),
type: String.t()
}, map()}
defp parse_erc1155_params(
%{
first_topic: unquote(TokenTransfer.erc1155_batch_transfer_signature()),
third_topic: third_topic,
@ -333,7 +349,7 @@ defmodule Indexer.Transform.TokenTransfers do
end
end
def parse_erc1155_params(%{third_topic: third_topic, fourth_topic: fourth_topic, data: data} = log) do
defp parse_erc1155_params(%{third_topic: third_topic, fourth_topic: fourth_topic, data: data} = log) do
[token_id, value] = Helper.decode_data(data, [{:uint, 256}, {:uint, 256}])
from_address_hash = truncate_address_hash(third_topic)
@ -360,6 +376,84 @@ defmodule Indexer.Transform.TokenTransfers do
{token, token_transfer}
end
@spec parse_erc404_params(map()) ::
nil
| {%{
contract_address_hash: Hash.Address.t(),
type: String.t()
}, map()}
defp parse_erc404_params(
%{
first_topic: unquote(TokenTransfer.erc404_erc20_transfer_event()),
second_topic: second_topic,
third_topic: third_topic,
fourth_topic: nil,
data: data
} = log
) do
[value] = Helper.decode_data(data, [{:uint, 256}])
if is_nil(value) or value == [] do
nil
else
token_transfer = %{
block_number: log.block_number,
block_hash: log.block_hash,
log_index: log.index,
from_address_hash: truncate_address_hash(second_topic),
to_address_hash: truncate_address_hash(third_topic),
token_contract_address_hash: log.address_hash,
transaction_hash: log.transaction_hash,
token_type: "ERC-404",
token_ids: [],
amounts: [value]
}
token = %{
contract_address_hash: log.address_hash,
type: "ERC-404"
}
{token, token_transfer}
end
end
defp parse_erc404_params(
%{
first_topic: unquote(TokenTransfer.erc404_erc721_transfer_event()),
second_topic: second_topic,
third_topic: third_topic,
fourth_topic: fourth_topic,
data: _data
} = log
) do
[token_id] = Helper.decode_data(fourth_topic, [{:uint, 256}])
if is_nil(token_id) or token_id == [] do
nil
else
token_transfer = %{
block_number: log.block_number,
block_hash: log.block_hash,
log_index: log.index,
from_address_hash: truncate_address_hash(second_topic),
to_address_hash: truncate_address_hash(third_topic),
token_contract_address_hash: log.address_hash,
transaction_hash: log.transaction_hash,
token_type: "ERC-404",
token_ids: [token_id],
amounts: []
}
token = %{
contract_address_hash: log.address_hash,
type: "ERC-404"
}
{token, token_transfer}
end
end
defp truncate_address_hash(nil), do: burn_address_hash_string()
defp truncate_address_hash("0x000000000000000000000000" <> truncated_hash) do

@ -261,7 +261,8 @@ defmodule Indexer.Fetcher.TokenBalanceTest do
address_hash: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
block_number: 19999,
token_contract_address_hash: to_string(contract.contract_address_hash),
token_id: 11,
token_id: nil,
value: 100_500,
token_type: "ERC-20"
},
%{
@ -275,5 +276,30 @@ defmodule Indexer.Fetcher.TokenBalanceTest do
assert TokenBalance.import_token_balances(token_balances_params) == :ok
end
test "import ERC-404 token balances and return :ok" do
contract = 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: 11,
token_type: "ERC-404"
},
%{
address_hash: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
block_number: 19999,
token_contract_address_hash: to_string(contract.contract_address_hash),
token_id: nil,
value: 100_500,
token_type: "ERC-404"
}
]
assert TokenBalance.import_token_balances(token_balances_params) == :ok
end
end
end

@ -88,6 +88,63 @@ defmodule Indexer.TokenBalancesTest do
} = result
end
test "fetches balances of ERC-404 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: nil,
value: 10,
token_type: "ERC-404"
},
%{
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-404",
value: 2
}
]
get_404_ft_balances_from_blockchain()
get_404_nft_balances_from_blockchain()
{:ok, result} = TokenBalances.fetch_token_balances_from_blockchain(data)
assert %{
failed_token_balances: [],
fetched_token_balances: [
%{
value: 10,
token_contract_address_hash: ^token_contract_address_hash,
address_hash: ^address_hash_string,
block_number: 1_000,
value_fetched_at: _
},
%{
token_id: 5,
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")
@ -363,6 +420,67 @@ defmodule Indexer.TokenBalancesTest do
)
end
defp get_404_ft_balances_from_blockchain() do
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn [
%{
id: id,
method: "eth_call",
params: [
%{
data: "0x70a08231000000000000000000000000609991ca0ae39bc4eaf2669976237296d40c2f31",
to: "0xf7f79032fd395978acb7069c74d21e5a53206559"
},
"0x3E8"
]
}
],
_options ->
{:ok,
[
%{
id: id,
jsonrpc: "2.0",
result: "0x000000000000000000000000000000000000000000000000000000000000000a"
}
]}
end
)
end
defp get_404_nft_balances_from_blockchain() do
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn [
%{
id: id,
method: "eth_call",
params: [
%{
data:
"0x00fdd58e000000000000000000000000609991ca0ae39bc4eaf2669976237296d40c2f310000000000000000000000000000000000000000000000000000000000000005",
to: "0xf7f79032fd395978acb7069c74d21e5a53206559"
},
"0x3E8"
]
}
],
_options ->
{:ok,
[
%{
id: id,
jsonrpc: "2.0",
result: "0x0000000000000000000000000000000000000000000000000000000000000002"
}
]}
end
)
end
defp get_erc1155_balance_from_blockchain() do
expect(
EthereumJSONRPC.Mox,

@ -187,7 +187,7 @@ defmodule Indexer.Transform.TokenTransfersTest do
data:
"0x1000000000000c520000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001",
first_topic: "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62",
secon_topic: "0x0000000000000000000000009c978f4cfa1fe13406bcc05baf26a35716f881dd",
second_topic: "0x0000000000000000000000009c978f4cfa1fe13406bcc05baf26a35716f881dd",
third_topic: "0x0000000000000000000000009c978f4cfa1fe13406bcc05baf26a35716f881dd",
fourth_topic: "0x0000000000000000000000009c978f4cfa1fe13406bcc05baf26a35716f881dd",
index: 2,
@ -228,7 +228,7 @@ defmodule Indexer.Transform.TokenTransfersTest do
data:
"0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000001388",
first_topic: "0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb",
secon_topic: "0x0000000000000000000000006c943470780461b00783ad530a53913bd2c104d3",
second_topic: "0x0000000000000000000000006c943470780461b00783ad530a53913bd2c104d3",
third_topic: "0x0000000000000000000000006c943470780461b00783ad530a53913bd2c104d3",
fourth_topic: "0x0000000000000000000000006c943470780461b00783ad530a53913bd2c104d3",
index: 2,
@ -262,7 +262,7 @@ defmodule Indexer.Transform.TokenTransfersTest do
data:
"0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
first_topic: "0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb",
secon_topic: "0x81D0caF80E9bFfD9bF9c641ab964feB9ef69069e",
second_topic: "0x81D0caF80E9bFfD9bF9c641ab964feB9ef69069e",
third_topic: "0x598AF04C88122FA4D1e08C5da3244C39F10D4F14",
fourth_topic: "0x0000000000000000000000000000000000000000",
index: 6,
@ -340,7 +340,7 @@ defmodule Indexer.Transform.TokenTransfersTest do
data:
"0x1000000000000c520000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001",
first_topic: "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62",
secon_topic: "0x0000000000000000000000009c978f4cfa1fe13406bcc05baf26a35716f881dd",
second_topic: "0x0000000000000000000000009c978f4cfa1fe13406bcc05baf26a35716f881dd",
third_topic: "0x0000000000000000000000009c978f4cfa1fe13406bcc05baf26a35716f881dd",
fourth_topic: "0x0000000000000000000000009c978f4cfa1fe13406bcc05baf26a35716f881dd",
index: 2,
@ -357,6 +357,84 @@ defmodule Indexer.Transform.TokenTransfersTest do
tokens: [%{contract_address_hash: ^contract_address_hash, type: "ERC-1155"}]
} = TokenTransfers.parse(logs)
end
test "parses erc404 token transfer from ERC20Transfer" do
log = %{
address_hash: "0x03F6CCfCE60273eFbEB9535675C8EFA69D863f37",
block_number: 10_561_358,
data: "0x00000000000000000000000000000000000000000000003635c9adc5de9ffc48",
first_topic: "0xe59fdd36d0d223c0c7d996db7ad796880f45e1936cb0bb7ac102e7082e031487",
second_topic: "0x000000000000000000000000c36442b4a4522e871399cd717abdd847ab11fe88",
third_topic: "0x00000000000000000000000018336808ed2f2c80795861041f711b299ecd38ca",
fourth_topic: nil,
index: 34,
transaction_hash: "0x6be468f465911ec70103aa83e38c84697848feaf760eee3a181ebcdcab82dc4a",
block_hash: "0x7cffabfd975bded1ec397f44b4af3a97618b96ca0e2f92d70a3025ba233815ca"
}
assert TokenTransfers.parse([log]) == %{
token_transfers: [
%{
block_hash: "0x7cffabfd975bded1ec397f44b4af3a97618b96ca0e2f92d70a3025ba233815ca",
block_number: 10_561_358,
from_address_hash: "0xc36442b4a4522e871399cd717abdd847ab11fe88",
log_index: 34,
to_address_hash: "0x18336808ed2f2c80795861041f711b299ecd38ca",
token_contract_address_hash: "0x03F6CCfCE60273eFbEB9535675C8EFA69D863f37",
amounts: [
999_999_999_999_999_999_048
],
token_ids: [],
token_type: "ERC-404",
transaction_hash: "0x6be468f465911ec70103aa83e38c84697848feaf760eee3a181ebcdcab82dc4a"
}
],
tokens: [
%{
contract_address_hash: "0x03F6CCfCE60273eFbEB9535675C8EFA69D863f37",
type: "ERC-404"
}
]
}
end
test "parses erc404 token transfer from ERC721Transfer" do
log = %{
address_hash: "0x68995c84aFb019913942E53F27E7ceA47D86Cd9d",
block_number: 10_514_498,
data: "0x",
first_topic: "0xe5f815dc84b8cecdfd4beedfc3f91ab5be7af100eca4e8fb11552b867995394f",
second_topic: "0x000000000000000000000000fd7ec4d8b6ba1a72f3895b6ce3846b00d6b83aab",
third_topic: "0x0000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488d",
fourth_topic: "0x000000000000000000000000000000000000000000000000000000000000000a",
index: 41,
transaction_hash: "0xe201aed9c948f46395c6acc54de5e9c3ebe0c41a5c34cc6a507b67ec46057c55",
block_hash: "0xea065ff2fc04177bbef27317209a25f2633199aa453b86ee405b619c495b2e77"
}
assert TokenTransfers.parse([log]) == %{
token_transfers: [
%{
block_hash: "0xea065ff2fc04177bbef27317209a25f2633199aa453b86ee405b619c495b2e77",
block_number: 10_514_498,
from_address_hash: "0xfd7ec4d8b6ba1a72f3895b6ce3846b00d6b83aab",
log_index: 41,
to_address_hash: "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
token_contract_address_hash: "0x68995c84aFb019913942E53F27E7ceA47D86Cd9d",
amounts: [],
token_ids: [10],
token_type: "ERC-404",
transaction_hash: "0xe201aed9c948f46395c6acc54de5e9c3ebe0c41a5c34cc6a507b67ec46057c55"
}
],
tokens: [
%{
contract_address_hash: "0x68995c84aFb019913942E53F27E7ceA47D86Cd9d",
type: "ERC-404"
}
]
}
end
end
defp truncated_hash("0x000000000000000000000000" <> rest) do

Loading…
Cancel
Save