Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update token balances in token instance fetcher #6168

Merged
merged 4 commits into from Sep 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -5,6 +5,7 @@
- [#6092](https://github.com/blockscout/blockscout/pull/6092) - Blockscout Account functionality
- [#6073](https://github.com/blockscout/blockscout/pull/6073) - Add vyper support for rust verifier microservice integration
- [#6111](https://github.com/blockscout/blockscout/pull/6111) - Add Prometheus metrics to indexer
- [#6168](https://github.com/blockscout/blockscout/pull/6168) - Token instance fetcher checks instance owner and updates current token balance

### Fixes

Expand Down
76 changes: 76 additions & 0 deletions apps/explorer/lib/explorer/token/instance_owner_reader.ex
@@ -0,0 +1,76 @@
defmodule Explorer.Token.InstanceOwnerReader do
@moduledoc """
Reads Token Instance owner using Smart Contract function from the blockchain.
"""

require Logger

alias Explorer.SmartContract.Reader

@owner_function_signature "6352211e"

@owner_function_abi [
%{
"type" => "function",
"stateMutability" => "view",
"payable" => false,
"outputs" => [
%{
"type" => "address",
"name" => "owner"
}
],
"name" => "ownerOf",
"inputs" => [
%{
"type" => "uint256",
"name" => "tokenId"
}
]
}
]

@spec get_owner_of([%{token_contract_address_hash: String.t(), token_id: integer}]) :: [
{:ok, String.t()} | {:error, String.t()}
]
def get_owner_of(instance_owner_requests) do
instance_owner_requests
|> Enum.map(&format_owner_request/1)
|> Reader.query_contracts(@owner_function_abi)
|> Enum.zip(instance_owner_requests)
|> Enum.reduce([], fn {result, request}, acc ->
case format_owner_result(result, request) do
{:ok, ok_result} ->
[ok_result] ++ acc

{:error, error_message} ->
Logger.error(
"Failed to get owner of token #{request.token_contract_address_hash}, token_id #{request.token_id}, reason: #{error_message}"
)

acc
end
end)
end

defp format_owner_request(%{token_contract_address_hash: token_contract_address_hash, token_id: token_id}) do
%{
contract_address: token_contract_address_hash,
method_id: @owner_function_signature,
args: [token_id]
}
end

defp format_owner_result({:ok, [owner]}, request) do
{:ok,
%{
token_contract_address_hash: request.token_contract_address_hash,
token_id: request.token_id,
owner: owner
}}
end

defp format_owner_result({:error, error_message}, _request) do
{:error, error_message}
end
end
57 changes: 55 additions & 2 deletions apps/indexer/lib/indexer/fetcher/token_instance.ex
Expand Up @@ -8,8 +8,9 @@ defmodule Indexer.Fetcher.TokenInstance do

require Logger

alias Explorer.Chain
alias Explorer.Token.InstanceMetadataRetriever
alias Explorer.{Chain, Repo}
alias Explorer.Chain.{Address, Cache.BlockNumber, Token}
alias Explorer.Token.{InstanceMetadataRetriever, InstanceOwnerReader}
alias Indexer.BufferedTask

@behaviour BufferedTask
Expand Down Expand Up @@ -59,6 +60,7 @@ defmodule Indexer.Fetcher.TokenInstance do
end

Enum.each(all_token_ids, &fetch_instance(hash, &1))
update_current_token_balances(hash, all_token_ids)

:ok
end
Expand Down Expand Up @@ -97,6 +99,57 @@ defmodule Indexer.Fetcher.TokenInstance do
end
end

defp update_current_token_balances(token_contract_address_hash, token_ids) do
token_ids
|> Enum.map(&instance_owner_request(token_contract_address_hash, &1))
|> InstanceOwnerReader.get_owner_of()
|> Enum.map(&current_token_balances_import_params/1)
|> all_import_params()
|> Chain.import()
end

defp instance_owner_request(token_contract_address_hash, token_id) do
%{
token_contract_address_hash: to_string(token_contract_address_hash),
token_id: Decimal.to_integer(token_id)
}
end

defp current_token_balances_import_params(%{token_contract_address_hash: hash, token_id: token_id, owner: owner}) do
%{
value: Decimal.new(1),
block_number: BlockNumber.get_max(),
value_fetched_at: DateTime.utc_now(),
token_id: token_id,
token_type: Repo.get_by(Token, contract_address_hash: hash).type,
address_hash: owner,
token_contract_address_hash: hash
}
end

defp all_import_params(balances_import_params) do
addresses_import_params =
balances_import_params
|> Enum.reduce([], fn %{address_hash: address_hash}, acc ->
case Repo.get_by(Address, hash: address_hash) do
nil -> [%{hash: address_hash} | acc]
_address -> acc
end
end)
|> case do
[] -> %{}
params -> %{addresses: %{params: params}}
end

current_token_balances_import_params = %{
address_current_token_balances: %{
params: balances_import_params
}
}

Map.merge(current_token_balances_import_params, addresses_import_params)
end

@doc """
Fetches token instance data asynchronously.
"""
Expand Down
87 changes: 87 additions & 0 deletions apps/indexer/test/indexer/fetcher/token_instance_test.exs
@@ -0,0 +1,87 @@
defmodule Indexer.Fetcher.TokenInstanceTest do
use EthereumJSONRPC.Case, async: false
use Explorer.DataCase

import Mox

alias Explorer.Chain
alias Explorer.Chain.Address
alias Explorer.Chain.Address.CurrentTokenBalance
alias Explorer.Repo
alias Indexer.Fetcher.TokenInstance

describe "run/2" do
test "updates current token balance" do
token = insert(:token, type: "ERC-1155")
token_contract_address_hash = token.contract_address_hash
instance = insert(:token_instance, token_contract_address_hash: token_contract_address_hash)
token_id = instance.token_id
address = insert(:address, hash: "0x57e93bb58268de818b42e3795c97bad58afcd3fe")
address_hash = address.hash

EthereumJSONRPC.Mox
|> expect(:json_rpc, fn [%{id: 0, method: "eth_call", params: [%{data: "0xc87b56dd" <> _}, _]}], _ ->
{:ok,
[
%{
id: 0,
result:
"0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000027b7d000000000000000000000000000000000000000000000000000000000000"
}
]}
end)
|> expect(:json_rpc, fn [%{id: 0, method: "eth_call", params: [%{data: "0x6352211e" <> _}, _]}], _ ->
{:ok, [%{id: 0, result: "0x00000000000000000000000057e93bb58268de818b42e3795c97bad58afcd3fe"}]}
end)

TokenInstance.run(
[%{contract_address_hash: token_contract_address_hash, token_id: nil, token_ids: [token_id]}],
nil
)

assert %{
token_id: ^token_id,
token_type: "ERC-1155",
token_contract_address_hash: ^token_contract_address_hash,
address_hash: ^address_hash
} = Repo.one(CurrentTokenBalance)
end

test "updates current token balance with missing address" do
token = insert(:token, type: "ERC-1155")
token_contract_address_hash = token.contract_address_hash
instance = insert(:token_instance, token_contract_address_hash: token_contract_address_hash)
token_id = instance.token_id
{:ok, address_hash} = Chain.string_to_address_hash("0x57e93bb58268de818b42e3795c97bad58afcd3fe")

EthereumJSONRPC.Mox
|> expect(:json_rpc, fn [%{id: 0, method: "eth_call", params: [%{data: "0xc87b56dd" <> _}, _]}], _ ->
{:ok,
[
%{
id: 0,
result:
"0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000027b7d000000000000000000000000000000000000000000000000000000000000"
}
]}
end)
|> expect(:json_rpc, fn [%{id: 0, method: "eth_call", params: [%{data: "0x6352211e" <> _}, _]}], _ ->
{:ok, [%{id: 0, result: "0x00000000000000000000000057e93bb58268de818b42e3795c97bad58afcd3fe"}]}
end)

TokenInstance.run(
[%{contract_address_hash: token_contract_address_hash, token_id: token_id, token_ids: nil}],
nil
)

assert %{
token_id: ^token_id,
token_type: "ERC-1155",
token_contract_address_hash: ^token_contract_address_hash,
address_hash: ^address_hash
} = Repo.one(CurrentTokenBalance)

assert %Address{} = Repo.get_by(Address, hash: address_hash)
end
end
end