Add event logs API support

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
Sebastian Abondano 6 years ago
parent ca63a6fc0b
commit e4dcdc3e92
  1. 238
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/logs_controller.ex
  2. 197
      apps/block_scout_web/lib/block_scout_web/etherscan.ex
  3. 3
      apps/block_scout_web/lib/block_scout_web/router.ex
  4. 2
      apps/block_scout_web/lib/block_scout_web/templates/api_docs/_module_card.html.eex
  5. 49
      apps/block_scout_web/lib/block_scout_web/views/api/rpc/logs_view.ex
  6. 771
      apps/block_scout_web/test/block_scout_web/controllers/api/rpc/logs_controller_test.exs
  7. 33
      apps/explorer/lib/explorer/etherscan.ex
  8. 150
      apps/explorer/lib/explorer/etherscan/logs.ex
  9. 630
      apps/explorer/test/explorer/etherscan/logs_test.exs

@ -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

@ -68,6 +68,35 @@ defmodule BlockScoutWeb.Etherscan do
"result" => []
}
@logs_getlogs_example_value %{
"status" => "1",
"message" => "OK",
result: [
%{
"address" => "0x33990122638b9132ca29c723bdf037f1a891a70c",
"topics" => [
"0xf63780e752c6a54a94fc52715dbc5518a3b4c3c2833d301a204226548a2a8545",
"0x72657075746174696f6e00000000000000000000000000000000000000000000",
"0x000000000000000000000000d9b2f59f3b5c7b3c67047d2f03c3e8052470be92"
],
"data" => "0x",
"blockNumber" => "0x5c958",
"timeStamp" => "0x561d688c",
"gasPrice" => "0xba43b7400",
"gasUsed" => "0x10682",
"logIndex" => "0x",
"transactionHash" => "0x0b03498648ae2da924f961dda00dc6bb0a8df15519262b7e012b7d67f4bb7e83",
"transactionIndex" => "0x"
}
]
}
@logs_getlogs_example_value_error %{
"status" => "0",
"message" => "Invalid address format",
"result" => nil
}
@status_type %{
type: "status",
enum: ~s(["0", "1"]),
@ -170,6 +199,52 @@ defmodule BlockScoutWeb.Etherscan do
}
}
@log %{
name: "Log",
fields: %{
address: @address_hash_type,
topics: %{
type: "topics",
definition: "An array including the topics for the log.",
example: ~s(["0xf63780e752c6a54a94fc52715dbc5518a3b4c3c2833d301a204226548a2a8545"])
},
data: %{
type: "data",
definition: "Non-indexed log parameters.",
example: ~s("0x")
},
blockNumber: %{
type: "block number",
definition: "A nonnegative number used to identify blocks.",
example: ~s("0x5c958")
},
timeStamp: %{
type: "timestamp",
definition: "The transaction's block-timestamp.",
example: ~s("0x561d688c")
},
gasPrice: %{
type: "wei",
definition: &__MODULE__.wei_type_definition/1,
example: ~s("0xba43b7400")
},
gasUsed: %{
type: "gas",
definition: "A nonnegative number roughly equivalent to computational steps.",
example: ~s("0x10682")
},
logIndex: %{
type: "hexadecimal",
example: ~s("0x")
},
transactionHash: @transaction_hash_type,
transactionIndex: %{
type: "hexadecimal",
example: ~s("0x")
}
}
}
@account_balance_action %{
name: "balance",
description: "Get balance for address",
@ -308,6 +383,121 @@ defmodule BlockScoutWeb.Etherscan do
]
}
@logs_getlogs_action %{
name: "getLogs",
description: "Get event logs for an address and/or topics. Up to a maximum of 1,000 event logs.",
required_params: [
%{
key: "fromBlock",
placeholder: "blockNumber",
type: "integer",
description:
"A nonnegative integer that represents the starting block number. The use of 'latest' is also supported."
},
%{
key: "toBlock",
placeholder: "blockNumber",
type: "integer",
description:
"A nonnegative integer that represents the ending block number. The use of 'latest' is also supported."
},
%{
key: "address",
placeholder: "addressHash",
type: "string",
description: "A 160-bit code used for identifying contracts. An address and/or topic{x} is required."
},
%{
key: "topic0",
placeholder: "firstTopic",
type: "string",
description: "A string equal to the first topic. A topic{x} and/or address is required."
}
],
optional_params: [
%{
key: "topic1",
type: "string",
description: "A string equal to the second topic. A topic{x} and/or address is required."
},
%{
key: "topic2",
type: "string",
description: "A string equal to the third topic. A topic{x} and/or address is required."
},
%{
key: "topic3",
type: "string",
description: "A string equal to the fourth topic. A topic{x} and/or address is required."
},
%{
key: "topic0_1_opr",
type: "string",
description:
"A string representing the and|or operator for topic0 and topic1. " <>
"Required if topic0 and topic1 is used. Available values: and, or"
},
%{
key: "topic0_2_opr",
type: "string",
description:
"A string representing the and|or operator for topic0 and topic2. " <>
"Required if topic0 and topic2 is used. Available values: and, or"
},
%{
key: "topic0_3_opr",
type: "string",
description:
"A string representing the and|or operator for topic0 and topic3. " <>
"Required if topic0 and topic3 is used. Available values: and, or"
},
%{
key: "topic1_2_opr",
type: "string",
description:
"A string representing the and|or operator for topic1 and topic2. " <>
"Required if topic1 and topic2 is used. Available values: and, or"
},
%{
key: "topic1_3_opr",
type: "string",
description:
"A string representing the and|or operator for topic1 and topic3. " <>
"Required if topic1 and topic3 is used. Available values: and, or"
},
%{
key: "topic2_3_opr",
type: "string",
description:
"A string representing the and|or operator for topic2 and topic3. " <>
"Required if topic2 and topic3 is used. Available values: and, or"
}
],
responses: [
%{
code: "200",
description: "successful operation",
example_value: Jason.encode!(@logs_getlogs_example_value),
model: %{
name: "Result",
fields: %{
status: @status_type,
message: @message_type,
result: %{
type: "array",
array_type: @log
}
}
}
},
%{
code: "200",
description: "error",
example_value: Jason.encode!(@logs_getlogs_example_value_error)
}
]
}
@account_module %{
name: "account",
actions: [
@ -317,7 +507,12 @@ defmodule BlockScoutWeb.Etherscan do
]
}
@documentation [@account_module]
@logs_module %{
name: "logs",
actions: [@logs_getlogs_action]
}
@documentation [@account_module, @logs_module]
def get_documentation do
@documentation

@ -30,7 +30,8 @@ defmodule BlockScoutWeb.Router do
forward("/", RPCTranslator, %{
"block" => RPC.BlockController,
"account" => RPC.AddressController
"account" => RPC.AddressController,
"logs" => RPC.LogsController
})
end

@ -1,6 +1,6 @@
<div class="card">
<h2 class="card-header">
<%= "#{String.capitalize(@module.name)}s" %>
<%= "#{String.capitalize(@module.name)}" %>
<small class="text-primary">?module=<%= @module.name %></small>
</h2>

@ -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

@ -5,6 +5,7 @@ defmodule Explorer.Etherscan do
import Ecto.Query, only: [from: 2, where: 3]
alias Explorer.Etherscan.Logs
alias Explorer.{Repo, Chain}
alias Explorer.Chain.{Hash, Transaction}
@ -99,4 +100,36 @@ defmodule Explorer.Etherscan do
end
defp offset(options), do: (options.page_number - 1) * options.page_size
@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: Logs.list_logs(filter)
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…
Cancel
Save