diff --git a/CHANGELOG.md b/CHANGELOG.md index 34ec49312a..c72a4798de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,16 @@ ## Current ### Features +- [#2918](https://github.com/poanetwork/blockscout/pull/2918) - Add tokenID for tokentx API action ### Fixes +- [#2914](https://github.com/poanetwork/blockscout/pull/2914) - Reduce execution time of stream_unfetched_token_instances query +- [#2906](https://github.com/poanetwork/blockscout/pull/2906) - fix address sum cache +- [#2902](https://github.com/poanetwork/blockscout/pull/2902) - Offset in blocks retrieval for average block time +- [#2900](https://github.com/poanetwork/blockscout/pull/2900) - check fetched instance metadata in multiple places - [#2899](https://github.com/poanetwork/blockscout/pull/2899) - fix empty buffered task ### Chore - - [#2896](https://github.com/poanetwork/blockscout/pull/2896) - Disable Parity websockets tests @@ -14,6 +18,7 @@ ### Features - [#2862](https://github.com/poanetwork/blockscout/pull/2862) - Coin total supply from DB API endpoint +- [#2857](https://github.com/poanetwork/blockscout/pull/2857) - Extend getsourcecode API view with new output fields - [#2822](https://github.com/poanetwork/blockscout/pull/2822) - Estimated address count on the main page, if cache is empty - [#2821](https://github.com/poanetwork/blockscout/pull/2821) - add autodetection of constructor arguments - [#2825](https://github.com/poanetwork/blockscout/pull/2825) - separate token transfers and transactions diff --git a/apps/block_scout_web/lib/block_scout_web/etherscan.ex b/apps/block_scout_web/lib/block_scout_web/etherscan.ex index abb358179c..5e6c05bb41 100644 --- a/apps/block_scout_web/lib/block_scout_web/etherscan.ex +++ b/apps/block_scout_web/lib/block_scout_web/etherscan.ex @@ -589,6 +589,12 @@ defmodule BlockScoutWeb.Etherscan do example: ~s("Some Token Name") } + @token_id_type %{ + type: "integer", + definition: "id of token", + example: ~s("0") + } + @token_symbol_type %{ type: "string", definition: "Trading symbol of the token.", @@ -752,6 +758,7 @@ defmodule BlockScoutWeb.Etherscan do example: ~s("663046792267785498951364") }, tokenName: @token_name_type, + tokenID: @token_id_type, tokenSymbol: @token_symbol_type, tokenDecimal: @token_decimal_type, transactionIndex: @transaction_index_type, diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex index 8063b1db40..62da759428 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex @@ -121,7 +121,7 @@ defmodule BlockScoutWeb.API.RPC.AddressView do } end - defp prepare_token_transfer(token_transfer) do + defp prepare_common_token_transfer(token_transfer) do %{ "blockNumber" => to_string(token_transfer.block_number), "timeStamp" => to_string(DateTime.to_unix(token_transfer.block_timestamp)), @@ -132,7 +132,6 @@ defmodule BlockScoutWeb.API.RPC.AddressView do "contractAddress" => to_string(token_transfer.token_contract_address_hash), "to" => to_string(token_transfer.to_address_hash), "logIndex" => to_string(token_transfer.token_log_index), - "value" => get_token_value(token_transfer), "tokenName" => token_transfer.token_name, "tokenSymbol" => token_transfer.token_symbol, "tokenDecimal" => to_string(token_transfer.token_decimals), @@ -146,12 +145,20 @@ defmodule BlockScoutWeb.API.RPC.AddressView do } end - defp get_token_value(%{token_type: "ERC-721"} = token_transfer) do - to_string(token_transfer.token_id) + defp prepare_token_transfer(%{token_type: "ERC-721"} = token_transfer) do + token_transfer + |> prepare_common_token_transfer() + |> Map.put_new(:tokenID, token_transfer.token_id) + end + + defp prepare_token_transfer(%{token_type: "ERC-20"} = token_transfer) do + token_transfer + |> prepare_common_token_transfer() + |> Map.put_new(:value, to_string(token_transfer.amount)) end - defp get_token_value(token_transfer) do - to_string(token_transfer.amount) + defp prepare_token_transfer(token_transfer) do + prepare_common_token_transfer(token_transfer) end defp prepare_block(block) do diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex index 8f996a2bcc..41a0b7734d 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex @@ -4,6 +4,8 @@ defmodule BlockScoutWeb.API.RPC.ContractView do alias BlockScoutWeb.API.RPC.RPCView alias Explorer.Chain.{Address, DecompiledSmartContract, SmartContract} + defguardp is_empty_string(input) when input == "" or input == nil + def render("listcontracts.json", %{contracts: contracts}) do contracts = Enum.map(contracts, &prepare_contract/1) @@ -35,7 +37,11 @@ defmodule BlockScoutWeb.API.RPC.ContractView do "CompilerVersion" => "", "DecompiledSourceCode" => "", "DecompilerVersion" => decompiler_version(nil), - "OptimizationUsed" => "" + "OptimizationUsed" => "", + "OptimizationRuns" => "", + "EVMVersion" => "", + "ConstructorArguments" => "", + "ExternalLibraries" => "" } end @@ -43,6 +49,68 @@ defmodule BlockScoutWeb.API.RPC.ContractView do decompiled_smart_contract = latest_decompiled_smart_contract(address.decompiled_smart_contracts) contract = address.smart_contract || %{} + optimization = Map.get(contract, :optimization, "") + + contract_output = %{ + "Address" => to_string(address.hash) + } + + contract_output + |> set_decompiled_contract_data(decompiled_smart_contract) + |> set_optimization_runs(contract, optimization) + |> set_constructor_arguments(contract) + |> set_external_libraries(contract) + |> set_verified_contract_data(contract, address, optimization) + end + + defp set_decompiled_contract_data(contract_output, decompiled_smart_contract) do + if decompiled_smart_contract do + contract_output + |> Map.put_new(:DecompiledSourceCode, decompiled_source_code(decompiled_smart_contract)) + |> Map.put_new(:DecompilerVersion, decompiler_version(decompiled_smart_contract)) + else + contract_output + end + end + + defp set_optimization_runs(contract_output, contract, optimization) do + optimization_runs = Map.get(contract, :optimization_runs, "") + + if optimization && optimization != "" do + contract_output + |> Map.put_new(:OptimizationRuns, optimization_runs) + else + contract_output + end + end + + defp set_constructor_arguments(contract_output, %{constructor_arguments: arguments}) when is_empty_string(arguments), + do: contract_output + + defp set_constructor_arguments(contract_output, %{constructor_arguments: arguments}) do + contract_output + |> Map.put_new(:ConstructorArguments, arguments) + end + + defp set_constructor_arguments(contract_output, _), do: contract_output + + defp set_external_libraries(contract_output, contract) do + external_libraries = Map.get(contract, :external_libraries, []) + + if Enum.count(external_libraries) > 0 do + external_libraries_without_id = + Enum.map(external_libraries, fn %{name: name, address_hash: address_hash} -> + %{"name" => name, "address_hash" => address_hash} + end) + + contract_output + |> Map.put_new(:ExternalLibraries, external_libraries_without_id) + else + contract_output + end + end + + defp set_verified_contract_data(contract_output, contract, address, optimization) do contract_abi = if is_nil(address.smart_contract) do "Contract source code not verified" @@ -51,27 +119,28 @@ defmodule BlockScoutWeb.API.RPC.ContractView do end contract_optimization = - case Map.get(contract, :optimization, "") do + case optimization do true -> - "1" + "true" false -> - "0" + "false" "" -> "" end - %{ - "Address" => to_string(address.hash), - "SourceCode" => Map.get(contract, :contract_source_code, ""), - "ABI" => contract_abi, - "ContractName" => Map.get(contract, :name, ""), - "DecompiledSourceCode" => decompiled_source_code(decompiled_smart_contract), - "DecompilerVersion" => decompiler_version(decompiled_smart_contract), - "CompilerVersion" => Map.get(contract, :compiler_version, ""), - "OptimizationUsed" => contract_optimization - } + if Map.equal?(contract, %{}) do + contract_output + else + contract_output + |> Map.put_new(:SourceCode, Map.get(contract, :contract_source_code, "")) + |> Map.put_new(:ABI, contract_abi) + |> Map.put_new(:ContractName, Map.get(contract, :name, "")) + |> Map.put_new(:CompilerVersion, Map.get(contract, :compiler_version, "")) + |> Map.put_new(:OptimizationUsed, contract_optimization) + |> Map.put_new(:EVMVersion, Map.get(contract, :evm_version, "")) + end end defp prepare_contract(%Address{ @@ -80,10 +149,7 @@ defmodule BlockScoutWeb.API.RPC.ContractView do }) do %{ "Address" => to_string(hash), - "ABI" => "Contract source code not verified", - "ContractName" => "", - "CompilerVersion" => "", - "OptimizationUsed" => "" + "ABI" => "Contract source code not verified" } end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs index 135fbe9948..2db1bdb33c 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs @@ -88,21 +88,24 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do mining_address = insert(:address, fetched_coin_balance: 0, - fetched_coin_balance_block_number: 2, + fetched_coin_balance_block_number: 102, inserted_at: Timex.shift(now, minutes: -10) ) mining_address_hash = to_string(mining_address.hash) # we space these very far apart so that we know it will consider the 0th block stale (it calculates how far # back we'd need to go to get 24 hours in the past) - insert(:block, number: 0, timestamp: Timex.shift(now, hours: -50), miner: mining_address) - insert(:block, number: 1, timestamp: Timex.shift(now, hours: -25), miner: mining_address) + Enum.each(0..100, fn i -> + insert(:block, number: i, timestamp: Timex.shift(now, hours: -(102 - i) * 25), miner: mining_address) + end) + + insert(:block, number: 101, timestamp: Timex.shift(now, hours: -25), miner: mining_address) AverageBlockTime.refresh() address = insert(:address, fetched_coin_balance: 100, - fetched_coin_balance_block_number: 0, + fetched_coin_balance_block_number: 100, inserted_at: Timex.shift(now, minutes: -5) ) @@ -112,7 +115,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do %{ id: id, method: "eth_getBalance", - params: [^address_hash, "0x1"] + params: [^address_hash, "0x65"] } ], _options -> @@ -148,7 +151,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do assert received_address.hash == address.hash assert received_address.fetched_coin_balance == expected_wei - assert received_address.fetched_coin_balance_block_number == 1 + assert received_address.fetched_coin_balance_block_number == 101 end end @@ -1804,7 +1807,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do |> get("/api", params) |> json_response(200) - assert result["value"] == to_string(token_transfer.token_id) + assert result["tokenID"] == to_string(token_transfer.token_id) assert response["status"] == "1" assert response["message"] == "OK" assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) @@ -2615,6 +2618,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do "logIndex" => %{"type" => "string"}, "value" => %{"type" => "string"}, "tokenName" => %{"type" => "string"}, + "tokenID" => %{"type" => "string"}, "tokenSymbol" => %{"type" => "string"}, "tokenDecimal" => %{"type" => "string"}, "transactionIndex" => %{"type" => "string"}, diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs index 37ce57f508..5c12c1e5db 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs @@ -1,5 +1,6 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do use BlockScoutWeb.ConnCase + alias Explorer.Chain.SmartContract alias Explorer.{Chain, Factory} describe "listcontracts" do @@ -70,10 +71,7 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do assert response["result"] == [ %{ "ABI" => "Contract source code not verified", - "Address" => to_string(address.hash), - "CompilerVersion" => "", - "ContractName" => "", - "OptimizationUsed" => "" + "Address" => to_string(address.hash) } ] @@ -95,10 +93,7 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do assert response["result"] == [ %{ "ABI" => "Contract source code not verified", - "Address" => to_string(address.hash), - "CompilerVersion" => "", - "ContractName" => "", - "OptimizationUsed" => "" + "Address" => to_string(address.hash) } ] @@ -124,10 +119,7 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do assert response["result"] == [ %{ "ABI" => "Contract source code not verified", - "Address" => to_string(address.hash), - "CompilerVersion" => "", - "ContractName" => "", - "OptimizationUsed" => "" + "Address" => to_string(address.hash) } ] @@ -174,10 +166,7 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do assert response["result"] == [ %{ "ABI" => "Contract source code not verified", - "Address" => to_string(decompiled_smart_contract.address_hash), - "CompilerVersion" => "", - "ContractName" => "", - "OptimizationUsed" => "" + "Address" => to_string(decompiled_smart_contract.address_hash) } ] @@ -199,10 +188,7 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do assert response["result"] == [ %{ "ABI" => "Contract source code not verified", - "Address" => to_string(smart_contract.address_hash), - "CompilerVersion" => "", - "ContractName" => "", - "OptimizationUsed" => "" + "Address" => to_string(smart_contract.address_hash) } ] @@ -225,10 +211,7 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do assert %{ "ABI" => "Contract source code not verified", - "Address" => to_string(smart_contract.address_hash), - "CompilerVersion" => "", - "ContractName" => "", - "OptimizationUsed" => "" + "Address" => to_string(smart_contract.address_hash) } in response["result"] refute to_string(non_match.address_hash) in Enum.map(response["result"], &Map.get(&1, "Address")) @@ -251,10 +234,7 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do assert response["result"] == [ %{ "ABI" => "Contract source code not verified", - "Address" => to_string(contract_address.hash), - "CompilerVersion" => "", - "ContractName" => "", - "OptimizationUsed" => "" + "Address" => to_string(contract_address.hash) } ] @@ -281,10 +261,7 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do assert response["result"] == [ %{ "ABI" => "Contract source code not verified", - "Address" => to_string(contract_address.hash), - "CompilerVersion" => "", - "ContractName" => "", - "OptimizationUsed" => "" + "Address" => to_string(contract_address.hash) } ] @@ -423,7 +400,11 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do "CompilerVersion" => "", "OptimizationUsed" => "", "DecompiledSourceCode" => "", - "DecompilerVersion" => "" + "DecompilerVersion" => "", + "ConstructorArguments" => "", + "EVMVersion" => "", + "ExternalLibraries" => "", + "OptimizationRuns" => "" } ] @@ -439,7 +420,7 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do end test "with a verified contract address", %{conn: conn} do - contract = insert(:smart_contract, optimization: true) + contract = insert(:smart_contract, optimization: true, optimization_runs: 200, evm_version: "default") params = %{ "module" => "contract", @@ -456,12 +437,156 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do "ABI" => Jason.encode!(contract.abi), "ContractName" => contract.name, "CompilerVersion" => contract.compiler_version, - "DecompiledSourceCode" => "Contract source code not decompiled.", # The contract's optimization value is true, so the expected value # for `OptimizationUsed` is "1". If it was false, the expected value # would be "0". - "DecompilerVersion" => "", - "OptimizationUsed" => "1" + "OptimizationUsed" => "true", + "OptimizationRuns" => 200, + "EVMVersion" => "default" + } + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(getsourcecode_schema(), response) + end + + test "with constructor arguments", %{conn: conn} do + contract = + insert(:smart_contract, + optimization: true, + optimization_runs: 200, + evm_version: "default", + constructor_arguments: + "00000000000000000000000008e7592ce0d7ebabf42844b62ee6a878d4e1913e000000000000000000000000e1b6037da5f1d756499e184ca15254a981c92546" + ) + + params = %{ + "module" => "contract", + "action" => "getsourcecode", + "address" => to_string(contract.address_hash) + } + + expected_result = [ + %{ + "Address" => to_string(contract.address_hash), + "SourceCode" => + "/**\n* Submitted for verification at blockscout.com on #{contract.inserted_at}\n*/\n" <> + contract.contract_source_code, + "ABI" => Jason.encode!(contract.abi), + "ContractName" => contract.name, + "CompilerVersion" => contract.compiler_version, + "OptimizationUsed" => "true", + "OptimizationRuns" => 200, + "EVMVersion" => "default", + "ConstructorArguments" => + "00000000000000000000000008e7592ce0d7ebabf42844b62ee6a878d4e1913e000000000000000000000000e1b6037da5f1d756499e184ca15254a981c92546" + } + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(getsourcecode_schema(), response) + end + + test "with external library", %{conn: conn} do + smart_contract_bytecode = + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582040d82a7379b1ee1632ad4d8a239954fd940277b25628ead95259a85c5eddb2120029" + + created_contract_address = + insert( + :address, + hash: "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c", + contract_code: smart_contract_bytecode + ) + + transaction = + :transaction + |> insert() + |> with_block() + + insert( + :internal_transaction_create, + transaction: transaction, + index: 0, + created_contract_address: created_contract_address, + created_contract_code: smart_contract_bytecode, + block_number: transaction.block_number, + transaction_index: transaction.index + ) + + valid_attrs = %{ + address_hash: "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c", + name: "Test", + compiler_version: "0.4.23", + contract_source_code: + "pragma solidity ^0.4.23; contract SimpleStorage {uint storedData; function set(uint x) public {storedData = x; } function get() public constant returns (uint) {return storedData; } }", + abi: [ + %{ + "constant" => false, + "inputs" => [%{"name" => "x", "type" => "uint256"}], + "name" => "set", + "outputs" => [], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + }, + %{ + "constant" => true, + "inputs" => [], + "name" => "get", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + ], + optimization: true, + optimization_runs: 200, + evm_version: "default" + } + + external_libraries = [ + %SmartContract.ExternalLibrary{:address_hash => "0xb18aed9518d735482badb4e8b7fd8d2ba425ce95", :name => "Test"}, + %SmartContract.ExternalLibrary{:address_hash => "0x283539e1b1daf24cdd58a3e934d55062ea663c3f", :name => "Test2"} + ] + + {:ok, %SmartContract{} = contract} = Chain.create_smart_contract(valid_attrs, external_libraries) + + params = %{ + "module" => "contract", + "action" => "getsourcecode", + "address" => to_string(contract.address_hash) + } + + expected_result = [ + %{ + "Address" => to_string(contract.address_hash), + "SourceCode" => + "/**\n* Submitted for verification at blockscout.com on #{contract.inserted_at}\n*/\n" <> + contract.contract_source_code, + "ABI" => Jason.encode!(contract.abi), + "ContractName" => contract.name, + "CompilerVersion" => contract.compiler_version, + "OptimizationUsed" => "true", + "OptimizationRuns" => 200, + "EVMVersion" => "default", + "ExternalLibraries" => [ + %{"name" => "Test", "address_hash" => "0xb18aed9518d735482badb4e8b7fd8d2ba425ce95"}, + %{"name" => "Test2", "address_hash" => "0x283539e1b1daf24cdd58a3e934d55062ea663c3f"} + ] } ] @@ -508,9 +633,8 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do "ABI" => Jason.encode!(contract_code_info.abi), "ContractName" => contract_code_info.name, "CompilerVersion" => contract_code_info.version, - "DecompiledSourceCode" => "Contract source code not decompiled.", - "DecompilerVersion" => "", - "OptimizationUsed" => "0" + "OptimizationUsed" => "false", + "EVMVersion" => nil } assert response["status"] == "1" @@ -578,9 +702,9 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do contract_source_code assert result["ContractName"] == name - assert result["DecompiledSourceCode"] == "Contract source code not decompiled." - assert result["DecompilerVersion"] == "" - assert result["OptimizationUsed"] == "1" + assert result["DecompiledSourceCode"] == nil + assert result["DecompilerVersion"] == nil + assert result["OptimizationUsed"] == "true" assert :ok = ExJsonSchema.Validator.validate(verify_schema(), response) end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/stats_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/stats_controller_test.exs index a57db2221b..c20c3f128d 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/stats_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/stats_controller_test.exs @@ -116,7 +116,7 @@ defmodule BlockScoutWeb.API.RPC.StatsControllerTest do |> get("/api", params) |> json_response(200) - assert response["result"] == "6" + assert response["result"] == "0" assert response["status"] == "1" assert response["message"] == "OK" assert :ok = ExJsonSchema.Validator.validate(ethsupply_schema(), response) diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 9576eca1a6..401d3b8cbe 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -2922,7 +2922,7 @@ defmodule Explorer.Chain do """ @spec total_supply :: non_neg_integer() | nil def total_supply do - supply_module().total() + supply_module().total() || 0 end @doc """ @@ -2967,21 +2967,33 @@ defmodule Explorer.Chain do ) :: {:ok, accumulator} when accumulator: term() def stream_unfetched_token_instances(initial, reducer) when is_function(reducer, 2) do + nft_tokens = + from( + token in Token, + where: token.type == ^"ERC-721", + select: token.contract_address_hash + ) + query = from( token_transfer in TokenTransfer, - inner_join: token in Token, + inner_join: token in subquery(nft_tokens), on: token.contract_address_hash == token_transfer.token_contract_address_hash, left_join: instance in Instance, on: token_transfer.token_id == instance.token_id and token_transfer.token_contract_address_hash == instance.token_contract_address_hash, - where: token.type == ^"ERC-721" and is_nil(instance.token_id) and not is_nil(token_transfer.token_id), - distinct: [token_transfer.token_contract_address_hash, token_transfer.token_id], + where: is_nil(instance.token_id) and not is_nil(token_transfer.token_id), select: %{contract_address_hash: token_transfer.token_contract_address_hash, token_id: token_transfer.token_id} ) - Repo.stream_reduce(query, initial, reducer) + distinct_query = + from( + q in subquery(query), + distinct: [q.contract_address_hash, q.token_id] + ) + + Repo.stream_reduce(distinct_query, initial, reducer) end @doc """ diff --git a/apps/explorer/lib/explorer/chain/cache/address_sum.ex b/apps/explorer/lib/explorer/chain/cache/address_sum.ex index b0d3326312..107932b730 100644 --- a/apps/explorer/lib/explorer/chain/cache/address_sum.ex +++ b/apps/explorer/lib/explorer/chain/cache/address_sum.ex @@ -20,7 +20,7 @@ defmodule Explorer.Chain.Cache.AddressSum do # See next `handle_fallback` definition get_async_task() - {:return, nil} + {:return, Decimal.new(0)} end defp handle_fallback(:async_task) do diff --git a/apps/explorer/lib/explorer/counters/average_block_time.ex b/apps/explorer/lib/explorer/counters/average_block_time.ex index 1065ede2bd..ab49650030 100644 --- a/apps/explorer/lib/explorer/counters/average_block_time.ex +++ b/apps/explorer/lib/explorer/counters/average_block_time.ex @@ -63,24 +63,25 @@ defmodule Explorer.Counters.AverageBlockTime do defp refresh_timestamps do timestamps_query = - from(block in Block, - limit: 100, - offset: 0, - order_by: [desc: block.number], - select: {block.number, block.timestamp} - ) - - query = if Application.get_env(:explorer, :include_uncles_in_average_block_time) do - timestamps_query + from(block in Block, + limit: 100, + offset: 100, + order_by: [desc: block.number], + select: {block.number, block.timestamp} + ) else - from(block in timestamps_query, - where: block.consensus == true + from(block in Block, + limit: 100, + offset: 100, + order_by: [desc: block.number], + where: block.consensus == true, + select: {block.number, block.timestamp} ) end timestamps = - query + timestamps_query |> Repo.all() |> Enum.sort_by(fn {_, timestamp} -> timestamp end, &>=/2) |> Enum.map(fn {number, timestamp} -> diff --git a/apps/explorer/lib/explorer/token/instance_metadata_retriever.ex b/apps/explorer/lib/explorer/token/instance_metadata_retriever.ex index bdddfde482..bde63ee1e9 100644 --- a/apps/explorer/lib/explorer/token/instance_metadata_retriever.ex +++ b/apps/explorer/lib/explorer/token/instance_metadata_retriever.ex @@ -80,7 +80,7 @@ defmodule Explorer.Token.InstanceMetadataRetriever do def fetch_json(%{"tokenURI" => {:ok, [json]}}) do {:ok, json} = decode_json(json) - {:ok, %{metadata: json}} + check_type(json) rescue e -> Logger.debug(["Unknown metadata format #{inspect(json)}. error #{inspect(e)}"], @@ -101,11 +101,7 @@ defmodule Explorer.Token.InstanceMetadataRetriever do {:ok, %Response{body: body, status_code: 200}} -> {:ok, json} = decode_json(body) - if is_map(json) do - {:ok, %{metadata: json}} - else - {:error, :wrong_metadata_type} - end + check_type(json) {:ok, %Response{body: body}} -> {:error, body} @@ -131,4 +127,12 @@ defmodule Explorer.Token.InstanceMetadataRetriever do |> Jason.decode() end end + + defp check_type(json) when is_map(json) do + {:ok, %{metadata: json}} + end + + defp check_type(_) do + {:error, :wrong_metadata_type} + end end diff --git a/apps/explorer/test/explorer/chain/cache/address_sum_test.exs b/apps/explorer/test/explorer/chain/cache/address_sum_test.exs index 707c70635f..9d7cedaec0 100644 --- a/apps/explorer/test/explorer/chain/cache/address_sum_test.exs +++ b/apps/explorer/test/explorer/chain/cache/address_sum_test.exs @@ -12,7 +12,7 @@ defmodule Explorer.Chain.Cache.AddressSumTest do test "returns default address sum" do result = AddressSum.get_sum() - assert is_nil(result) + assert result == Decimal.new(0) end test "updates cache if initial value is zero" do diff --git a/apps/explorer/test/explorer/counters/average_block_time_test.exs b/apps/explorer/test/explorer/counters/average_block_time_test.exs index 5eb00d4c73..82db1f5af4 100644 --- a/apps/explorer/test/explorer/counters/average_block_time_test.exs +++ b/apps/explorer/test/explorer/counters/average_block_time_test.exs @@ -34,11 +34,19 @@ defmodule Explorer.Counters.AverageBlockTimeTest do first_timestamp = Timex.now() - insert(:block, number: block_number, consensus: true, timestamp: Timex.shift(first_timestamp, seconds: 3)) - insert(:block, number: block_number, consensus: false, timestamp: Timex.shift(first_timestamp, seconds: 9)) - insert(:block, number: block_number, consensus: false, timestamp: Timex.shift(first_timestamp, seconds: 6)) + insert(:block, number: block_number, consensus: true, timestamp: Timex.shift(first_timestamp, seconds: -100 - 3)) + insert(:block, number: block_number, consensus: false, timestamp: Timex.shift(first_timestamp, seconds: -100 - 9)) + insert(:block, number: block_number, consensus: false, timestamp: Timex.shift(first_timestamp, seconds: -100 - 6)) + + Enum.each(1..100, fn i -> + insert(:block, + number: block_number + i, + consensus: true, + timestamp: Timex.shift(first_timestamp, seconds: -(101 - i) - 9) + ) + end) - assert Repo.aggregate(Block, :count, :hash) == 3 + assert Repo.aggregate(Block, :count, :hash) == 103 AverageBlockTime.refresh() @@ -55,6 +63,14 @@ defmodule Explorer.Counters.AverageBlockTimeTest do insert(:block, number: block_number, consensus: false, timestamp: Timex.shift(first_timestamp, seconds: 4)) insert(:block, number: block_number + 1, consensus: true, timestamp: Timex.shift(first_timestamp, seconds: 5)) + Enum.each(1..100, fn i -> + insert(:block, + number: block_number + i + 1, + consensus: true, + timestamp: Timex.shift(first_timestamp, seconds: -(101 - i) - 5) + ) + end) + AverageBlockTime.refresh() assert AverageBlockTime.average_block_time() == Timex.Duration.parse!("PT2S") @@ -69,6 +85,14 @@ defmodule Explorer.Counters.AverageBlockTimeTest do insert(:block, number: block_number, consensus: false, timestamp: Timex.shift(first_timestamp, seconds: 4)) insert(:block, number: block_number + 1, consensus: true, timestamp: Timex.shift(first_timestamp, seconds: 5)) + Enum.each(1..100, fn i -> + insert(:block, + number: block_number + i + 1, + consensus: true, + timestamp: Timex.shift(first_timestamp, seconds: -(101 - i) - 5) + ) + end) + AverageBlockTime.refresh() assert AverageBlockTime.average_block_time() == Timex.Duration.parse!("PT1S") @@ -83,7 +107,15 @@ defmodule Explorer.Counters.AverageBlockTimeTest do insert(:block, number: block_number + 2, consensus: true, timestamp: Timex.shift(first_timestamp, seconds: 9)) insert(:block, number: block_number + 1, consensus: true, timestamp: Timex.shift(first_timestamp, seconds: 6)) - assert Repo.aggregate(Block, :count, :hash) == 3 + Enum.each(1..100, fn i -> + insert(:block, + number: block_number + i + 2, + consensus: true, + timestamp: Timex.shift(first_timestamp, seconds: -(101 - i) - 9) + ) + end) + + assert Repo.aggregate(Block, :count, :hash) == 103 AverageBlockTime.refresh() diff --git a/apps/indexer/test/indexer/fetcher/coin_balance_on_demand_test.exs b/apps/indexer/test/indexer/fetcher/coin_balance_on_demand_test.exs index 985b976a81..8f0ae262e4 100644 --- a/apps/indexer/test/indexer/fetcher/coin_balance_on_demand_test.exs +++ b/apps/indexer/test/indexer/fetcher/coin_balance_on_demand_test.exs @@ -41,15 +41,18 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemandTest do # we space these very far apart so that we know it will consider the 0th block stale (it calculates how far # back we'd need to go to get 24 hours in the past) - insert(:block, number: 0, timestamp: Timex.shift(now, hours: -50)) - insert(:block, number: 1, timestamp: now) + Enum.each(0..100, fn i -> + insert(:block, number: i, timestamp: Timex.shift(now, hours: -(101 - i) * 50)) + end) + + insert(:block, number: 101, timestamp: now) AverageBlockTime.refresh() - stale_address = insert(:address, fetched_coin_balance: 1, fetched_coin_balance_block_number: 0) - current_address = insert(:address, fetched_coin_balance: 1, fetched_coin_balance_block_number: 1) + stale_address = insert(:address, fetched_coin_balance: 1, fetched_coin_balance_block_number: 100) + current_address = insert(:address, fetched_coin_balance: 1, fetched_coin_balance_block_number: 101) - pending_address = insert(:address, fetched_coin_balance: 1, fetched_coin_balance_block_number: 1) - insert(:unfetched_balance, address_hash: pending_address.hash, block_number: 2) + pending_address = insert(:address, fetched_coin_balance: 1, fetched_coin_balance_block_number: 101) + insert(:unfetched_balance, address_hash: pending_address.hash, block_number: 102) %{stale_address: stale_address, current_address: current_address, pending_address: pending_address} end @@ -63,7 +66,7 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemandTest do test "if the address has not been fetched within the last 24 hours of blocks it is considered stale", %{ stale_address: address } do - assert CoinBalanceOnDemand.trigger_fetch(address) == {:stale, 1} + assert CoinBalanceOnDemand.trigger_fetch(address) == {:stale, 101} end test "if the address has been fetched within the last 24 hours of blocks it is considered current", %{ @@ -75,7 +78,7 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemandTest do test "if there is an unfetched balance within the window for an address, it is considered pending", %{ pending_address: pending_address } do - assert CoinBalanceOnDemand.trigger_fetch(pending_address) == {:pending, 2} + assert CoinBalanceOnDemand.trigger_fetch(pending_address) == {:pending, 102} end end @@ -88,15 +91,18 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemandTest do # we space these very far apart so that we know it will consider the 0th block stale (it calculates how far # back we'd need to go to get 24 hours in the past) - insert(:block, number: 0, timestamp: Timex.shift(now, hours: -50)) - insert(:block, number: 1, timestamp: now) + Enum.each(0..100, fn i -> + insert(:block, number: i, timestamp: Timex.shift(now, hours: -(101 - i) * 50)) + end) + + insert(:block, number: 101, timestamp: now) AverageBlockTime.refresh() :ok end test "a stale address broadcasts the new address" do - address = insert(:address, fetched_coin_balance: 1, fetched_coin_balance_block_number: 0) + address = insert(:address, fetched_coin_balance: 1, fetched_coin_balance_block_number: 100) address_hash = address.hash string_address_hash = to_string(address.hash) @@ -104,26 +110,26 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemandTest do %{ id: id, method: "eth_getBalance", - params: [^string_address_hash, "0x1"] + params: [^string_address_hash, "0x65"] } ], _options -> {:ok, [%{id: id, jsonrpc: "2.0", result: "0x02"}]} end) - assert CoinBalanceOnDemand.trigger_fetch(address) == {:stale, 1} + assert CoinBalanceOnDemand.trigger_fetch(address) == {:stale, 101} {:ok, expected_wei} = Wei.cast(2) assert_receive( {:chain_event, :addresses, :on_demand, - [%{hash: ^address_hash, fetched_coin_balance: ^expected_wei, fetched_coin_balance_block_number: 1}]} + [%{hash: ^address_hash, fetched_coin_balance: ^expected_wei, fetched_coin_balance_block_number: 101}]} ) end test "a pending address broadcasts the new address and the new coin balance" do - address = insert(:address, fetched_coin_balance: 0, fetched_coin_balance_block_number: 1) - insert(:unfetched_balance, address_hash: address.hash, block_number: 2) + address = insert(:address, fetched_coin_balance: 0, fetched_coin_balance_block_number: 101) + insert(:unfetched_balance, address_hash: address.hash, block_number: 102) address_hash = address.hash string_address_hash = to_string(address.hash) @@ -131,20 +137,20 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemandTest do %{ id: id, method: "eth_getBalance", - params: [^string_address_hash, "0x2"] + params: [^string_address_hash, "0x66"] } ], _options -> {:ok, [%{id: id, jsonrpc: "2.0", result: "0x02"}]} end) - assert CoinBalanceOnDemand.trigger_fetch(address) == {:pending, 2} + assert CoinBalanceOnDemand.trigger_fetch(address) == {:pending, 102} {:ok, expected_wei} = Wei.cast(2) assert_receive( {:chain_event, :addresses, :on_demand, - [%{hash: ^address_hash, fetched_coin_balance: ^expected_wei, fetched_coin_balance_block_number: 2}]} + [%{hash: ^address_hash, fetched_coin_balance: ^expected_wei, fetched_coin_balance_block_number: 102}]} ) end end