diff --git a/CHANGELOG.md b/CHANGELOG.md index dd02ff334f..de13ae4444 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,32 @@ ### Fixes +### Chore + + +## 1.3.6-beta + +### Features + + - [#1589](https://github.com/poanetwork/blockscout/pull/1589) - RPC endpoint to list addresses + - [#1567](https://github.com/poanetwork/blockscout/pull/1567) - Allow setting different configuration just for realtime fetcher + - [#1562](https://github.com/poanetwork/blockscout/pull/1562) - Add incoming transactions count to contract view + +### Fixes + + - [#1595](https://github.com/poanetwork/blockscout/pull/1595) - Reduce block_rewards in the catchup fetcher + - [#1590](https://github.com/poanetwork/blockscout/pull/1590) - Added guard for fetching blocks with invalid number + - [#1588](https://github.com/poanetwork/blockscout/pull/1588) - Fix usd value on address page + - [#1586](https://github.com/poanetwork/blockscout/pull/1586) - Exact timestamp display + - [#1581](https://github.com/poanetwork/blockscout/pull/1581) - Consider `creates` param when fetching transactions + - [#1559](https://github.com/poanetwork/blockscout/pull/1559) - Change v column type for Transactions table + +### Chore + + - [#1579](https://github.com/poanetwork/blockscout/pull/1579) - Add SpringChain to the list of Additional Chains Utilizing BlockScout + - [#1578](https://github.com/poanetwork/blockscout/pull/1578) - Refine contributing procedure + - [#1572](https://github.com/poanetwork/blockscout/pull/1572) - Add option to disable block rewards in indexer config + ## 1.3.5-beta diff --git a/README.md b/README.md index 152ec62d02..bb4e3a249b 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,8 @@ Currently available block explorers (i.e. Etherscan and Etherchain) are closed s * [ARTIS](https://explorer.sigma1.artis.network) * [SafeChain](https://explorer.safechain.io) * [SpringChain](https://explorer.springrole.com/) +* [PIRL](http://pirl.es/) + ### Visual Interface diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex index c2922e870c..76ddf4342c 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex @@ -4,6 +4,20 @@ defmodule BlockScoutWeb.API.RPC.AddressController do alias Explorer.{Chain, Etherscan} alias Explorer.Chain.{Address, Wei} + def listaccounts(conn, params) do + options = + params + |> optional_params() + |> Map.put_new(:page_number, 0) + |> Map.put_new(:page_size, 10) + + accounts = list_accounts(options) + + conn + |> put_status(200) + |> render(:listaccounts, %{accounts: accounts}) + end + def balance(conn, params, template \\ :balance) do with {:address_param, {:ok, address_param}} <- fetch_address(params), {:format, {:ok, address_hashes}} <- to_address_hashes(address_param) do @@ -260,6 +274,13 @@ defmodule BlockScoutWeb.API.RPC.AddressController do Enum.any?(address_hashes, &(&1 == :error)) end + defp list_accounts(%{page_number: page_number, page_size: page_size}) do + offset = (max(page_number, 1) - 1) * page_size + + # limit is just page_size + Chain.list_ordered_addresses(offset, page_size) + end + defp hashes_to_addresses(address_hashes) do address_hashes |> Chain.hashes_to_addresses() 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 e8b14869cc..e2282f74fe 100644 --- a/apps/block_scout_web/lib/block_scout_web/etherscan.ex +++ b/apps/block_scout_web/lib/block_scout_web/etherscan.ex @@ -168,6 +168,17 @@ defmodule BlockScoutWeb.Etherscan do ] } + @account_listaccounts_example_value %{ + "status" => "1", + "message" => "OK", + "result" => [ + %{ + "address" => "0x0000000000000000000000000000000000000000", + "balance" => "135499" + } + ] + } + @account_getminedblocks_example_value_error %{ "status" => "0", "message" => "No blocks found", @@ -720,6 +731,14 @@ defmodule BlockScoutWeb.Etherscan do } } + @account_model %{ + name: "Account", + fields: %{ + "address" => @address_hash_type, + "balance" => @wei_type + } + } + @contract_model %{ name: "Contract", fields: %{ @@ -1289,6 +1308,50 @@ defmodule BlockScoutWeb.Etherscan do ] } + @account_listaccounts_action %{ + name: "listaccounts", + description: + "Get a list of accounts and their balances, sorted ascending by the time they were first seen by the explorer.", + required_params: [], + optional_params: [ + %{ + key: "page", + type: "integer", + description: + "A nonnegative integer that represents the page number to be used for pagination. 'offset' must be provided in conjunction." + }, + %{ + key: "offset", + type: "integer", + description: + "A nonnegative integer that represents the maximum number of records to return when paginating. 'page' must be provided in conjunction." + } + ], + responses: [ + %{ + code: "200", + description: "successful operation", + example_value: Jason.encode!(@account_listaccounts_example_value), + model: %{ + name: "Result", + fields: %{ + status: @status_type, + message: @message_type, + result: %{ + type: "array", + array_type: @account_model + } + } + } + }, + %{ + code: "200", + description: "error", + example_value: Jason.encode!(@account_getminedblocks_example_value_error) + } + ] + } + @logs_getlogs_action %{ name: "getLogs", description: "Get event logs for an address and/or topics. Up to a maximum of 1,000 event logs.", @@ -1767,7 +1830,8 @@ defmodule BlockScoutWeb.Etherscan do @account_tokentx_action, @account_tokenbalance_action, @account_tokenlist_action, - @account_getminedblocks_action + @account_getminedblocks_action, + @account_listaccounts_action ] } 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 eb6233b033..f614f3cb88 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 @@ -3,6 +3,11 @@ defmodule BlockScoutWeb.API.RPC.AddressView do alias BlockScoutWeb.API.RPC.RPCView + def render("listaccounts.json", %{accounts: accounts}) do + accounts = Enum.map(accounts, &prepare_account/1) + RPCView.render("show.json", data: accounts) + end + def render("balance.json", %{addresses: [address]}) do RPCView.render("show.json", data: "#{address.fetched_coin_balance.value}") end @@ -56,6 +61,13 @@ defmodule BlockScoutWeb.API.RPC.AddressView do RPCView.render("error.json", assigns) end + defp prepare_account(address) do + %{ + "balance" => to_string(address.fetched_coin_balance.value), + "address" => to_string(address.hash) + } + end + defp prepare_transaction(transaction) do %{ "blockNumber" => "#{transaction.block_number}", 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 2e42c9829c..30cdbd84b0 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 @@ -6,6 +6,49 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do alias Explorer.Chain.{Transaction, Wei} alias BlockScoutWeb.API.RPC.AddressController + describe "listaccounts" do + setup do + %{params: %{"module" => "account", "action" => "listaccounts"}} + end + + test "with no addresses", %{params: params, conn: conn} do + response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] == "OK" + assert response["status"] == "1" + assert response["result"] == [] + end + + test "with existing addresses", %{params: params, conn: conn} do + first_address = insert(:address, fetched_coin_balance: 10, inserted_at: Timex.shift(Timex.now(), minutes: -10)) + second_address = insert(:address, fetched_coin_balance: 100, inserted_at: Timex.shift(Timex.now(), minutes: -5)) + first_address_hash = to_string(first_address.hash) + second_address_hash = to_string(second_address.hash) + + response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] == "OK" + assert response["status"] == "1" + + assert [ + %{ + "address" => ^first_address_hash, + "balance" => "10" + }, + %{ + "address" => ^second_address_hash, + "balance" => "100" + } + ] = response["result"] + end + end + describe "balance" do test "with missing address hash", %{conn: conn} do params = %{ diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex index a8ccafb0a2..604c7b2235 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex @@ -149,23 +149,25 @@ defmodule EthereumJSONRPC.Transaction do elixir_to_params(%{transaction | "input" => "0x"}) end - def elixir_to_params(%{ - "blockHash" => block_hash, - "blockNumber" => block_number, - "from" => from_address_hash, - "gas" => gas, - "gasPrice" => gas_price, - "hash" => hash, - "input" => input, - "nonce" => nonce, - "r" => r, - "s" => s, - "to" => to_address_hash, - "transactionIndex" => index, - "v" => v, - "value" => value - }) do - %{ + def elixir_to_params( + %{ + "blockHash" => block_hash, + "blockNumber" => block_number, + "from" => from_address_hash, + "gas" => gas, + "gasPrice" => gas_price, + "hash" => hash, + "input" => input, + "nonce" => nonce, + "r" => r, + "s" => s, + "to" => to_address_hash, + "transactionIndex" => index, + "v" => v, + "value" => value + } = transaction + ) do + result = %{ block_hash: block_hash, block_number: block_number, from_address_hash: from_address_hash, @@ -182,6 +184,12 @@ defmodule EthereumJSONRPC.Transaction do value: value, transaction_index: index } + + if transaction["creates"] do + Map.put(result, :created_contract_address_hash, transaction["creates"]) + else + result + end end # Ganache bug. it return `to: "0x0"` except of `to: null` diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transactions.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transactions.ex index cae14b1cd8..e056fb12c2 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transactions.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transactions.ex @@ -56,7 +56,8 @@ defmodule EthereumJSONRPC.Transactions do to_address_hash: nil, v: "0xbd", value: 0, - transaction_index: 0 + transaction_index: 0, + created_contract_address_hash: "0xffc87239eb0267bc3ca2cd51d12fbf278e02ccb4" } ] diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index a28b6aac96..58b6e1a34b 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -724,6 +724,19 @@ defmodule Explorer.Chain do Repo.all(query) end + @spec list_ordered_addresses(non_neg_integer(), non_neg_integer()) :: [Address.t()] + def list_ordered_addresses(offset, limit) do + query = + from( + address in Address, + order_by: [asc: address.inserted_at], + offset: ^offset, + limit: ^limit + ) + + Repo.all(query) + end + def find_contract_address(%Hash{byte_count: unquote(Hash.Address.byte_count())} = hash) do query = from( diff --git a/apps/explorer/priv/repo/migrations/20190318151809_add_inserted_at_index_to_accounts.exs b/apps/explorer/priv/repo/migrations/20190318151809_add_inserted_at_index_to_accounts.exs new file mode 100644 index 0000000000..f1ac257565 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20190318151809_add_inserted_at_index_to_accounts.exs @@ -0,0 +1,7 @@ +defmodule Explorer.Repo.Migrations.AddInsertedAtIndexToAccounts do + use Ecto.Migration + + def change do + create(index(:addresses, :inserted_at)) + end +end diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index 2ad5f9bd0d..73e4643a62 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -14,7 +14,7 @@ defmodule Indexer.Block.Fetcher do alias Indexer.{AddressExtraction, CoinBalance, MintTransfer, ReplacedTransaction, Token, TokenTransfers, Tracer} alias Indexer.Address.{CoinBalances, TokenBalances} alias Indexer.Block.Fetcher.Receipts - alias Indexer.Block.Transform + alias Indexer.Block.{Reward, Transform} @type address_hash_to_fetched_balance_block_number :: %{String.t() => Block.block_number()} @@ -127,7 +127,10 @@ defmodule Indexer.Block.Fetcher do transactions_params: transactions_with_receipts } |> CoinBalances.params_set(), - beneficiaries_with_gas_payment <- add_gas_payments(beneficiary_params_set, transactions_with_receipts), + beneficiaries_with_gas_payment <- + beneficiary_params_set + |> add_gas_payments(transactions_with_receipts) + |> Reward.Fetcher.reduce_uncle_rewards(), address_token_balances = TokenBalances.params_set(%{token_transfers_params: token_transfers}), {:ok, inserted} <- __MODULE__.import( diff --git a/apps/indexer/lib/indexer/block/fetcher/receipts.ex b/apps/indexer/lib/indexer/block/fetcher/receipts.ex index 1257d8db2a..f38676cf12 100644 --- a/apps/indexer/lib/indexer/block/fetcher/receipts.ex +++ b/apps/indexer/lib/indexer/block/fetcher/receipts.ex @@ -42,7 +42,14 @@ defmodule Indexer.Block.Fetcher.Receipts do end) Enum.map(transactions_params, fn %{hash: transaction_hash} = transaction_params -> - Map.merge(transaction_params, Map.fetch!(transaction_hash_to_receipt_params, transaction_hash)) + receipts_params = Map.fetch!(transaction_hash_to_receipt_params, transaction_hash) + merged_params = Map.merge(transaction_params, receipts_params) + + if transaction_params[:created_contract_address_hash] && is_nil(receipts_params[:created_contract_address_hash]) do + Map.put(merged_params, :created_contract_address_hash, transaction_params[:created_contract_address_hash]) + else + merged_params + end end) end diff --git a/apps/indexer/lib/indexer/block/realtime/consensus_ensurer.ex b/apps/indexer/lib/indexer/block/realtime/consensus_ensurer.ex index 363f54fbed..816f1d76bf 100644 --- a/apps/indexer/lib/indexer/block/realtime/consensus_ensurer.ex +++ b/apps/indexer/lib/indexer/block/realtime/consensus_ensurer.ex @@ -9,6 +9,8 @@ defmodule Indexer.Block.Realtime.ConsensusEnsurer do alias Explorer.Chain.Hash alias Indexer.Block.Realtime.Fetcher + def perform(_, number, _) when not is_integer(number) or number < 0, do: :ok + def perform(%Hash{byte_count: unquote(Hash.Full.byte_count())} = block_hash, number, block_fetcher) do case Chain.hash_to_block(block_hash) do {:ok, %{consensus: true} = _block} -> diff --git a/apps/indexer/lib/indexer/block/reward/fetcher.ex b/apps/indexer/lib/indexer/block/reward/fetcher.ex index 88a9f9391d..2329392a7b 100644 --- a/apps/indexer/lib/indexer/block/reward/fetcher.ex +++ b/apps/indexer/lib/indexer/block/reward/fetcher.ex @@ -204,7 +204,7 @@ defmodule Indexer.Block.Reward.Fetcher do end) end - defp reduce_uncle_rewards(beneficiaries_params) do + def reduce_uncle_rewards(beneficiaries_params) do beneficiaries_params |> Enum.reduce([], fn %{address_type: address_type} = beneficiary, acc -> current = diff --git a/apps/indexer/test/indexer/block/fetcher_test.exs b/apps/indexer/test/indexer/block/fetcher_test.exs index e691b4420b..542b278df2 100644 --- a/apps/indexer/test/indexer/block/fetcher_test.exs +++ b/apps/indexer/test/indexer/block/fetcher_test.exs @@ -609,6 +609,102 @@ defmodule Indexer.Block.FetcherTest do raise ArgumentError, "Unsupported variant (#{variant})" end end + + @tag :no_geth + test "correctly imports blocks with multiple uncle rewards for the same address", %{ + block_fetcher: %Fetcher{json_rpc_named_arguments: json_rpc_named_arguments} = block_fetcher + } do + block_number = 7_374_455 + + if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do + EthereumJSONRPC.Mox + |> expect(:json_rpc, 2, fn requests, _options -> + {:ok, + Enum.map(requests, fn + %{id: id, method: "eth_getBlockByNumber", params: ["0x708677", true]} -> + %{ + id: id, + result: %{ + "author" => "0x5a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c", + "difficulty" => "0x6bc767dd80781", + "extraData" => "0x5050594520737061726b706f6f6c2d6574682d7477", + "gasLimit" => "0x7a121d", + "gasUsed" => "0x79cbe9", + "hash" => "0x1b6fb99af0b51af6685a191b2f7bcba684f8565629bf084c70b2530479407455", + "logsBloom" => + "0x044d42d008801488400e1809190200a80d06105bc0c4100b047895c0d518327048496108388040140010b8208006288102e206160e21052322440924002090c1c808a0817405ab238086d028211014058e949401012403210314896702d06880c815c3060a0f0809987c81044488292cc11d57882c912a808ca10471c84460460040000c0001012804022000a42106591881d34407420ba401e1c08a8d00a000a34c11821a80222818a4102152c8a0c044032080c6462644223104d618e0e544072008120104408205c60510542264808488220403000106281a0290404220112c10b080145028c8000300b18a2c8280701c882e702210b00410834840108084", + "miner" => "0x5a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c", + "mixHash" => "0xda53ae7c2b3c529783d6cdacdb90587fd70eb651c0f04253e8ff17de97844010", + "nonce" => "0x0946e5f01fce12bc", + "number" => "0x708677", + "parentHash" => "0x62543e836e0ef7edfa9e38f26526092c4be97efdf5ba9e0f53a4b0b7d5bc930a", + "receiptsRoot" => "0xa7d2b82bd8526de11736c18bd5cc8cfe2692106c4364526f3310ad56d78669c4", + "sealFields" => [ + "0xa0da53ae7c2b3c529783d6cdacdb90587fd70eb651c0f04253e8ff17de97844010", + "0x880946e5f01fce12bc" + ], + "sha3Uncles" => "0x483a8a21a5825ad270f358b3ea56e060bbb8b3082d9a92ec8fa17a5c7e6fc1b6", + "size" => "0x544c", + "stateRoot" => "0x85daa9cd528004c1609d4cb3520fd958e85983bb4183124a4a9f7137fd39c691", + "timestamp" => "0x5c8bc76e", + "totalDifficulty" => "0x201a42c35142ae94458", + "transactions" => [], + "transactionsRoot" => "0xcd6c12fa43cd4e92ad5c0bf232b30488bbcbfe273c5b4af0366fced0767d54db", + "uncles" => [] + } + } + + %{id: id, method: "trace_block"} -> + %{ + id: id, + result: [ + %{ + "action" => %{ + "author" => "0x5a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c", + "rewardType" => "block", + "value" => "0x1d7d843dc3b48000" + }, + "blockHash" => "0x1b6fb99af0b51af6685a191b2f7bcba684f8565629bf084c70b2530479407455", + "blockNumber" => block_number, + "subtraces" => 0, + "traceAddress" => [], + "type" => "reward" + }, + %{ + "action" => %{ + "author" => "0xea674fdde714fd979de3edf0f56aa9716b898ec8", + "rewardType" => "uncle", + "value" => "0x14d1120d7b160000" + }, + "blockHash" => "0x1b6fb99af0b51af6685a191b2f7bcba684f8565629bf084c70b2530479407455", + "blockNumber" => block_number, + "subtraces" => 0, + "traceAddress" => [], + "type" => "reward" + }, + %{ + "action" => %{ + "author" => "0xea674fdde714fd979de3edf0f56aa9716b898ec8", + "rewardType" => "uncle", + "value" => "0x18493fba64ef0000" + }, + "blockHash" => "0x1b6fb99af0b51af6685a191b2f7bcba684f8565629bf084c70b2530479407455", + "blockNumber" => block_number, + "subtraces" => 0, + "traceAddress" => [], + "type" => "reward" + } + ] + } + end)} + end) + end + + assert {:ok, %{errors: [], inserted: %{block_rewards: block_rewards}}} = + Fetcher.fetch_and_import_range(block_fetcher, block_number..block_number) + + assert Repo.one!(select(Chain.Block.Reward, fragment("COUNT(*)"))) == 2 + end end defp wait_until(timeout, producer) do