Merge pull request #825 from poanetwork/sa-api-account-tokenlist

account#tokenlist API endpoint
pull/826/head
Luke Imhoff 6 years ago committed by GitHub
commit 1bb756a8f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 24
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex
  2. 67
      apps/block_scout_web/lib/block_scout_web/etherscan.ex
  3. 15
      apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex
  4. 126
      apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs
  5. 25
      apps/explorer/lib/explorer/etherscan.ex
  6. 82
      apps/explorer/test/explorer/etherscan_test.exs

@ -110,6 +110,23 @@ defmodule BlockScoutWeb.API.RPC.AddressController do
end
end
def tokenlist(conn, params) do
with {:address_param, {:ok, address_param}} <- fetch_address(params),
{:format, {:ok, address_hash}} <- to_address_hash(address_param),
{:ok, token_list} <- list_tokens(address_hash) do
render(conn, :token_list, %{token_list: token_list})
else
{:address_param, :error} ->
render(conn, :error, error: "Query parameter address is required")
{:format, :error} ->
render(conn, :error, error: "Invalid address format")
{:error, :not_found} ->
render(conn, :error, error: "No tokens found", data: [])
end
end
def getminedblocks(conn, params) do
options = put_pagination_options(%{}, params)
@ -364,4 +381,11 @@ defmodule BlockScoutWeb.API.RPC.AddressController do
token_balance -> token_balance.value
end
end
defp list_tokens(address_hash) do
case Etherscan.list_tokens(address_hash) do
[] -> {:error, :not_found}
token_list -> {:ok, token_list}
end
end
end

@ -142,6 +142,20 @@ defmodule BlockScoutWeb.Etherscan do
"result" => nil
}
@account_tokenlist_example_value %{
"status" => "1",
"message" => "OK",
"result" => [
%{
"balance" => "135499",
"contractAddress" => "0x0000000000000000000000000000000000000000",
"name" => "Example Token",
"decimals" => "18",
"symbol" => "ET"
}
]
}
@account_getminedblocks_example_value %{
"status" => "1",
"message" => "OK",
@ -625,6 +639,21 @@ defmodule BlockScoutWeb.Etherscan do
}
}
@token_balance_model %{
name: "TokenBalance",
fields: %{
balance: %{
type: "integer",
definition: "The token account balance.",
example: ~s("135499")
},
name: @token_name_type,
symbol: @token_symbol_type,
decimals: @token_decimal_type,
contractAddress: @address_hash_type
}
}
@block_reward_model %{
name: "BlockReward",
fields: %{
@ -1047,6 +1076,43 @@ defmodule BlockScoutWeb.Etherscan do
]
}
@account_tokenlist_action %{
name: "tokenlist",
description: "Get list of tokens owned by address.",
required_params: [
%{
key: "address",
placeholder: "addressHash",
type: "string",
description: "A 160-bit code used for identifying accounts."
}
],
optional_params: [],
responses: [
%{
code: "200",
description: "successful operation",
example_value: Jason.encode!(@account_tokenlist_example_value),
model: %{
name: "Result",
fields: %{
status: @status_type,
message: @message_type,
result: %{
type: "array",
array_type: @token_balance_model
}
}
}
},
%{
code: "200",
description: "error",
example_value: Jason.encode!(@account_tokenbalance_example_value_error)
}
]
}
@account_getminedblocks_action %{
name: "getminedblocks",
description: "Get list of blocks mined by address.",
@ -1537,6 +1603,7 @@ defmodule BlockScoutWeb.Etherscan do
@account_txlistinternal_action,
@account_tokentx_action,
@account_tokenbalance_action,
@account_tokenlist_action,
@account_getminedblocks_action
]
}

@ -42,6 +42,11 @@ defmodule BlockScoutWeb.API.RPC.AddressView do
RPCView.render("show.json", data: to_string(token_balance))
end
def render("token_list.json", %{token_list: token_list}) do
data = Enum.map(token_list, &prepare_token/1)
RPCView.render("show.json", data: data)
end
def render("getminedblocks.json", %{blocks: blocks}) do
data = Enum.map(blocks, &prepare_block/1)
RPCView.render("show.json", data: data)
@ -122,4 +127,14 @@ defmodule BlockScoutWeb.API.RPC.AddressView do
"blockReward" => to_string(block.reward.value)
}
end
defp prepare_token(token) do
%{
"balance" => to_string(token.balance),
"contractAddress" => to_string(token.contract_address_hash),
"name" => token.name,
"decimals" => to_string(token.decimals),
"symbol" => token.symbol
}
end
end

@ -2,6 +2,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do
use BlockScoutWeb.ConnCase
alias Explorer.Chain
alias Explorer.Repo
alias Explorer.Chain.{Transaction, Wei}
alias BlockScoutWeb.API.RPC.AddressController
@ -1540,6 +1541,131 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do
end
end
describe "tokenlist" do
test "without address param", %{conn: conn} do
params = %{
"module" => "account",
"action" => "tokenlist"
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["message"] =~ "address is required"
assert response["status"] == "0"
assert Map.has_key?(response, "result")
refute response["result"]
end
test "with an invalid address hash", %{conn: conn} do
params = %{
"module" => "account",
"action" => "tokenlist",
"address" => "badhash"
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["message"] =~ "Invalid address format"
assert response["status"] == "0"
assert Map.has_key?(response, "result")
refute response["result"]
end
test "with an address that doesn't exist", %{conn: conn} do
params = %{
"module" => "account",
"action" => "tokenlist",
"address" => "0x9bf38d4764929064f2d4d3a56520a76ab3df415b"
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["result"] == []
assert response["status"] == "0"
assert response["message"] == "No tokens found"
end
test "with an address without row in token_balances table", %{conn: conn} do
address = insert(:address)
params = %{
"module" => "account",
"action" => "tokenlist",
"address" => to_string(address.hash)
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["result"] == []
assert response["status"] == "0"
assert response["message"] == "No tokens found"
end
test "with address with existing balance in token_balances table", %{conn: conn} do
token_balance = :token_balance |> insert() |> Repo.preload(:token)
params = %{
"module" => "account",
"action" => "tokenlist",
"address" => to_string(token_balance.address_hash)
}
expected_result = [
%{
"balance" => to_string(token_balance.value),
"contractAddress" => to_string(token_balance.token_contract_address_hash),
"name" => token_balance.token.name,
"decimals" => to_string(token_balance.token.decimals),
"symbol" => token_balance.token.symbol
}
]
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["result"] == expected_result
assert response["status"] == "1"
assert response["message"] == "OK"
end
test "with address with multiple tokens", %{conn: conn} do
address = insert(:address)
other_address = insert(:address)
insert(:token_balance, address: address)
insert(:token_balance, address: address)
insert(:token_balance, address: other_address)
params = %{
"module" => "account",
"action" => "tokenlist",
"address" => to_string(address.hash)
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert length(response["result"]) == 2
assert response["status"] == "1"
assert response["message"] == "OK"
end
end
describe "getminedblocks" do
test "with missing address hash", %{conn: conn} do
params = %{

@ -183,6 +183,31 @@ defmodule Explorer.Etherscan do
Repo.one(query)
end
@doc """
Gets a list of tokens owned by the given address hash.
"""
@spec list_tokens(Hash.Address.t()) :: map() | []
def list_tokens(%Hash{byte_count: unquote(Hash.Address.byte_count())} = address_hash) do
query =
from(
tb in TokenBalance,
inner_join: t in assoc(tb, :token),
where: tb.address_hash == ^address_hash,
distinct: :token_contract_address_hash,
order_by: [desc: :block_number],
select: %{
balance: tb.value,
contract_address_hash: tb.token_contract_address_hash,
name: t.name,
decimals: t.decimals,
symbol: t.symbol
}
)
Repo.all(query)
end
@transaction_fields ~w(
block_hash
block_number

@ -1103,4 +1103,86 @@ defmodule Explorer.EtherscanTest do
assert found_token_balance.id == token_balance2.id
end
end
describe "list_tokens/1" do
test "returns the tokens owned by an address hash" do
address = insert(:address)
token_balance =
:token_balance
|> insert(address: address)
|> Repo.preload(:token)
insert(:token_balance, address: build(:address))
token_list = Etherscan.list_tokens(address.hash)
expected_tokens = [
%{
balance: token_balance.value,
contract_address_hash: token_balance.token_contract_address_hash,
name: token_balance.token.name,
decimals: token_balance.token.decimals,
symbol: token_balance.token.symbol
}
]
assert token_list == expected_tokens
end
test "returns the latest known balance per token" do
# The latest balance is the one with the latest block number
address = insert(:address)
token = insert(:token)
token_balance_details1 = %{
address: address,
token_contract_address_hash: token.contract_address.hash,
block_number: 1
}
token_balance_details2 = %{
address: address,
token_contract_address_hash: token.contract_address.hash,
block_number: 2
}
token_balance_details3 = %{
address: address,
token_contract_address_hash: token.contract_address.hash,
block_number: 3
}
insert(:token_balance, token_balance_details1)
token_balance =
:token_balance
|> insert(token_balance_details3)
|> Repo.preload(:token)
insert(:token_balance, token_balance_details2)
token_list = Etherscan.list_tokens(address.hash)
expected_tokens = [
%{
balance: token_balance.value,
contract_address_hash: token_balance.token_contract_address_hash,
name: token_balance.token.name,
decimals: token_balance.token.decimals,
symbol: token_balance.token.symbol
}
]
assert token_list == expected_tokens
end
test "returns an empty list when there are no token balances" do
address = insert(:address)
insert(:token_balance, address: build(:address))
assert Etherscan.list_tokens(address.hash) == []
end
end
end

Loading…
Cancel
Save