From 5bc627d368326ed966b8783b4daf5587c7e8731b Mon Sep 17 00:00:00 2001 From: zachdaniel Date: Mon, 18 Mar 2019 11:52:14 -0400 Subject: [PATCH] feat: rpc address to list addresses --- .../controllers/api/rpc/address_controller.ex | 21 ++++++ .../lib/block_scout_web/etherscan.ex | 66 ++++++++++++++++++- .../views/api/rpc/address_view.ex | 12 ++++ .../api/rpc/address_controller_test.exs | 43 ++++++++++++ apps/explorer/lib/explorer/chain.ex | 13 ++++ ...1809_add_inserted_at_index_to_accounts.exs | 7 ++ 6 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 apps/explorer/priv/repo/migrations/20190318151809_add_inserted_at_index_to_accounts.exs 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/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