Merge pull request #544 from poanetwork/sa-api-logs-module-getLogs-action
Add event logs API supportpull/555/head
commit
514f00b080
@ -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