Why: * For API users to be able to get event logs in an Etherscan-compatible way. * Issue link: https://github.com/poanetwork/blockscout/issues/138 This change addresses the need by: * Editing the router to add support for `/api?module=logs&action=getLogs...` calls. * Adding a new `API.RPC.LogsController` to handle log related requests. * Adding `API.RPC.LogsView` * Adding `Explorer.Etherscan.Logs` where we've added functions to help us fetch logs as required by the supported filter parameters. The `list_logs/1` function in this new module is exposed through the `Explorer.Etherscan` context. * Adding 'logs' module and 'getLogs' action to API docs.pull/544/head
parent
ca63a6fc0b
commit
e4dcdc3e92
@ -0,0 +1,238 @@ |
||||
defmodule BlockScoutWeb.API.RPC.LogsController do |
||||
use BlockScoutWeb, :controller |
||||
|
||||
alias Explorer.{Etherscan, Chain} |
||||
|
||||
def getlogs(conn, params) do |
||||
with {:required_params, {:ok, fetched_params}} <- fetch_required_params(params), |
||||
{:format, {:ok, validated_params}} <- to_valid_format(fetched_params), |
||||
{:ok, logs} <- list_logs(validated_params) do |
||||
render(conn, :getlogs, %{logs: logs}) |
||||
else |
||||
{:required_params, {:error, missing_params}} -> |
||||
error = "Required query parameters missing: #{Enum.join(missing_params, ", ")}" |
||||
render(conn, :error, error: error) |
||||
|
||||
{:format, {:error, param}} -> |
||||
render(conn, :error, error: "Invalid #{param} format") |
||||
|
||||
{:error, :not_found} -> |
||||
render(conn, :error, error: "No logs found", data: []) |
||||
end |
||||
end |
||||
|
||||
# Interpretation of `@maybe_required_params`: |
||||
# |
||||
# If a pair of `topic{x}` params is provided, then the corresponding |
||||
# `topic{x}_{x}_opr` param is required. |
||||
# |
||||
# For example, if "topic0" and "topic1" are provided, then "topic0_1_opr" is |
||||
# required. |
||||
# |
||||
@maybe_required_params %{ |
||||
["topic0", "topic1"] => "topic0_1_opr", |
||||
["topic0", "topic2"] => "topic0_2_opr", |
||||
["topic0", "topic3"] => "topic0_3_opr", |
||||
["topic1", "topic2"] => "topic1_2_opr", |
||||
["topic1", "topic3"] => "topic1_3_opr", |
||||
["topic2", "topic3"] => "topic2_3_opr" |
||||
} |
||||
|
||||
@required_params %{ |
||||
# all_of: all of these parameters are required |
||||
all_of: ["fromBlock", "toBlock"], |
||||
# one_of: at least one of these parameters is required |
||||
one_of: ["address", "topic0", "topic1", "topic2", "topic3"] |
||||
} |
||||
|
||||
@doc """ |
||||
Fetches required params. Returns error tuple if required params are missing. |
||||
|
||||
""" |
||||
@spec fetch_required_params(map()) :: {:required_params, {:ok, map()} | {:error, [String.t(), ...]}} |
||||
def fetch_required_params(params) do |
||||
all_of_params = fetch_required_params(params, :all_of) |
||||
one_of_params = fetch_required_params(params, :one_of) |
||||
maybe_params = fetch_required_params(params, :maybe) |
||||
|
||||
result = |
||||
case {all_of_params, one_of_params, maybe_params} do |
||||
{{:error, missing_params}, {:error, _}, _} -> |
||||
{:error, Enum.concat(missing_params, ["address and/or topic{x}"])} |
||||
|
||||
{{:error, missing_params}, {:ok, _}, _} -> |
||||
{:error, missing_params} |
||||
|
||||
{{:ok, _}, {:error, _}, _} -> |
||||
{:error, ["address and/or topic{x}"]} |
||||
|
||||
{{:ok, _}, {:ok, _}, {:error, missing_params}} -> |
||||
{:error, missing_params} |
||||
|
||||
{{:ok, all_of_params}, {:ok, one_of_params}, {:ok, maybe_params}} -> |
||||
fetched_params = |
||||
all_of_params |
||||
|> Map.merge(one_of_params) |
||||
|> Map.merge(maybe_params) |
||||
|
||||
{:ok, fetched_params} |
||||
end |
||||
|
||||
{:required_params, result} |
||||
end |
||||
|
||||
@doc """ |
||||
Prepares params for processing. Returns error tuple if invalid format is |
||||
found. |
||||
|
||||
""" |
||||
@spec to_valid_format(map()) :: {:format, {:ok, map()} | {:error, String.t()}} |
||||
def to_valid_format(params) do |
||||
result = |
||||
with {:ok, from_block} <- to_block_number(params, "fromBlock"), |
||||
{:ok, to_block} <- to_block_number(params, "toBlock"), |
||||
{:ok, address_hash} <- to_address_hash(params["address"]), |
||||
:ok <- validate_topic_operators(params) do |
||||
validated_params = %{ |
||||
from_block: from_block, |
||||
to_block: to_block, |
||||
address_hash: address_hash, |
||||
first_topic: params["topic0"], |
||||
second_topic: params["topic1"], |
||||
third_topic: params["topic2"], |
||||
fourth_topic: params["topic3"], |
||||
topic0_1_opr: params["topic0_1_opr"], |
||||
topic0_2_opr: params["topic0_2_opr"], |
||||
topic0_3_opr: params["topic0_3_opr"], |
||||
topic1_2_opr: params["topic1_2_opr"], |
||||
topic1_3_opr: params["topic1_3_opr"], |
||||
topic2_3_opr: params["topic2_3_opr"] |
||||
} |
||||
|
||||
{:ok, validated_params} |
||||
else |
||||
{:error, param_key} -> |
||||
{:error, param_key} |
||||
end |
||||
|
||||
{:format, result} |
||||
end |
||||
|
||||
defp fetch_required_params(params, :all_of) do |
||||
fetched_params = Map.take(params, @required_params.all_of) |
||||
|
||||
if all_of_required_keys_found?(fetched_params) do |
||||
{:ok, fetched_params} |
||||
else |
||||
missing_params = get_missing_required_params(fetched_params, :all_of) |
||||
{:error, missing_params} |
||||
end |
||||
end |
||||
|
||||
defp fetch_required_params(params, :one_of) do |
||||
fetched_params = Map.take(params, @required_params.one_of) |
||||
found_keys = Map.keys(fetched_params) |
||||
|
||||
if length(found_keys) > 0 do |
||||
{:ok, fetched_params} |
||||
else |
||||
{:error, @required_params.one_of} |
||||
end |
||||
end |
||||
|
||||
defp fetch_required_params(params, :maybe) do |
||||
case get_missing_required_params(params, :maybe) do |
||||
[] -> |
||||
keys_to_fetch = Map.values(@maybe_required_params) |
||||
{:ok, Map.take(params, keys_to_fetch)} |
||||
|
||||
missing_params -> |
||||
{:error, Enum.reverse(missing_params)} |
||||
end |
||||
end |
||||
|
||||
defp all_of_required_keys_found?(fetched_params) do |
||||
Enum.all?(@required_params.all_of, &Map.has_key?(fetched_params, &1)) |
||||
end |
||||
|
||||
defp get_missing_required_params(fetched_params, :all_of) do |
||||
fetched_keys = fetched_params |> Map.keys() |> MapSet.new() |
||||
|
||||
@required_params.all_of |
||||
|> MapSet.new() |
||||
|> MapSet.difference(fetched_keys) |
||||
|> MapSet.to_list() |
||||
end |
||||
|
||||
defp get_missing_required_params(fetched_params, :maybe) do |
||||
Enum.reduce(@maybe_required_params, [], fn {[key1, key2], expectation}, missing_params -> |
||||
has_key1? = Map.has_key?(fetched_params, key1) |
||||
has_key2? = Map.has_key?(fetched_params, key2) |
||||
has_expectation? = Map.has_key?(fetched_params, expectation) |
||||
|
||||
case {has_key1?, has_key2?, has_expectation?} do |
||||
{true, true, false} -> |
||||
[expectation | missing_params] |
||||
|
||||
_ -> |
||||
missing_params |
||||
end |
||||
end) |
||||
end |
||||
|
||||
defp to_block_number(params, param_key) do |
||||
case params[param_key] do |
||||
"latest" -> |
||||
Chain.max_block_number() |
||||
|
||||
_ -> |
||||
to_integer(params, param_key) |
||||
end |
||||
end |
||||
|
||||
defp to_integer(params, param_key) do |
||||
case Integer.parse(params[param_key]) do |
||||
{integer, ""} -> |
||||
{:ok, integer} |
||||
|
||||
_ -> |
||||
{:error, param_key} |
||||
end |
||||
end |
||||
|
||||
defp to_address_hash(nil), do: {:ok, nil} |
||||
|
||||
defp to_address_hash(address_hash_string) do |
||||
case Chain.string_to_address_hash(address_hash_string) do |
||||
:error -> |
||||
{:error, "address"} |
||||
|
||||
{:ok, address_hash} -> |
||||
{:ok, address_hash} |
||||
end |
||||
end |
||||
|
||||
defp validate_topic_operators(params) do |
||||
topic_operator_keys = Map.values(@maybe_required_params) |
||||
|
||||
first_invalid_topic_operator = |
||||
Enum.find(topic_operator_keys, fn topic_operator -> |
||||
params[topic_operator] not in ["and", "or", nil] |
||||
end) |
||||
|
||||
case first_invalid_topic_operator do |
||||
nil -> |
||||
:ok |
||||
|
||||
invalid_topic_operator -> |
||||
{:error, invalid_topic_operator} |
||||
end |
||||
end |
||||
|
||||
defp list_logs(filter) do |
||||
case Etherscan.list_logs(filter) do |
||||
[] -> {:error, :not_found} |
||||
logs -> {:ok, logs} |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,49 @@ |
||||
defmodule BlockScoutWeb.API.RPC.LogsView do |
||||
use BlockScoutWeb, :view |
||||
|
||||
alias BlockScoutWeb.API.RPC.RPCView |
||||
|
||||
def render("getlogs.json", %{logs: logs}) do |
||||
data = Enum.map(logs, &prepare_log/1) |
||||
RPCView.render("show.json", data: data) |
||||
end |
||||
|
||||
def render("error.json", assigns) do |
||||
RPCView.render("error.json", assigns) |
||||
end |
||||
|
||||
defp prepare_log(log) do |
||||
%{ |
||||
"address" => "#{log.address_hash}", |
||||
"topics" => get_topics(log), |
||||
"data" => "#{log.data}", |
||||
"blockNumber" => integer_to_hex(log.block_number), |
||||
"timeStamp" => datetime_to_hex(log.block_timestamp), |
||||
"gasPrice" => decimal_to_hex(log.gas_price.value), |
||||
"gasUsed" => decimal_to_hex(log.gas_used), |
||||
"logIndex" => integer_to_hex(log.index), |
||||
"transactionHash" => "#{log.transaction_hash}", |
||||
"transactionIndex" => integer_to_hex(log.transaction_index) |
||||
} |
||||
end |
||||
|
||||
defp get_topics(log) do |
||||
log |
||||
|> Map.take([:first_topic, :second_topic, :third_topic, :fourth_topic]) |
||||
|> Map.values() |
||||
end |
||||
|
||||
defp integer_to_hex(integer), do: Integer.to_string(integer, 16) |
||||
|
||||
defp decimal_to_hex(decimal) do |
||||
decimal |
||||
|> Decimal.to_integer() |
||||
|> integer_to_hex() |
||||
end |
||||
|
||||
defp datetime_to_hex(datetime) do |
||||
datetime |
||||
|> DateTime.to_unix() |
||||
|> integer_to_hex() |
||||
end |
||||
end |
@ -0,0 +1,771 @@ |
||||
defmodule BlockScoutWeb.API.RPC.LogsControllerTest do |
||||
use BlockScoutWeb.ConnCase |
||||
|
||||
alias BlockScoutWeb.API.RPC.LogsController |
||||
alias Explorer.Chain.Transaction |
||||
|
||||
describe "getLogs" do |
||||
test "without fromBlock, toBlock, address, and topic{x}", %{conn: conn} do |
||||
params = %{ |
||||
"module" => "logs", |
||||
"action" => "getLogs" |
||||
} |
||||
|
||||
expected_message = "Required query parameters missing: fromBlock, toBlock, address and/or topic{x}" |
||||
|
||||
assert response = |
||||
conn |
||||
|> get("/api", params) |
||||
|> json_response(200) |
||||
|
||||
assert response["message"] == expected_message |
||||
assert response["status"] == "0" |
||||
assert Map.has_key?(response, "result") |
||||
refute response["result"] |
||||
end |
||||
|
||||
test "without fromBlock", %{conn: conn} do |
||||
params = %{ |
||||
"module" => "logs", |
||||
"action" => "getLogs", |
||||
"toBlock" => "10", |
||||
"address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" |
||||
} |
||||
|
||||
expected_message = "Required query parameters missing: fromBlock" |
||||
|
||||
assert response = |
||||
conn |
||||
|> get("/api", params) |
||||
|> json_response(200) |
||||
|
||||
assert response["message"] == expected_message |
||||
assert response["status"] == "0" |
||||
assert Map.has_key?(response, "result") |
||||
refute response["result"] |
||||
end |
||||
|
||||
test "without toBlock", %{conn: conn} do |
||||
params = %{ |
||||
"module" => "logs", |
||||
"action" => "getLogs", |
||||
"fromBlock" => "5", |
||||
"address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" |
||||
} |
||||
|
||||
expected_message = "Required query parameters missing: toBlock" |
||||
|
||||
assert response = |
||||
conn |
||||
|> get("/api", params) |
||||
|> json_response(200) |
||||
|
||||
assert response["message"] == expected_message |
||||
assert response["status"] == "0" |
||||
assert Map.has_key?(response, "result") |
||||
refute response["result"] |
||||
end |
||||
|
||||
test "without address and topic{x}", %{conn: conn} do |
||||
params = %{ |
||||
"module" => "logs", |
||||
"action" => "getLogs", |
||||
"fromBlock" => "5", |
||||
"toBlock" => "10" |
||||
} |
||||
|
||||
expected_message = "Required query parameters missing: address and/or topic{x}" |
||||
|
||||
assert response = |
||||
conn |
||||
|> get("/api", params) |
||||
|> json_response(200) |
||||
|
||||
assert response["message"] == expected_message |
||||
assert response["status"] == "0" |
||||
assert Map.has_key?(response, "result") |
||||
refute response["result"] |
||||
end |
||||
|
||||
test "without topic{x}_{x}_opr", %{conn: conn} do |
||||
conditions = %{ |
||||
["topic0", "topic1"] => "topic0_1_opr", |
||||
["topic0", "topic2"] => "topic0_2_opr", |
||||
["topic0", "topic3"] => "topic0_3_opr", |
||||
["topic1", "topic2"] => "topic1_2_opr", |
||||
["topic1", "topic3"] => "topic1_3_opr", |
||||
["topic2", "topic3"] => "topic2_3_opr" |
||||
} |
||||
|
||||
for {[key1, key2], expectation} <- conditions do |
||||
params = %{ |
||||
"module" => "logs", |
||||
"action" => "getLogs", |
||||
"fromBlock" => "5", |
||||
"toBlock" => "10", |
||||
key1 => "some topic", |
||||
key2 => "some other topic" |
||||
} |
||||
|
||||
expected_message = "Required query parameters missing: #{expectation}" |
||||
|
||||
assert response = |
||||
conn |
||||
|> get("/api", params) |
||||
|> json_response(200) |
||||
|
||||
assert response["message"] == expected_message |
||||
assert response["status"] == "0" |
||||
assert Map.has_key?(response, "result") |
||||
refute response["result"] |
||||
end |
||||
end |
||||
|
||||
test "without multiple topic{x}_{x}_opr", %{conn: conn} do |
||||
params = %{ |
||||
"module" => "logs", |
||||
"action" => "getLogs", |
||||
"fromBlock" => "5", |
||||
"toBlock" => "10", |
||||
"topic0" => "some topic", |
||||
"topic1" => "some other topic", |
||||
"topic2" => "some extra topic", |
||||
"topic3" => "some different topic" |
||||
} |
||||
|
||||
expected_message = |
||||
"Required query parameters missing: " <> |
||||
"topic0_1_opr, topic0_2_opr, topic0_3_opr, topic1_2_opr, topic1_3_opr, topic2_3_opr" |
||||
|
||||
assert response = |
||||
conn |
||||
|> get("/api", params) |
||||
|> json_response(200) |
||||
|
||||
assert response["message"] == expected_message |
||||
assert response["status"] == "0" |
||||
assert Map.has_key?(response, "result") |
||||
refute response["result"] |
||||
end |
||||
|
||||
test "with invalid fromBlock", %{conn: conn} do |
||||
params = %{ |
||||
"module" => "logs", |
||||
"action" => "getLogs", |
||||
"fromBlock" => "invalid", |
||||
"toBlock" => "10", |
||||
"address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" |
||||
} |
||||
|
||||
assert response = |
||||
conn |
||||
|> get("/api", params) |
||||
|> json_response(200) |
||||
|
||||
assert response["message"] =~ "Invalid fromBlock format" |
||||
assert response["status"] == "0" |
||||
assert Map.has_key?(response, "result") |
||||
refute response["result"] |
||||
end |
||||
|
||||
test "with invalid toBlock", %{conn: conn} do |
||||
params = %{ |
||||
"module" => "logs", |
||||
"action" => "getLogs", |
||||
"fromBlock" => "5", |
||||
"toBlock" => "invalid", |
||||
"address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" |
||||
} |
||||
|
||||
assert response = |
||||
conn |
||||
|> get("/api", params) |
||||
|> json_response(200) |
||||
|
||||
assert response["message"] =~ "Invalid toBlock format" |
||||
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" => "logs", |
||||
"action" => "getLogs", |
||||
"fromBlock" => "5", |
||||
"toBlock" => "10", |
||||
"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 invalid topic{x}_{x}_opr", %{conn: conn} do |
||||
conditions = %{ |
||||
["topic0", "topic1"] => "topic0_1_opr", |
||||
["topic0", "topic2"] => "topic0_2_opr", |
||||
["topic0", "topic3"] => "topic0_3_opr", |
||||
["topic1", "topic2"] => "topic1_2_opr", |
||||
["topic1", "topic3"] => "topic1_3_opr", |
||||
["topic2", "topic3"] => "topic2_3_opr" |
||||
} |
||||
|
||||
for {[key1, key2], expectation} <- conditions do |
||||
params = %{ |
||||
"module" => "logs", |
||||
"action" => "getLogs", |
||||
"fromBlock" => "5", |
||||
"toBlock" => "10", |
||||
key1 => "some topic", |
||||
key2 => "some other topic", |
||||
expectation => "invalid" |
||||
} |
||||
|
||||
assert response = |
||||
conn |
||||
|> get("/api", params) |
||||
|> json_response(200) |
||||
|
||||
assert response["message"] =~ "Invalid #{expectation} format" |
||||
assert response["status"] == "0" |
||||
assert Map.has_key?(response, "result") |
||||
refute response["result"] |
||||
end |
||||
end |
||||
|
||||
test "with an address that doesn't exist", %{conn: conn} do |
||||
params = %{ |
||||
"module" => "logs", |
||||
"action" => "getLogs", |
||||
"fromBlock" => "5", |
||||
"toBlock" => "10", |
||||
"address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b" |
||||
} |
||||
|
||||
assert response = |
||||
conn |
||||
|> get("/api", params) |
||||
|> json_response(200) |
||||
|
||||
assert response["result"] == [] |
||||
assert response["status"] == "0" |
||||
assert response["message"] == "No logs found" |
||||
end |
||||
|
||||
test "with a valid contract address", %{conn: conn} do |
||||
contract_address = insert(:contract_address) |
||||
|
||||
transaction = |
||||
%Transaction{block: block} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block() |
||||
|
||||
log = insert(:log, address: contract_address, transaction: transaction) |
||||
|
||||
params = %{ |
||||
"module" => "logs", |
||||
"action" => "getLogs", |
||||
"fromBlock" => "#{block.number}", |
||||
"toBlock" => "#{block.number}", |
||||
"address" => "#{contract_address.hash}" |
||||
} |
||||
|
||||
expected_result = [ |
||||
%{ |
||||
"address" => "#{contract_address.hash}", |
||||
"topics" => get_topics(log), |
||||
"data" => "#{log.data}", |
||||
"blockNumber" => integer_to_hex(transaction.block_number), |
||||
"timeStamp" => datetime_to_hex(block.timestamp), |
||||
"gasPrice" => decimal_to_hex(transaction.gas_price.value), |
||||
"gasUsed" => decimal_to_hex(transaction.gas_used), |
||||
"logIndex" => integer_to_hex(log.index), |
||||
"transactionHash" => "#{transaction.hash}", |
||||
"transactionIndex" => integer_to_hex(transaction.index) |
||||
} |
||||
] |
||||
|
||||
assert response = |
||||
conn |
||||
|> get("/api", params) |
||||
|> json_response(200) |
||||
|
||||
assert response["result"] == expected_result |
||||
assert response["status"] == "1" |
||||
assert response["message"] == "OK" |
||||
end |
||||
|
||||
test "ignores logs with block below fromBlock", %{conn: conn} do |
||||
first_block = insert(:block) |
||||
second_block = insert(:block) |
||||
|
||||
contract_address = insert(:contract_address) |
||||
|
||||
transaction_block1 = |
||||
%Transaction{} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block(first_block) |
||||
|
||||
transaction_block2 = |
||||
%Transaction{} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block(second_block) |
||||
|
||||
insert(:log, address: contract_address, transaction: transaction_block1) |
||||
insert(:log, address: contract_address, transaction: transaction_block2) |
||||
|
||||
params = %{ |
||||
"module" => "logs", |
||||
"action" => "getLogs", |
||||
"fromBlock" => "#{second_block.number}", |
||||
"toBlock" => "#{second_block.number}", |
||||
"address" => "#{contract_address.hash}" |
||||
} |
||||
|
||||
assert response = |
||||
conn |
||||
|> get("/api", params) |
||||
|> json_response(200) |
||||
|
||||
assert response["status"] == "1" |
||||
assert response["message"] == "OK" |
||||
|
||||
[found_log] = response["result"] |
||||
|
||||
assert found_log["blockNumber"] == integer_to_hex(second_block.number) |
||||
assert found_log["transactionHash"] == "#{transaction_block2.hash}" |
||||
end |
||||
|
||||
test "ignores logs with block above toBlock", %{conn: conn} do |
||||
first_block = insert(:block) |
||||
second_block = insert(:block) |
||||
|
||||
contract_address = insert(:contract_address) |
||||
|
||||
transaction_block1 = |
||||
%Transaction{} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block(first_block) |
||||
|
||||
transaction_block2 = |
||||
%Transaction{} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block(second_block) |
||||
|
||||
insert(:log, address: contract_address, transaction: transaction_block1) |
||||
insert(:log, address: contract_address, transaction: transaction_block2) |
||||
|
||||
params = %{ |
||||
"module" => "logs", |
||||
"action" => "getLogs", |
||||
"fromBlock" => "#{first_block.number}", |
||||
"toBlock" => "#{first_block.number}", |
||||
"address" => "#{contract_address.hash}" |
||||
} |
||||
|
||||
assert response = |
||||
conn |
||||
|> get("/api", params) |
||||
|> json_response(200) |
||||
|
||||
assert response["status"] == "1" |
||||
assert response["message"] == "OK" |
||||
|
||||
[found_log] = response["result"] |
||||
|
||||
assert found_log["blockNumber"] == integer_to_hex(first_block.number) |
||||
assert found_log["transactionHash"] == "#{transaction_block1.hash}" |
||||
end |
||||
|
||||
test "with a valid topic{x}", %{conn: conn} do |
||||
contract_address = insert(:contract_address) |
||||
|
||||
transaction = |
||||
%Transaction{block: block} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block() |
||||
|
||||
log1_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some topic" |
||||
] |
||||
|
||||
log2_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some other topic" |
||||
] |
||||
|
||||
log1 = insert(:log, log1_details) |
||||
_log2 = insert(:log, log2_details) |
||||
|
||||
params = %{ |
||||
"module" => "logs", |
||||
"action" => "getLogs", |
||||
"fromBlock" => "#{block.number}", |
||||
"toBlock" => "#{block.number}", |
||||
"topic0" => log1.first_topic |
||||
} |
||||
|
||||
expected_result = [ |
||||
%{ |
||||
"address" => "#{contract_address.hash}", |
||||
"topics" => get_topics(log1), |
||||
"data" => "#{log1.data}", |
||||
"blockNumber" => integer_to_hex(transaction.block_number), |
||||
"timeStamp" => datetime_to_hex(block.timestamp), |
||||
"gasPrice" => decimal_to_hex(transaction.gas_price.value), |
||||
"gasUsed" => decimal_to_hex(transaction.gas_used), |
||||
"logIndex" => integer_to_hex(log1.index), |
||||
"transactionHash" => "#{transaction.hash}", |
||||
"transactionIndex" => integer_to_hex(transaction.index) |
||||
} |
||||
] |
||||
|
||||
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 a topic{x} AND another", %{conn: conn} do |
||||
contract_address = insert(:contract_address) |
||||
|
||||
transaction = |
||||
%Transaction{block: block} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block() |
||||
|
||||
log1_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some topic", |
||||
second_topic: "some second topic" |
||||
] |
||||
|
||||
log2_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some other topic", |
||||
second_topic: "some other second topic" |
||||
] |
||||
|
||||
log1 = insert(:log, log1_details) |
||||
_log2 = insert(:log, log2_details) |
||||
|
||||
params = %{ |
||||
"module" => "logs", |
||||
"action" => "getLogs", |
||||
"fromBlock" => "#{block.number}", |
||||
"toBlock" => "#{block.number}", |
||||
"topic0" => log1.first_topic, |
||||
"topic1" => log1.second_topic, |
||||
"topic0_1_opr" => "and" |
||||
} |
||||
|
||||
assert response = |
||||
conn |
||||
|> get("/api", params) |
||||
|> json_response(200) |
||||
|
||||
assert [found_log] = response["result"] |
||||
assert found_log["logIndex"] == integer_to_hex(log1.index) |
||||
assert found_log["topics"] == get_topics(log1) |
||||
assert response["status"] == "1" |
||||
assert response["message"] == "OK" |
||||
end |
||||
|
||||
test "with a topic{x} OR another", %{conn: conn} do |
||||
contract_address = insert(:contract_address) |
||||
|
||||
transaction = |
||||
%Transaction{block: block} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block() |
||||
|
||||
log1_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some topic", |
||||
second_topic: "some second topic" |
||||
] |
||||
|
||||
log2_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some other topic", |
||||
second_topic: "some other second topic" |
||||
] |
||||
|
||||
log1 = insert(:log, log1_details) |
||||
log2 = insert(:log, log2_details) |
||||
|
||||
params = %{ |
||||
"module" => "logs", |
||||
"action" => "getLogs", |
||||
"fromBlock" => "#{block.number}", |
||||
"toBlock" => "#{block.number}", |
||||
"topic0" => log1.first_topic, |
||||
"topic1" => log2.second_topic, |
||||
"topic0_1_opr" => "or" |
||||
} |
||||
|
||||
assert %{"result" => result} = |
||||
response = |
||||
conn |
||||
|> get("/api", params) |
||||
|> json_response(200) |
||||
|
||||
assert length(result) == 2 |
||||
assert response["status"] == "1" |
||||
assert response["message"] == "OK" |
||||
end |
||||
|
||||
test "with all available 'topic{x}'s and 'topic{x}_{x}_opr's", %{conn: conn} do |
||||
contract_address = insert(:contract_address) |
||||
|
||||
transaction = |
||||
%Transaction{block: block} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block() |
||||
|
||||
log1_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some topic", |
||||
second_topic: "some second topic", |
||||
third_topic: "some third topic", |
||||
fourth_topic: "some fourth topic" |
||||
] |
||||
|
||||
log2_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some topic", |
||||
second_topic: "some second topic", |
||||
third_topic: "some third topic", |
||||
fourth_topic: "some other fourth topic" |
||||
] |
||||
|
||||
log1 = insert(:log, log1_details) |
||||
log2 = insert(:log, log2_details) |
||||
|
||||
params = %{ |
||||
"module" => "logs", |
||||
"action" => "getLogs", |
||||
"fromBlock" => "#{block.number}", |
||||
"toBlock" => "#{block.number}", |
||||
"topic0" => log1.first_topic, |
||||
"topic1" => log1.second_topic, |
||||
"topic2" => log1.third_topic, |
||||
"topic3" => log2.fourth_topic, |
||||
"topic0_1_opr" => "and", |
||||
"topic0_2_opr" => "and", |
||||
"topic0_3_opr" => "or", |
||||
"topic1_2_opr" => "and", |
||||
"topic1_3_opr" => "or", |
||||
"topic2_3_opr" => "or" |
||||
} |
||||
|
||||
assert %{"result" => result} = |
||||
response = |
||||
conn |
||||
|> get("/api", params) |
||||
|> json_response(200) |
||||
|
||||
assert length(result) == 2 |
||||
assert response["status"] == "1" |
||||
assert response["message"] == "OK" |
||||
end |
||||
end |
||||
|
||||
describe "fetch_required_params/1" do |
||||
test "without any required params" do |
||||
params = %{} |
||||
|
||||
{_, {:error, missing_params}} = LogsController.fetch_required_params(params) |
||||
|
||||
assert missing_params == ["fromBlock", "toBlock", "address and/or topic{x}"] |
||||
end |
||||
|
||||
test "without fromBlock" do |
||||
params = %{ |
||||
"toBlock" => "5", |
||||
"address" => "some address" |
||||
} |
||||
|
||||
{_, {:error, [missing_param]}} = LogsController.fetch_required_params(params) |
||||
|
||||
assert missing_param == "fromBlock" |
||||
end |
||||
|
||||
test "without toBlock" do |
||||
params = %{ |
||||
"fromBlock" => "5", |
||||
"address" => "some address" |
||||
} |
||||
|
||||
{_, {:error, [missing_param]}} = LogsController.fetch_required_params(params) |
||||
|
||||
assert missing_param == "toBlock" |
||||
end |
||||
|
||||
test "without fromBlock or toBlock" do |
||||
params = %{ |
||||
"address" => "some address" |
||||
} |
||||
|
||||
{_, {:error, missing_params}} = LogsController.fetch_required_params(params) |
||||
|
||||
assert missing_params == ["fromBlock", "toBlock"] |
||||
end |
||||
|
||||
test "without address or topic{x}" do |
||||
params = %{ |
||||
"toBlock" => "5", |
||||
"fromBlock" => "5" |
||||
} |
||||
|
||||
{_, {:error, [missing_param]}} = LogsController.fetch_required_params(params) |
||||
|
||||
assert missing_param == "address and/or topic{x}" |
||||
end |
||||
|
||||
test "with address" do |
||||
params = %{ |
||||
"fromBlock" => "5", |
||||
"toBlock" => "5", |
||||
"address" => "some address" |
||||
} |
||||
|
||||
{_, {:ok, fetched_params}} = LogsController.fetch_required_params(params) |
||||
|
||||
assert fetched_params == params |
||||
end |
||||
|
||||
test "with topic{x}" do |
||||
for topic <- ["topic0", "topic1", "topic2", "topic3"] do |
||||
params = %{ |
||||
"fromBlock" => "5", |
||||
"toBlock" => "5", |
||||
topic => "some topic" |
||||
} |
||||
|
||||
{_, {:ok, fetched_params}} = LogsController.fetch_required_params(params) |
||||
|
||||
assert fetched_params == params |
||||
end |
||||
end |
||||
|
||||
test "with address and topic{x}" do |
||||
params = %{ |
||||
"fromBlock" => "5", |
||||
"toBlock" => "5", |
||||
"address" => "some address", |
||||
"topic0" => "some topic" |
||||
} |
||||
|
||||
{_, {:ok, fetched_params}} = LogsController.fetch_required_params(params) |
||||
|
||||
assert fetched_params == params |
||||
end |
||||
end |
||||
|
||||
describe "to_valid_format/1" do |
||||
test "with invalid fromBlock" do |
||||
params = %{"fromBlock" => "invalid"} |
||||
|
||||
assert {_, {:error, "fromBlock"}} = LogsController.to_valid_format(params) |
||||
end |
||||
|
||||
test "with invalid toBlock" do |
||||
params = %{ |
||||
"fromBlock" => "5", |
||||
"toBlock" => "invalid" |
||||
} |
||||
|
||||
assert {_, {:error, "toBlock"}} = LogsController.to_valid_format(params) |
||||
end |
||||
|
||||
test "with invalid address" do |
||||
params = %{ |
||||
"fromBlock" => "5", |
||||
"toBlock" => "10", |
||||
"address" => "invalid" |
||||
} |
||||
|
||||
assert {_, {:error, "address"}} = LogsController.to_valid_format(params) |
||||
end |
||||
|
||||
test "address_hash returns as nil when missing" do |
||||
params = %{ |
||||
"fromBlock" => "5", |
||||
"toBlock" => "10" |
||||
} |
||||
|
||||
assert {_, {:ok, validated_params}} = LogsController.to_valid_format(params) |
||||
refute validated_params.address_hash |
||||
end |
||||
|
||||
test "fromBlock and toBlock support use of 'latest'" do |
||||
params = %{ |
||||
"fromBlock" => "latest", |
||||
"toBlock" => "latest" |
||||
} |
||||
|
||||
# Without any blocks in the db we want to return {:error, :not_found} |
||||
assert {_, {:error, :not_found}} = LogsController.to_valid_format(params) |
||||
|
||||
# We insert a block, try again, and assert 'latest' points to the latest |
||||
# block number. |
||||
insert(:block) |
||||
{:ok, max_block_number} = Explorer.Chain.max_block_number() |
||||
|
||||
assert {_, {:ok, validated_params}} = LogsController.to_valid_format(params) |
||||
assert validated_params.from_block == max_block_number |
||||
assert validated_params.to_block == max_block_number |
||||
end |
||||
end |
||||
|
||||
defp get_topics(log) do |
||||
log |
||||
|> Map.take([:first_topic, :second_topic, :third_topic, :fourth_topic]) |
||||
|> Map.values() |
||||
end |
||||
|
||||
defp integer_to_hex(integer), do: Integer.to_string(integer, 16) |
||||
|
||||
defp decimal_to_hex(decimal) do |
||||
decimal |
||||
|> Decimal.to_integer() |
||||
|> integer_to_hex() |
||||
end |
||||
|
||||
defp datetime_to_hex(datetime) do |
||||
datetime |
||||
|> DateTime.to_unix() |
||||
|> integer_to_hex() |
||||
end |
||||
end |
@ -0,0 +1,150 @@ |
||||
defmodule Explorer.Etherscan.Logs do |
||||
@moduledoc """ |
||||
This module contains functions for working with logs, as they pertain to the |
||||
`Explorer.Etherscan` context. |
||||
|
||||
""" |
||||
|
||||
import Ecto.Query, only: [from: 2, where: 3] |
||||
|
||||
alias Explorer.Repo |
||||
alias Explorer.Chain.Log |
||||
|
||||
@base_filter %{ |
||||
from_block: nil, |
||||
to_block: nil, |
||||
address_hash: nil, |
||||
first_topic: nil, |
||||
second_topic: nil, |
||||
third_topic: nil, |
||||
fourth_topic: nil, |
||||
topic0_1_opr: nil, |
||||
topic0_2_opr: nil, |
||||
topic0_3_opr: nil, |
||||
topic1_2_opr: nil, |
||||
topic1_3_opr: nil, |
||||
topic2_3_opr: nil |
||||
} |
||||
|
||||
@log_fields [ |
||||
:data, |
||||
:first_topic, |
||||
:second_topic, |
||||
:third_topic, |
||||
:fourth_topic, |
||||
:index, |
||||
:address_hash, |
||||
:transaction_hash |
||||
] |
||||
|
||||
@doc """ |
||||
Gets a list of logs that meet the criteria in a given filter map. |
||||
|
||||
Required filter parameters: |
||||
|
||||
* `from_block` |
||||
* `to_block` |
||||
* `address_hash` and/or `{x}_topic` |
||||
* When multiple `{x}_topic` params are provided, then the corresponding |
||||
`topic{x}_{x}_opr` param is required. For example, if "first_topic" and |
||||
"second_topic" are provided, then "topic0_1_opr" is required. |
||||
|
||||
Supported `{x}_topic`s: |
||||
|
||||
* first_topic |
||||
* second_topic |
||||
* third_topic |
||||
* fourth_topic |
||||
|
||||
Supported `topic{x}_{x}_opr`s: |
||||
|
||||
* topic0_1_opr |
||||
* topic0_2_opr |
||||
* topic0_3_opr |
||||
* topic1_2_opr |
||||
* topic1_3_opr |
||||
* topic2_3_opr |
||||
|
||||
""" |
||||
@spec list_logs(map()) :: [map()] |
||||
def list_logs(filter) do |
||||
prepared_filter = Map.merge(@base_filter, filter) |
||||
|
||||
query = |
||||
from( |
||||
l in Log, |
||||
inner_join: t in assoc(l, :transaction), |
||||
inner_join: b in assoc(t, :block), |
||||
where: b.number >= ^prepared_filter.from_block, |
||||
where: b.number <= ^prepared_filter.to_block, |
||||
order_by: b.number, |
||||
limit: 1_000, |
||||
select: |
||||
merge(map(l, ^@log_fields), %{ |
||||
gas_price: t.gas_price, |
||||
gas_used: t.gas_used, |
||||
transaction_index: t.index, |
||||
block_number: b.number, |
||||
block_timestamp: b.timestamp |
||||
}) |
||||
) |
||||
|
||||
query |
||||
|> where_address_match(prepared_filter) |
||||
|> where_topic_match(prepared_filter) |
||||
|> Repo.all() |
||||
end |
||||
|
||||
@topics [ |
||||
:first_topic, |
||||
:second_topic, |
||||
:third_topic, |
||||
:fourth_topic |
||||
] |
||||
|
||||
@topic_operations %{ |
||||
topic0_1_opr: {:first_topic, :second_topic}, |
||||
topic0_2_opr: {:first_topic, :third_topic}, |
||||
topic0_3_opr: {:first_topic, :fourth_topic}, |
||||
topic1_2_opr: {:second_topic, :third_topic}, |
||||
topic1_3_opr: {:second_topic, :fourth_topic}, |
||||
topic2_3_opr: {:third_topic, :fourth_topic} |
||||
} |
||||
|
||||
defp where_address_match(query, %{address_hash: address_hash}) when not is_nil(address_hash) do |
||||
where(query, [l], l.address_hash == ^address_hash) |
||||
end |
||||
|
||||
defp where_address_match(query, _), do: query |
||||
|
||||
defp where_topic_match(query, filter) do |
||||
case Enum.filter(@topics, &filter[&1]) do |
||||
[] -> |
||||
query |
||||
|
||||
[topic] -> |
||||
where(query, [l], field(l, ^topic) == ^filter[topic]) |
||||
|
||||
_ -> |
||||
where_multiple_topics_match(query, filter) |
||||
end |
||||
end |
||||
|
||||
defp where_multiple_topics_match(query, filter) do |
||||
Enum.reduce(Map.keys(@topic_operations), query, fn topic_operation, acc_query -> |
||||
where_multiple_topics_match(acc_query, filter, topic_operation, filter[topic_operation]) |
||||
end) |
||||
end |
||||
|
||||
defp where_multiple_topics_match(query, filter, topic_operation, "and") do |
||||
{topic_a, topic_b} = @topic_operations[topic_operation] |
||||
where(query, [l], field(l, ^topic_a) == ^filter[topic_a] and field(l, ^topic_b) == ^filter[topic_b]) |
||||
end |
||||
|
||||
defp where_multiple_topics_match(query, filter, topic_operation, "or") do |
||||
{topic_a, topic_b} = @topic_operations[topic_operation] |
||||
where(query, [l], field(l, ^topic_a) == ^filter[topic_a] or field(l, ^topic_b) == ^filter[topic_b]) |
||||
end |
||||
|
||||
defp where_multiple_topics_match(query, _, _, _), do: query |
||||
end |
@ -0,0 +1,630 @@ |
||||
defmodule Explorer.Etherscan.LogsTest do |
||||
use Explorer.DataCase |
||||
|
||||
import Explorer.Factory |
||||
|
||||
alias Explorer.Etherscan.Logs |
||||
alias Explorer.Chain.Transaction |
||||
|
||||
describe "list_logs/1" do |
||||
test "with empty db" do |
||||
contract_address = build(:contract_address) |
||||
|
||||
filter = %{ |
||||
from_block: 0, |
||||
to_block: 9999, |
||||
address_hash: contract_address.hash |
||||
} |
||||
|
||||
assert Logs.list_logs(filter) == [] |
||||
end |
||||
|
||||
test "with address with zero logs" do |
||||
contract_address = insert(:contract_address) |
||||
|
||||
%Transaction{block: block} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block() |
||||
|
||||
filter = %{ |
||||
from_block: block.number, |
||||
to_block: block.number, |
||||
address_hash: contract_address.hash |
||||
} |
||||
|
||||
assert Logs.list_logs(filter) == [] |
||||
end |
||||
|
||||
test "with address with one log response includes all required information" do |
||||
contract_address = insert(:contract_address) |
||||
|
||||
transaction = |
||||
%Transaction{block: block} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block() |
||||
|
||||
log = insert(:log, address: contract_address, transaction: transaction) |
||||
|
||||
filter = %{ |
||||
from_block: block.number, |
||||
to_block: block.number, |
||||
address_hash: contract_address.hash |
||||
} |
||||
|
||||
[found_log] = Logs.list_logs(filter) |
||||
|
||||
assert found_log.data == log.data |
||||
assert found_log.first_topic == log.first_topic |
||||
assert found_log.second_topic == log.second_topic |
||||
assert found_log.third_topic == log.third_topic |
||||
assert found_log.fourth_topic == log.fourth_topic |
||||
assert found_log.index == log.index |
||||
assert found_log.address_hash == log.address_hash |
||||
assert found_log.transaction_hash == log.transaction_hash |
||||
assert found_log.gas_price == transaction.gas_price |
||||
assert found_log.gas_used == transaction.gas_used |
||||
assert found_log.transaction_index == transaction.index |
||||
assert found_log.block_number == block.number |
||||
assert found_log.block_timestamp == block.timestamp |
||||
end |
||||
|
||||
test "with address with two logs" do |
||||
contract_address = insert(:contract_address) |
||||
|
||||
transaction = |
||||
%Transaction{block: block} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block() |
||||
|
||||
insert_list(2, :log, address: contract_address, transaction: transaction) |
||||
|
||||
filter = %{ |
||||
from_block: block.number, |
||||
to_block: block.number, |
||||
address_hash: contract_address.hash |
||||
} |
||||
|
||||
found_logs = Logs.list_logs(filter) |
||||
|
||||
assert length(found_logs) == 2 |
||||
end |
||||
|
||||
test "ignores logs with block below fromBlock" do |
||||
first_block = insert(:block) |
||||
second_block = insert(:block) |
||||
|
||||
contract_address = insert(:contract_address) |
||||
|
||||
transaction_block1 = |
||||
%Transaction{} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block(first_block) |
||||
|
||||
transaction_block2 = |
||||
%Transaction{} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block(second_block) |
||||
|
||||
insert(:log, address: contract_address, transaction: transaction_block1) |
||||
insert(:log, address: contract_address, transaction: transaction_block2) |
||||
|
||||
filter = %{ |
||||
from_block: second_block.number, |
||||
to_block: second_block.number, |
||||
address_hash: contract_address.hash |
||||
} |
||||
|
||||
[found_log] = Logs.list_logs(filter) |
||||
|
||||
assert found_log.block_number == second_block.number |
||||
assert found_log.transaction_hash == transaction_block2.hash |
||||
end |
||||
|
||||
test "ignores logs with block above toBlock" do |
||||
first_block = insert(:block) |
||||
second_block = insert(:block) |
||||
|
||||
contract_address = insert(:contract_address) |
||||
|
||||
transaction_block1 = |
||||
%Transaction{} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block(first_block) |
||||
|
||||
transaction_block2 = |
||||
%Transaction{} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block(second_block) |
||||
|
||||
insert(:log, address: contract_address, transaction: transaction_block1) |
||||
insert(:log, address: contract_address, transaction: transaction_block2) |
||||
|
||||
filter = %{ |
||||
from_block: first_block.number, |
||||
to_block: first_block.number, |
||||
address_hash: contract_address.hash |
||||
} |
||||
|
||||
[found_log] = Logs.list_logs(filter) |
||||
|
||||
assert found_log.block_number == first_block.number |
||||
assert found_log.transaction_hash == transaction_block1.hash |
||||
end |
||||
|
||||
test "with a valid topic{x}" do |
||||
contract_address = insert(:contract_address) |
||||
|
||||
transaction = |
||||
%Transaction{block: block} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block() |
||||
|
||||
log1_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some topic" |
||||
] |
||||
|
||||
log2_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some other topic" |
||||
] |
||||
|
||||
log1 = insert(:log, log1_details) |
||||
_log2 = insert(:log, log2_details) |
||||
|
||||
filter = %{ |
||||
from_block: block.number, |
||||
to_block: block.number, |
||||
first_topic: log1.first_topic |
||||
} |
||||
|
||||
[found_log] = Logs.list_logs(filter) |
||||
|
||||
assert found_log.first_topic == log1.first_topic |
||||
assert found_log.index == log1.index |
||||
end |
||||
|
||||
test "with a valid topic{x} AND another" do |
||||
contract_address = insert(:contract_address) |
||||
|
||||
transaction = |
||||
%Transaction{block: block} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block() |
||||
|
||||
log1_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some first topic", |
||||
second_topic: "some second topic" |
||||
] |
||||
|
||||
log2_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some first topic", |
||||
second_topic: "some OTHER second topic" |
||||
] |
||||
|
||||
_log1 = insert(:log, log1_details) |
||||
log2 = insert(:log, log2_details) |
||||
|
||||
filter = %{ |
||||
from_block: block.number, |
||||
to_block: block.number, |
||||
first_topic: log2.first_topic, |
||||
second_topic: log2.second_topic, |
||||
topic0_1_opr: "and" |
||||
} |
||||
|
||||
[found_log] = Logs.list_logs(filter) |
||||
|
||||
assert found_log.second_topic == log2.second_topic |
||||
assert found_log.first_topic == log2.first_topic |
||||
assert found_log.index == log2.index |
||||
end |
||||
|
||||
test "with a valid topic{x} OR another" do |
||||
contract_address = insert(:contract_address) |
||||
|
||||
transaction = |
||||
%Transaction{block: block} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block() |
||||
|
||||
log1_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some first topic", |
||||
second_topic: "some second topic" |
||||
] |
||||
|
||||
log2_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some OTHER first topic", |
||||
second_topic: "some OTHER second topic" |
||||
] |
||||
|
||||
log1 = insert(:log, log1_details) |
||||
log2 = insert(:log, log2_details) |
||||
|
||||
filter = %{ |
||||
from_block: block.number, |
||||
to_block: block.number, |
||||
first_topic: log1.first_topic, |
||||
second_topic: log2.second_topic, |
||||
topic0_1_opr: "or" |
||||
} |
||||
|
||||
found_logs = Logs.list_logs(filter) |
||||
|
||||
assert length(found_logs) == 2 |
||||
end |
||||
|
||||
test "with address and topic{x}" do |
||||
contract_address = insert(:contract_address) |
||||
|
||||
transaction = |
||||
%Transaction{block: block} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block() |
||||
|
||||
log1_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some first topic" |
||||
] |
||||
|
||||
log2_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some OTHER first topic" |
||||
] |
||||
|
||||
_log1 = insert(:log, log1_details) |
||||
log2 = insert(:log, log2_details) |
||||
|
||||
filter = %{ |
||||
from_block: block.number, |
||||
to_block: block.number, |
||||
address_hash: contract_address.hash, |
||||
first_topic: log2.first_topic |
||||
} |
||||
|
||||
[found_log] = Logs.list_logs(filter) |
||||
|
||||
assert found_log.index == log2.index |
||||
assert found_log.first_topic == log2.first_topic |
||||
end |
||||
|
||||
test "with address and two topic{x}s" do |
||||
contract_address = insert(:contract_address) |
||||
|
||||
transaction = |
||||
%Transaction{block: block} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block() |
||||
|
||||
log1_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some first topic", |
||||
second_topic: "some second topic" |
||||
] |
||||
|
||||
log2_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some OTHER first topic", |
||||
second_topic: "some OTHER second topic" |
||||
] |
||||
|
||||
_log1 = insert(:log, log1_details) |
||||
log2 = insert(:log, log2_details) |
||||
|
||||
filter = %{ |
||||
from_block: block.number, |
||||
to_block: block.number, |
||||
address_hash: contract_address.hash, |
||||
first_topic: log2.first_topic, |
||||
second_topic: log2.second_topic, |
||||
topic0_1_opr: "and" |
||||
} |
||||
|
||||
[found_log] = Logs.list_logs(filter) |
||||
|
||||
assert found_log.index == log2.index |
||||
assert found_log.first_topic == log2.first_topic |
||||
end |
||||
|
||||
test "with address and three topic{x}s with AND operator" do |
||||
contract_address = insert(:contract_address) |
||||
|
||||
transaction = |
||||
%Transaction{block: block} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block() |
||||
|
||||
log1_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some first topic", |
||||
second_topic: "some second topic", |
||||
third_topic: "some third topic" |
||||
] |
||||
|
||||
log2_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some OTHER first topic", |
||||
second_topic: "some OTHER second topic", |
||||
third_topic: "some OTHER third topic" |
||||
] |
||||
|
||||
log3_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some ALT first topic", |
||||
second_topic: "some ALT second topic", |
||||
third_topic: "some ALT third topic" |
||||
] |
||||
|
||||
_log1 = insert(:log, log1_details) |
||||
log2 = insert(:log, log2_details) |
||||
_log3 = insert(:log, log3_details) |
||||
|
||||
filter = %{ |
||||
from_block: block.number, |
||||
to_block: block.number, |
||||
address_hash: contract_address.hash, |
||||
first_topic: log2.first_topic, |
||||
second_topic: log2.second_topic, |
||||
third_topic: log2.third_topic, |
||||
topic0_1_opr: "and", |
||||
topic0_2_opr: "and", |
||||
topic1_2_opr: "and" |
||||
} |
||||
|
||||
[found_log] = Logs.list_logs(filter) |
||||
|
||||
assert found_log.index == log2.index |
||||
assert found_log.first_topic == log2.first_topic |
||||
assert found_log.second_topic == log2.second_topic |
||||
assert found_log.third_topic == log2.third_topic |
||||
end |
||||
|
||||
test "with address and three topic{x}s with OR operator" do |
||||
contract_address = insert(:contract_address) |
||||
|
||||
transaction = |
||||
%Transaction{block: block} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block() |
||||
|
||||
log1_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some first topic", |
||||
second_topic: "some second topic", |
||||
third_topic: "some third topic" |
||||
] |
||||
|
||||
log2_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some OTHER first topic", |
||||
second_topic: "some OTHER second topic", |
||||
third_topic: "some OTHER third topic" |
||||
] |
||||
|
||||
log3_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some ALT first topic", |
||||
second_topic: "some ALT second topic", |
||||
third_topic: "some ALT third topic" |
||||
] |
||||
|
||||
log1 = insert(:log, log1_details) |
||||
log2 = insert(:log, log2_details) |
||||
_log3 = insert(:log, log3_details) |
||||
|
||||
filter = %{ |
||||
from_block: block.number, |
||||
to_block: block.number, |
||||
address_hash: contract_address.hash, |
||||
first_topic: log1.first_topic, |
||||
second_topic: log2.second_topic, |
||||
third_topic: log2.third_topic, |
||||
topic0_1_opr: "or", |
||||
topic0_2_opr: "or", |
||||
topic1_2_opr: "or" |
||||
} |
||||
|
||||
[found_log] = Logs.list_logs(filter) |
||||
|
||||
assert found_log.index == log2.index |
||||
assert found_log.first_topic == log2.first_topic |
||||
assert found_log.second_topic == log2.second_topic |
||||
assert found_log.third_topic == log2.third_topic |
||||
end |
||||
|
||||
test "three topic{x}s with OR and AND operator" do |
||||
contract_address = insert(:contract_address) |
||||
|
||||
transaction = |
||||
%Transaction{block: block} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block() |
||||
|
||||
log1_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some topic", |
||||
second_topic: "some second topic", |
||||
third_topic: "some third topic" |
||||
] |
||||
|
||||
log2_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some topic", |
||||
second_topic: "some OTHER second topic", |
||||
third_topic: "some third topic" |
||||
] |
||||
|
||||
log3_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some topic", |
||||
second_topic: "some second topic", |
||||
third_topic: "some third topic" |
||||
] |
||||
|
||||
log1 = insert(:log, log1_details) |
||||
log2 = insert(:log, log2_details) |
||||
_log3 = insert(:log, log3_details) |
||||
|
||||
filter = %{ |
||||
from_block: block.number, |
||||
to_block: block.number, |
||||
address_hash: contract_address.hash, |
||||
first_topic: log1.first_topic, |
||||
second_topic: log2.second_topic, |
||||
third_topic: log2.third_topic, |
||||
topic0_1_opr: "or", |
||||
topic0_2_opr: "or", |
||||
topic1_2_opr: "and" |
||||
} |
||||
|
||||
[found_log] = Logs.list_logs(filter) |
||||
|
||||
assert found_log.index == log2.index |
||||
assert found_log.first_topic == log2.first_topic |
||||
assert found_log.second_topic == log2.second_topic |
||||
assert found_log.third_topic == log2.third_topic |
||||
end |
||||
|
||||
test "four topic{x}s with all possible operators" do |
||||
contract_address = insert(:contract_address) |
||||
|
||||
transaction = |
||||
%Transaction{block: block} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block() |
||||
|
||||
log1_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some topic", |
||||
second_topic: "some second topic" |
||||
] |
||||
|
||||
log2_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some OTHER topic", |
||||
second_topic: "some OTHER second topic", |
||||
third_topic: "some OTHER third topic", |
||||
fourth_topic: "some fourth topic" |
||||
] |
||||
|
||||
log3_details = [ |
||||
address: contract_address, |
||||
transaction: transaction, |
||||
first_topic: "some topic", |
||||
second_topic: "some second topic", |
||||
third_topic: "some third topic", |
||||
fourth_topic: "some fourth topic" |
||||
] |
||||
|
||||
log1 = insert(:log, log1_details) |
||||
log2 = insert(:log, log2_details) |
||||
_log3 = insert(:log, log3_details) |
||||
|
||||
filter = %{ |
||||
from_block: block.number, |
||||
to_block: block.number, |
||||
address_hash: contract_address.hash, |
||||
first_topic: log1.first_topic, |
||||
second_topic: log2.second_topic, |
||||
third_topic: log2.third_topic, |
||||
fourth_topic: log2.fourth_topic, |
||||
topic0_1_opr: "or", |
||||
topic0_2_opr: "or", |
||||
topic0_3_opr: "or", |
||||
topic1_2_opr: "and", |
||||
topic1_3_opr: "and", |
||||
topic2_3_opr: "and" |
||||
} |
||||
|
||||
[found_log] = Logs.list_logs(filter) |
||||
|
||||
assert found_log.index == log2.index |
||||
assert found_log.first_topic == log2.first_topic |
||||
assert found_log.second_topic == log2.second_topic |
||||
assert found_log.third_topic == log2.third_topic |
||||
end |
||||
|
||||
test "returned logs are sorted by block" do |
||||
first_block = insert(:block) |
||||
second_block = insert(:block) |
||||
third_block = insert(:block) |
||||
|
||||
contract_address = insert(:contract_address) |
||||
|
||||
transaction_block1 = |
||||
%Transaction{} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block(first_block) |
||||
|
||||
transaction_block2 = |
||||
%Transaction{} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block(second_block) |
||||
|
||||
transaction_block3 = |
||||
%Transaction{} = |
||||
:transaction |
||||
|> insert() |
||||
|> with_block(third_block) |
||||
|
||||
insert(:log, address: contract_address, transaction: transaction_block3) |
||||
insert(:log, address: contract_address, transaction: transaction_block1) |
||||
insert(:log, address: contract_address, transaction: transaction_block2) |
||||
|
||||
filter = %{ |
||||
from_block: first_block.number, |
||||
to_block: third_block.number, |
||||
address_hash: contract_address.hash |
||||
} |
||||
|
||||
found_logs = Logs.list_logs(filter) |
||||
|
||||
block_number_order = Enum.map(found_logs, & &1.block_number) |
||||
|
||||
assert block_number_order == Enum.sort(block_number_order) |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue