feat: Advanced Filters (#9769)
* feat: Advanced Filters * Fix query performance * Fix timestamp filtering; Fix query construction * Add csv export * Frontend integration Add search_params to response Add limit to tokens endpoint Add fee in api response Add exclusion/inclusion of from/to addresses Remove raw_input from api response * Remove comment * Add methods search; Optimize internal txs query * Fix `method_id_to_name_from_params` * Fix filtering by amount; add filter by native * Fix review comments * Handle all token types * Optimize query * Process review comments * Process review comments --------- Co-authored-by: Viktor Baranov <baranov.viktor.27@gmail.com>pull/10240/head
parent
569cb8bbb6
commit
e02dde7ee9
@ -0,0 +1,372 @@ |
||||
defmodule BlockScoutWeb.API.V2.AdvancedFilterController do |
||||
use BlockScoutWeb, :controller |
||||
|
||||
import BlockScoutWeb.Chain, only: [default_paging_options: 0, split_list_by_page: 1, next_page_params: 4] |
||||
|
||||
alias BlockScoutWeb.API.V2.{AdvancedFilterView, CSVExportController, TransactionView} |
||||
alias Explorer.{Chain, PagingOptions} |
||||
alias Explorer.Chain.{AdvancedFilter, ContractMethod, Data, Token, Transaction} |
||||
alias Explorer.Chain.CSVExport.Helper, as: CSVHelper |
||||
alias Plug.Conn |
||||
|
||||
action_fallback(BlockScoutWeb.API.V2.FallbackController) |
||||
|
||||
@api_true [api?: true] |
||||
|
||||
@methods [ |
||||
%{method_id: "0xa9059cbb", name: "transfer"}, |
||||
%{method_id: "0xa0712d68", name: "mint"}, |
||||
%{method_id: "0x095ea7b3", name: "approve"}, |
||||
%{method_id: "0x40993b26", name: "buy"}, |
||||
%{method_id: "0x3593564c", name: "execute"}, |
||||
%{method_id: "0x3ccfd60b", name: "withdraw"}, |
||||
%{method_id: "0xd0e30db0", name: "deposit"}, |
||||
%{method_id: "0x0a19b14a", name: "trade"}, |
||||
%{method_id: "0x4420e486", name: "register"}, |
||||
%{method_id: "0x5f575529", name: "swap"}, |
||||
%{method_id: "0xd9627aa4", name: "sellToUniswap"}, |
||||
%{method_id: "0xe9e05c42", name: "depositTransaction"}, |
||||
%{method_id: "0x23b872dd", name: "transferFrom"}, |
||||
%{method_id: "0xa22cb465", name: "setApprovalForAll"}, |
||||
%{method_id: "0x2e7ba6ef", name: "claim"}, |
||||
%{method_id: "0x0502b1c5", name: "unoswap"}, |
||||
%{method_id: "0xb2267a7b", name: "sendMessage"}, |
||||
%{method_id: "0x9871efa4", name: "unxswapByOrderId"}, |
||||
%{method_id: "0xbf6eac2f", name: "stake"}, |
||||
%{method_id: "0x3ce33bff", name: "bridge"}, |
||||
%{method_id: "0xeb672419", name: "requestL2Transaction"}, |
||||
%{method_id: "0xe449022e", name: "uniswapV3Swap"}, |
||||
%{method_id: "0x0162e2d0", name: "swapETHForExactTokens"} |
||||
] |
||||
|
||||
@methods_id_to_name_map Map.new(@methods, fn %{method_id: method_id, name: name} -> {method_id, name} end) |
||||
@methods_name_to_id_map Map.new(@methods, fn %{method_id: method_id, name: name} -> {name, method_id} end) |
||||
|
||||
@methods_filter_limit 20 |
||||
@tokens_filter_limit 20 |
||||
|
||||
@doc """ |
||||
Function responsible for `api/v2/advanced-filters/` endpoint. |
||||
""" |
||||
@spec list(Plug.Conn.t(), map()) :: Plug.Conn.t() |
||||
def list(conn, params) do |
||||
full_options = params |> extract_filters() |> Keyword.merge(paging_options(params)) |> Keyword.merge(@api_true) |
||||
|
||||
advanced_filters_plus_one = AdvancedFilter.list(full_options) |
||||
|
||||
{advanced_filters, next_page} = split_list_by_page(advanced_filters_plus_one) |
||||
|
||||
{decoded_transactions, _abi_acc, methods_acc} = |
||||
advanced_filters |
||||
|> Enum.map(fn af -> %Transaction{to_address: af.to_address, input: af.input, hash: af.hash} end) |
||||
|> TransactionView.decode_transactions(true) |
||||
|
||||
next_page_params = |
||||
next_page |> next_page_params(advanced_filters, Map.take(params, ["items_count"]), &paging_params/1) |
||||
|
||||
render(conn, :advanced_filters, |
||||
advanced_filters: advanced_filters, |
||||
decoded_transactions: decoded_transactions, |
||||
search_params: %{ |
||||
method_ids: method_id_to_name_from_params(full_options[:methods] || [], methods_acc), |
||||
tokens: contract_address_hash_to_token_from_params(full_options[:token_contract_address_hashes]) |
||||
}, |
||||
next_page_params: next_page_params |
||||
) |
||||
end |
||||
|
||||
@doc """ |
||||
Function responsible for `api/v2/advanced-filters/csv` endpoint. |
||||
""" |
||||
@spec list_csv(Plug.Conn.t(), map()) :: Plug.Conn.t() |
||||
def list_csv(conn, params) do |
||||
with {:recaptcha, true} <- |
||||
{:recaptcha, |
||||
Application.get_env(:block_scout_web, :recaptcha)[:is_disabled] || |
||||
CSVHelper.captcha_helper().recaptcha_passed?(params["recaptcha_response"])} do |
||||
full_options = |
||||
params |
||||
|> extract_filters() |
||||
|> Keyword.merge(paging_options(params)) |
||||
|> Keyword.update(:paging_options, %PagingOptions{page_size: CSVHelper.limit()}, fn paging_options -> |
||||
%PagingOptions{paging_options | page_size: CSVHelper.limit()} |
||||
end) |
||||
|
||||
full_options |
||||
|> AdvancedFilter.list() |
||||
|> AdvancedFilterView.to_csv_format() |
||||
|> CSVHelper.dump_to_stream() |
||||
|> Enum.reduce_while(CSVExportController.put_resp_params(conn), fn chunk, conn -> |
||||
case Conn.chunk(conn, chunk) do |
||||
{:ok, conn} -> |
||||
{:cont, conn} |
||||
|
||||
{:error, :closed} -> |
||||
{:halt, conn} |
||||
end |
||||
end) |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Function responsible for `api/v2/advanced-filters/methods` endpoint, |
||||
including `api/v2/advanced-filters/methods/?q=:search_string`. |
||||
""" |
||||
@spec list_methods(Plug.Conn.t(), map()) :: {:method, nil | Explorer.Chain.ContractMethod.t()} | Plug.Conn.t() |
||||
def list_methods(conn, %{"q" => query}) do |
||||
case {@methods_id_to_name_map[query], @methods_name_to_id_map[query]} do |
||||
{name, _} when is_binary(name) -> |
||||
render(conn, :methods, methods: [%{method_id: query, name: name}]) |
||||
|
||||
{_, id} when is_binary(id) -> |
||||
render(conn, :methods, methods: [%{method_id: id, name: query}]) |
||||
|
||||
_ -> |
||||
mb_contract_method = |
||||
case Data.cast(query) do |
||||
{:ok, %Data{bytes: <<_::bytes-size(4)>> = binary_method_id}} -> |
||||
ContractMethod.find_contract_method_by_selector_id(binary_method_id, @api_true) |
||||
|
||||
_ -> |
||||
ContractMethod.find_contract_method_by_name(query, @api_true) |
||||
end |
||||
|
||||
with {:method, %ContractMethod{abi: %{"name" => name}, identifier: identifier}} <- {:method, mb_contract_method} do |
||||
render(conn, :methods, methods: [%{method_id: "0x" <> Base.encode16(identifier, case: :lower), name: name}]) |
||||
end |
||||
end |
||||
end |
||||
|
||||
def list_methods(conn, _params) do |
||||
render(conn, :methods, methods: @methods) |
||||
end |
||||
|
||||
defp method_id_to_name_from_params(prepared_method_ids, methods_acc) do |
||||
{decoded_method_ids, method_ids_to_find} = |
||||
Enum.reduce(prepared_method_ids, {%{}, []}, fn method_id, {decoded, to_decode} -> |
||||
{:ok, method_id_hash} = Data.cast(method_id) |
||||
|
||||
case {Map.get(@methods_id_to_name_map, method_id), |
||||
methods_acc |
||||
|> Map.get(method_id_hash.bytes, []) |
||||
|> Enum.find( |
||||
&match?(%ContractMethod{abi: %{"type" => "function", "name" => name}} when is_binary(name), &1) |
||||
)} do |
||||
{name, _} when is_binary(name) -> |
||||
{Map.put(decoded, method_id, name), to_decode} |
||||
|
||||
{_, %ContractMethod{abi: %{"type" => "function", "name" => name}}} when is_binary(name) -> |
||||
{Map.put(decoded, method_id, name), to_decode} |
||||
|
||||
{nil, nil} -> |
||||
{decoded, [method_id_hash.bytes | to_decode]} |
||||
end |
||||
end) |
||||
|
||||
method_ids_to_find |
||||
|> ContractMethod.find_contract_methods(@api_true) |
||||
|> Enum.reduce(%{}, fn contract_method, acc -> |
||||
case contract_method do |
||||
%ContractMethod{abi: %{"name" => name}, identifier: identifier} when is_binary(name) -> |
||||
Map.put(acc, "0x" <> Base.encode16(identifier, case: :lower), name) |
||||
|
||||
_ -> |
||||
acc |
||||
end |
||||
end) |
||||
|> Map.merge(decoded_method_ids) |
||||
end |
||||
|
||||
defp contract_address_hash_to_token_from_params(tokens) do |
||||
token_contract_address_hashes_to_include = tokens[:include] || [] |
||||
|
||||
token_contract_address_hashes_to_exclude = tokens[:exclude] || [] |
||||
|
||||
token_contract_address_hashes_to_include |
||||
|> Kernel.++(token_contract_address_hashes_to_exclude) |
||||
|> Enum.reject(&(&1 == "native")) |
||||
|> Enum.uniq() |
||||
|> Enum.take(@tokens_filter_limit) |
||||
|> Token.get_by_contract_address_hashes(@api_true) |
||||
|> Map.new(fn token -> {token.contract_address_hash, token} end) |
||||
end |
||||
|
||||
defp extract_filters(params) do |
||||
[ |
||||
tx_types: prepare_tx_types(params["tx_types"]), |
||||
methods: params["methods"] |> prepare_methods(), |
||||
age: prepare_age(params["age_from"], params["age_to"]), |
||||
from_address_hashes: |
||||
prepare_include_exclude_address_hashes( |
||||
params["from_address_hashes_to_include"], |
||||
params["from_address_hashes_to_exclude"], |
||||
&prepare_address_hash/1 |
||||
), |
||||
to_address_hashes: |
||||
prepare_include_exclude_address_hashes( |
||||
params["to_address_hashes_to_include"], |
||||
params["to_address_hashes_to_exclude"], |
||||
&prepare_address_hash/1 |
||||
), |
||||
address_relation: prepare_address_relation(params["address_relation"]), |
||||
amount: prepare_amount(params["amount_from"], params["amount_to"]), |
||||
token_contract_address_hashes: |
||||
params["token_contract_address_hashes_to_include"] |
||||
|> prepare_include_exclude_address_hashes( |
||||
params["token_contract_address_hashes_to_exclude"], |
||||
&prepare_token_address_hash/1 |
||||
) |
||||
|> Enum.map(fn |
||||
{key, value} when is_list(value) -> {key, Enum.take(value, @tokens_filter_limit)} |
||||
key_value -> key_value |
||||
end) |
||||
] |
||||
end |
||||
|
||||
@allowed_tx_types ~w(COIN_TRANSFER ERC-20 ERC-404 ERC-721 ERC-1155) |
||||
|
||||
defp prepare_tx_types(tx_types) when is_binary(tx_types) do |
||||
tx_types |
||||
|> String.upcase() |
||||
|> String.split(",") |
||||
|> Enum.filter(&(&1 in @allowed_tx_types)) |
||||
end |
||||
|
||||
defp prepare_tx_types(_), do: nil |
||||
|
||||
defp prepare_methods(methods) when is_binary(methods) do |
||||
methods |
||||
|> String.downcase() |
||||
|> String.split(",") |
||||
|> Enum.filter(fn |
||||
"0x" <> method_id when byte_size(method_id) == 8 -> |
||||
case Base.decode16(method_id, case: :mixed) do |
||||
{:ok, _} -> true |
||||
_ -> false |
||||
end |
||||
|
||||
_ -> |
||||
false |
||||
end) |
||||
|> Enum.uniq() |
||||
|> Enum.take(@methods_filter_limit) |
||||
end |
||||
|
||||
defp prepare_methods(_), do: nil |
||||
|
||||
defp prepare_age(from, to), do: [from: parse_date(from), to: parse_date(to)] |
||||
|
||||
defp parse_date(string_date) do |
||||
case string_date && DateTime.from_iso8601(string_date) do |
||||
{:ok, date, _utc_offset} -> date |
||||
_ -> nil |
||||
end |
||||
end |
||||
|
||||
defp prepare_address_hashes(address_hashes, map_filter_function) |
||||
when is_binary(address_hashes) do |
||||
address_hashes |
||||
|> String.split(",") |
||||
|> Enum.flat_map(&map_filter_function.(&1)) |
||||
end |
||||
|
||||
defp prepare_address_hashes(_, _), do: nil |
||||
|
||||
defp prepare_address_hash(maybe_address_hash) do |
||||
case Chain.string_to_address_hash(maybe_address_hash) do |
||||
{:ok, address_hash} -> [address_hash] |
||||
_ -> [] |
||||
end |
||||
end |
||||
|
||||
defp prepare_token_address_hash(token_address_hash) do |
||||
case String.downcase(token_address_hash) do |
||||
"native" -> ["native"] |
||||
_ -> prepare_address_hash(token_address_hash) |
||||
end |
||||
end |
||||
|
||||
defp prepare_address_relation(relation) do |
||||
case relation && String.downcase(relation) do |
||||
r when r in [nil, "or"] -> :or |
||||
"and" -> :and |
||||
_ -> nil |
||||
end |
||||
end |
||||
|
||||
defp prepare_amount(from, to), do: [from: parse_decimal(from), to: parse_decimal(to)] |
||||
|
||||
defp parse_decimal(string_decimal) do |
||||
case string_decimal && Decimal.parse(string_decimal) do |
||||
{decimal, ""} -> decimal |
||||
_ -> nil |
||||
end |
||||
end |
||||
|
||||
defp prepare_include_exclude_address_hashes(include, exclude, map_filter_function) do |
||||
[ |
||||
include: prepare_address_hashes(include, map_filter_function), |
||||
exclude: prepare_address_hashes(exclude, map_filter_function) |
||||
] |
||||
end |
||||
|
||||
# Paging |
||||
|
||||
defp paging_options(%{ |
||||
"block_number" => block_number_string, |
||||
"transaction_index" => tx_index_string, |
||||
"internal_transaction_index" => internal_tx_index_string, |
||||
"token_transfer_index" => token_transfer_index_string, |
||||
"token_transfer_batch_index" => token_transfer_batch_index_string |
||||
}) do |
||||
with {block_number, ""} <- block_number_string && Integer.parse(block_number_string), |
||||
{tx_index, ""} <- tx_index_string && Integer.parse(tx_index_string), |
||||
{:ok, internal_tx_index} <- parse_nullable_integer_paging_parameter(internal_tx_index_string), |
||||
{:ok, token_transfer_index} <- parse_nullable_integer_paging_parameter(token_transfer_index_string), |
||||
{:ok, token_transfer_batch_index} <- parse_nullable_integer_paging_parameter(token_transfer_batch_index_string) do |
||||
[ |
||||
paging_options: %{ |
||||
default_paging_options() |
||||
| key: %{ |
||||
block_number: block_number, |
||||
transaction_index: tx_index, |
||||
internal_transaction_index: internal_tx_index, |
||||
token_transfer_index: token_transfer_index, |
||||
token_transfer_batch_index: token_transfer_batch_index |
||||
} |
||||
} |
||||
] |
||||
else |
||||
_ -> [paging_options: default_paging_options()] |
||||
end |
||||
end |
||||
|
||||
defp paging_options(_), do: [paging_options: default_paging_options()] |
||||
|
||||
defp parse_nullable_integer_paging_parameter(""), do: {:ok, nil} |
||||
|
||||
defp parse_nullable_integer_paging_parameter(string) when is_binary(string) do |
||||
case Integer.parse(string) do |
||||
{integer, ""} -> {:ok, integer} |
||||
_ -> {:error, :invalid_paging_parameter} |
||||
end |
||||
end |
||||
|
||||
defp parse_nullable_integer_paging_parameter(_), do: {:error, :invalid_paging_parameter} |
||||
|
||||
defp paging_params(%AdvancedFilter{ |
||||
block_number: block_number, |
||||
transaction_index: tx_index, |
||||
internal_transaction_index: internal_tx_index, |
||||
token_transfer_index: token_transfer_index, |
||||
token_transfer_batch_index: token_transfer_batch_index |
||||
}) do |
||||
%{ |
||||
block_number: block_number, |
||||
transaction_index: tx_index, |
||||
internal_transaction_index: internal_tx_index, |
||||
token_transfer_index: token_transfer_index, |
||||
token_transfer_batch_index: token_transfer_batch_index |
||||
} |
||||
end |
||||
end |
@ -0,0 +1,172 @@ |
||||
defmodule BlockScoutWeb.API.V2.AdvancedFilterView do |
||||
use BlockScoutWeb, :view |
||||
|
||||
alias BlockScoutWeb.API.V2.{Helper, TokenView, TransactionView} |
||||
alias Explorer.Chain.{Address, Data, Transaction} |
||||
alias Explorer.Market |
||||
alias Explorer.Market.MarketHistory |
||||
|
||||
def render("advanced_filters.json", %{ |
||||
advanced_filters: advanced_filters, |
||||
decoded_transactions: decoded_transactions, |
||||
search_params: %{ |
||||
method_ids: method_ids, |
||||
tokens: tokens |
||||
}, |
||||
next_page_params: next_page_params |
||||
}) do |
||||
%{ |
||||
items: |
||||
advanced_filters |
||||
|> Enum.zip(decoded_transactions) |
||||
|> Enum.map(fn {af, decoded_input} -> prepare_advanced_filter(af, decoded_input) end), |
||||
search_params: prepare_search_params(method_ids, tokens), |
||||
next_page_params: next_page_params |
||||
} |
||||
end |
||||
|
||||
def render("methods.json", %{methods: methods}) do |
||||
methods |
||||
end |
||||
|
||||
def to_csv_format(advanced_filters) do |
||||
exchange_rate = Market.get_coin_exchange_rate() |
||||
|
||||
date_to_prices = |
||||
Enum.reduce(advanced_filters, %{}, fn af, acc -> |
||||
date = DateTime.to_date(af.timestamp) |
||||
|
||||
if Map.has_key?(acc, date) do |
||||
acc |
||||
else |
||||
market_history = MarketHistory.price_at_date(date) |
||||
|
||||
Map.put( |
||||
acc, |
||||
date, |
||||
{market_history && market_history.opening_price, market_history && market_history.closing_price} |
||||
) |
||||
end |
||||
end) |
||||
|
||||
row_names = [ |
||||
"TxHash", |
||||
"Type", |
||||
"MethodId", |
||||
"UtcTimestamp", |
||||
"FromAddress", |
||||
"ToAddress", |
||||
"Value", |
||||
"TokenContractAddressHash", |
||||
"TokenDecimals", |
||||
"TokenSymbol", |
||||
"BlockNumber", |
||||
"Fee", |
||||
"CurrentPrice", |
||||
"TxDateOpeningPrice", |
||||
"TxDateClosingPrice" |
||||
] |
||||
|
||||
af_lists = |
||||
advanced_filters |
||||
|> Stream.map(fn advanced_filter -> |
||||
method_id = |
||||
case advanced_filter.input do |
||||
%{bytes: <<method_id::binary-size(4), _::binary>>} -> method_id |
||||
_ -> nil |
||||
end |
||||
|
||||
{opening_price, closing_price} = date_to_prices[DateTime.to_date(advanced_filter.timestamp)] |
||||
|
||||
[ |
||||
to_string(advanced_filter.hash), |
||||
advanced_filter.type, |
||||
method_id, |
||||
advanced_filter.timestamp, |
||||
Address.checksum(advanced_filter.from_address.hash), |
||||
Address.checksum(advanced_filter.to_address.hash), |
||||
advanced_filter.value, |
||||
if(advanced_filter.type != "coin_transfer", |
||||
do: advanced_filter.token_transfer.token.contract_address_hash, |
||||
else: nil |
||||
), |
||||
if(advanced_filter.type != "coin_transfer", do: advanced_filter.token_transfer.token.decimals, else: nil), |
||||
if(advanced_filter.type != "coin_transfer", do: advanced_filter.token_transfer.token.symbol, else: nil), |
||||
advanced_filter.block_number, |
||||
advanced_filter.fee, |
||||
exchange_rate.usd_value, |
||||
opening_price, |
||||
closing_price |
||||
] |
||||
end) |
||||
|
||||
Stream.concat([row_names], af_lists) |
||||
end |
||||
|
||||
defp prepare_advanced_filter(advanced_filter, decoded_input) do |
||||
%{ |
||||
hash: advanced_filter.hash, |
||||
type: advanced_filter.type, |
||||
method: |
||||
if(advanced_filter.type != "coin_transfer", |
||||
do: |
||||
TransactionView.method_name( |
||||
%Transaction{ |
||||
to_address: %Address{ |
||||
hash: advanced_filter.token_transfer.token.contract_address_hash, |
||||
contract_code: "0x" |> Data.cast() |> elem(1) |
||||
}, |
||||
input: advanced_filter.input |
||||
}, |
||||
decoded_input |
||||
), |
||||
else: |
||||
TransactionView.method_name( |
||||
%Transaction{to_address: advanced_filter.to_address, input: advanced_filter.input}, |
||||
decoded_input |
||||
) |
||||
), |
||||
from: |
||||
Helper.address_with_info( |
||||
nil, |
||||
advanced_filter.from_address, |
||||
advanced_filter.from_address.hash, |
||||
false |
||||
), |
||||
to: |
||||
Helper.address_with_info( |
||||
nil, |
||||
advanced_filter.to_address, |
||||
advanced_filter.to_address.hash, |
||||
false |
||||
), |
||||
value: advanced_filter.value, |
||||
total: |
||||
if(advanced_filter.type != "coin_transfer", |
||||
do: TransactionView.prepare_token_transfer_total(advanced_filter.token_transfer), |
||||
else: nil |
||||
), |
||||
token: |
||||
if(advanced_filter.type != "coin_transfer", |
||||
do: TokenView.render("token.json", %{token: advanced_filter.token_transfer.token}), |
||||
else: nil |
||||
), |
||||
timestamp: advanced_filter.timestamp, |
||||
block_number: advanced_filter.block_number, |
||||
transaction_index: advanced_filter.transaction_index, |
||||
internal_transaction_index: advanced_filter.internal_transaction_index, |
||||
token_transfer_index: advanced_filter.token_transfer_index, |
||||
token_transfer_batch_index: advanced_filter.token_transfer_batch_index, |
||||
fee: advanced_filter.fee |
||||
} |
||||
end |
||||
|
||||
defp prepare_search_params(method_ids, tokens) do |
||||
tokens_map = |
||||
Map.new(tokens, fn {contract_address_hash, token} -> |
||||
{contract_address_hash, TokenView.render("token.json", %{token: token})} |
||||
end) |
||||
|
||||
%{methods: method_ids, tokens: tokens_map} |
||||
end |
||||
end |
@ -0,0 +1,945 @@ |
||||
defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do |
||||
use BlockScoutWeb.ConnCase |
||||
|
||||
import Mox |
||||
|
||||
alias Explorer.Chain.{AdvancedFilter, Data} |
||||
alias Explorer.{Factory, TestHelper} |
||||
|
||||
describe "/advanced_filters" do |
||||
test "empty list", %{conn: conn} do |
||||
request = get(conn, "/api/v2/advanced-filters") |
||||
assert response = json_response(request, 200) |
||||
assert response["items"] == [] |
||||
assert response["next_page_params"] == nil |
||||
end |
||||
|
||||
test "get and paginate advanced filter (transactions split between pages)", %{conn: conn} do |
||||
first_tx = :transaction |> insert() |> with_block() |
||||
insert_list(3, :token_transfer, transaction: first_tx) |
||||
|
||||
for i <- 0..2 do |
||||
insert(:internal_transaction, |
||||
transaction: first_tx, |
||||
block_hash: first_tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
end |
||||
|
||||
insert_list(51, :transaction) |> with_block() |
||||
|
||||
request = get(conn, "/api/v2/advanced-filters") |
||||
assert response = json_response(request, 200) |
||||
request_2nd_page = get(conn, "/api/v2/advanced-filters", response["next_page_params"]) |
||||
|
||||
assert response_2nd_page = json_response(request_2nd_page, 200) |
||||
check_paginated_response(AdvancedFilter.list(), response["items"], response_2nd_page["items"]) |
||||
end |
||||
|
||||
test "get and paginate advanced filter (token transfers split between pages)", %{conn: conn} do |
||||
first_tx = :transaction |> insert() |> with_block() |
||||
insert_list(3, :token_transfer, transaction: first_tx) |
||||
|
||||
for i <- 0..2 do |
||||
insert(:internal_transaction, |
||||
transaction: first_tx, |
||||
block_hash: first_tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
end |
||||
|
||||
second_tx = :transaction |> insert() |> with_block() |
||||
insert_list(50, :token_transfer, transaction: second_tx, block_number: second_tx.block_number) |
||||
|
||||
request = get(conn, "/api/v2/advanced-filters") |
||||
assert response = json_response(request, 200) |
||||
request_2nd_page = get(conn, "/api/v2/advanced-filters", response["next_page_params"]) |
||||
assert response_2nd_page = json_response(request_2nd_page, 200) |
||||
|
||||
check_paginated_response(AdvancedFilter.list(), response["items"], response_2nd_page["items"]) |
||||
end |
||||
|
||||
test "get and paginate advanced filter (batch token transfers split between pages)", %{conn: conn} do |
||||
first_tx = :transaction |> insert() |> with_block() |
||||
insert_list(3, :token_transfer, transaction: first_tx) |
||||
|
||||
for i <- 0..2 do |
||||
insert(:internal_transaction, |
||||
transaction: first_tx, |
||||
block_hash: first_tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
end |
||||
|
||||
second_tx = :transaction |> insert() |> with_block() |
||||
|
||||
insert_list(5, :token_transfer, |
||||
transaction: second_tx, |
||||
block_number: second_tx.block_number, |
||||
token_type: "ERC-1155", |
||||
token_ids: 0..10 |> Enum.to_list(), |
||||
amounts: 10..20 |> Enum.to_list() |
||||
) |
||||
|
||||
request = get(conn, "/api/v2/advanced-filters") |
||||
assert response = json_response(request, 200) |
||||
request_2nd_page = get(conn, "/api/v2/advanced-filters", response["next_page_params"]) |
||||
assert response_2nd_page = json_response(request_2nd_page, 200) |
||||
|
||||
check_paginated_response(AdvancedFilter.list(), response["items"], response_2nd_page["items"]) |
||||
end |
||||
|
||||
test "get and paginate advanced filter (internal transactions split between pages)", %{conn: conn} do |
||||
first_tx = :transaction |> insert() |> with_block() |
||||
insert_list(3, :token_transfer, transaction: first_tx) |
||||
|
||||
for i <- 0..2 do |
||||
insert(:internal_transaction, |
||||
transaction: first_tx, |
||||
block_hash: first_tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
end |
||||
|
||||
second_tx = :transaction |> insert() |> with_block() |
||||
|
||||
for i <- 0..49 do |
||||
insert(:internal_transaction, |
||||
transaction: second_tx, |
||||
block_hash: second_tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
end |
||||
|
||||
request = get(conn, "/api/v2/advanced-filters") |
||||
assert response = json_response(request, 200) |
||||
request_2nd_page = get(conn, "/api/v2/advanced-filters", response["next_page_params"]) |
||||
|
||||
assert response_2nd_page = json_response(request_2nd_page, 200) |
||||
check_paginated_response(AdvancedFilter.list(), response["items"], response_2nd_page["items"]) |
||||
end |
||||
|
||||
test "filter by tx_type", %{conn: conn} do |
||||
30 |> insert_list(:transaction) |> with_block() |
||||
|
||||
tx = insert(:transaction) |> with_block() |
||||
|
||||
for token_type <- ~w(ERC-20 ERC-404 ERC-721 ERC-1155), |
||||
_ <- 0..4 do |
||||
insert(:token_transfer, transaction: tx, token_type: token_type) |
||||
end |
||||
|
||||
tx = :transaction |> insert() |> with_block() |
||||
|
||||
for i <- 0..29 do |
||||
insert(:internal_transaction, |
||||
transaction: tx, |
||||
block_hash: tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
end |
||||
|
||||
for tx_type_filter_string <- |
||||
~w(COIN_TRANSFER COIN_TRANSFER,ERC-404 ERC-721,ERC-1155 ERC-20,COIN_TRANSFER,ERC-1155) do |
||||
tx_type_filter = tx_type_filter_string |> String.split(",") |
||||
request = get(conn, "/api/v2/advanced-filters", %{"tx_types" => tx_type_filter_string}) |
||||
assert response = json_response(request, 200) |
||||
|
||||
assert Enum.all?(response["items"], fn item -> String.upcase(item["type"]) in tx_type_filter end) |
||||
|
||||
if response["next_page_params"] do |
||||
request_2nd_page = |
||||
get( |
||||
conn, |
||||
"/api/v2/advanced-filters", |
||||
Map.merge(%{"tx_types" => tx_type_filter_string}, response["next_page_params"]) |
||||
) |
||||
|
||||
assert response_2nd_page = json_response(request_2nd_page, 200) |
||||
|
||||
assert Enum.all?(response_2nd_page["items"], fn item -> String.upcase(item["type"]) in tx_type_filter end) |
||||
|
||||
check_paginated_response( |
||||
AdvancedFilter.list(tx_types: tx_type_filter), |
||||
response["items"], |
||||
response_2nd_page["items"] |
||||
) |
||||
end |
||||
end |
||||
end |
||||
|
||||
test "filter by methods", %{conn: conn} do |
||||
TestHelper.get_eip1967_implementation_zero_addresses() |
||||
|
||||
tx = :transaction |> insert() |> with_block() |
||||
|
||||
smart_contract = build(:smart_contract) |
||||
|
||||
contract_address = |
||||
insert(:address, |
||||
hash: address_hash(), |
||||
verified: true, |
||||
contract_code: Factory.contract_code_info().bytecode, |
||||
smart_contract: smart_contract |
||||
) |
||||
|
||||
method_id1_string = "0xa9059cbb" |
||||
method_id2_string = "0xa0712d68" |
||||
method_id3_string = "0x095ea7b3" |
||||
method_id4_string = "0x40993b26" |
||||
|
||||
{:ok, method1} = Data.cast(method_id1_string <> "ab0ba0") |
||||
{:ok, method2} = Data.cast(method_id2_string <> "ab0ba0") |
||||
{:ok, method3} = Data.cast(method_id3_string <> "ab0ba0") |
||||
{:ok, method4} = Data.cast(method_id4_string <> "ab0ba0") |
||||
|
||||
for i <- 0..4 do |
||||
insert(:internal_transaction, |
||||
transaction: tx, |
||||
to_address_hash: contract_address.hash, |
||||
to_address: contract_address, |
||||
block_hash: tx.block_hash, |
||||
index: i, |
||||
block_index: i, |
||||
input: method1 |
||||
) |
||||
end |
||||
|
||||
for i <- 5..9 do |
||||
insert(:internal_transaction, |
||||
transaction: tx, |
||||
to_address_hash: contract_address.hash, |
||||
to_address: contract_address, |
||||
block_hash: tx.block_hash, |
||||
index: i, |
||||
block_index: i, |
||||
input: method2 |
||||
) |
||||
end |
||||
|
||||
5 |
||||
|> insert_list(:transaction, to_address_hash: contract_address.hash, to_address: contract_address, input: method2) |
||||
|> with_block() |
||||
|
||||
5 |
||||
|> insert_list(:transaction, to_address_hash: contract_address.hash, to_address: contract_address, input: method3) |
||||
|> with_block() |
||||
|
||||
method3_transaction = |
||||
:transaction |
||||
|> insert(to_address_hash: contract_address.hash, to_address: contract_address, input: method3) |
||||
|> with_block() |
||||
|
||||
method4_transaction = |
||||
:transaction |
||||
|> insert(to_address_hash: contract_address.hash, to_address: contract_address, input: method4) |
||||
|> with_block() |
||||
|
||||
5 |> insert_list(:token_transfer, transaction: method3_transaction) |
||||
5 |> insert_list(:token_transfer, transaction: method4_transaction) |
||||
|
||||
request = get(conn, "/api/v2/advanced-filters", %{"methods" => "0xa0712d68,0x095ea7b3"}) |
||||
assert response = json_response(request, 200) |
||||
|
||||
assert Enum.all?(response["items"], fn item -> |
||||
String.slice(item["method"], 0..9) in [method_id2_string, method_id3_string] |
||||
end) |
||||
|
||||
assert Enum.count(response["items"]) == 21 |
||||
end |
||||
|
||||
test "filter by age", %{conn: conn} do |
||||
first_timestamp = ~U[2023-12-12 00:00:00.000000Z] |
||||
|
||||
for i <- 0..4 do |
||||
tx = :transaction |> insert() |> with_block(block_timestamp: Timex.shift(first_timestamp, days: i)) |
||||
|
||||
insert(:internal_transaction, |
||||
transaction: tx, |
||||
block_hash: tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
|
||||
insert(:token_transfer, transaction: tx, block_number: tx.block_number, log_index: i) |
||||
end |
||||
|
||||
request = |
||||
get(conn, "/api/v2/advanced-filters", %{ |
||||
"age_from" => "2023-12-14T00:00:00Z", |
||||
"age_to" => "2023-12-16T00:00:00Z" |
||||
}) |
||||
|
||||
assert response = json_response(request, 200) |
||||
|
||||
assert Enum.count(response["items"]) == 9 |
||||
end |
||||
|
||||
test "filter by from address include", %{conn: conn} do |
||||
address = insert(:address) |
||||
|
||||
for i <- 0..4 do |
||||
tx = :transaction |> insert() |> with_block() |
||||
|
||||
if i < 2 do |
||||
:transaction |> insert(from_address_hash: address.hash, from_address: address) |> with_block() |
||||
|
||||
insert(:internal_transaction, |
||||
transaction: tx, |
||||
from_address_hash: address.hash, |
||||
from_address: address, |
||||
block_hash: tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
|
||||
insert(:token_transfer, |
||||
from_address_hash: address.hash, |
||||
from_address: address, |
||||
transaction: tx, |
||||
block_number: tx.block_number, |
||||
log_index: i |
||||
) |
||||
else |
||||
insert(:internal_transaction, |
||||
transaction: tx, |
||||
block_hash: tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
|
||||
insert(:token_transfer, transaction: tx, block_number: tx.block_number, log_index: i) |
||||
end |
||||
end |
||||
|
||||
request = get(conn, "/api/v2/advanced-filters", %{"from_address_hashes_to_include" => to_string(address.hash)}) |
||||
|
||||
assert response = json_response(request, 200) |
||||
|
||||
assert Enum.count(response["items"]) == 6 |
||||
end |
||||
|
||||
test "filter by from address exclude", %{conn: conn} do |
||||
address = insert(:address) |
||||
|
||||
for i <- 0..4 do |
||||
tx = :transaction |> insert() |> with_block() |
||||
|
||||
if i < 4 do |
||||
:transaction |> insert(from_address_hash: address.hash, from_address: address) |> with_block() |
||||
|
||||
insert(:internal_transaction, |
||||
transaction: tx, |
||||
from_address_hash: address.hash, |
||||
from_address: address, |
||||
block_hash: tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
|
||||
insert(:token_transfer, |
||||
from_address_hash: address.hash, |
||||
from_address: address, |
||||
transaction: tx, |
||||
block_number: tx.block_number, |
||||
log_index: i |
||||
) |
||||
else |
||||
insert(:internal_transaction, |
||||
transaction: tx, |
||||
block_hash: tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
|
||||
insert(:token_transfer, transaction: tx, block_number: tx.block_number, log_index: i) |
||||
end |
||||
end |
||||
|
||||
request = get(conn, "/api/v2/advanced-filters", %{"from_address_hashes_to_exclude" => to_string(address.hash)}) |
||||
|
||||
assert response = json_response(request, 200) |
||||
|
||||
assert Enum.count(response["items"]) == 7 |
||||
end |
||||
|
||||
test "filter by from address include and exclude", %{conn: conn} do |
||||
address_to_include = insert(:address) |
||||
address_to_exclude = insert(:address) |
||||
|
||||
for i <- 0..2 do |
||||
tx = |
||||
:transaction |
||||
|> insert(from_address_hash: address_to_exclude.hash, from_address: address_to_exclude) |
||||
|> with_block() |
||||
|
||||
if i < 4 do |
||||
:transaction |
||||
|> insert(from_address_hash: address_to_include.hash, from_address: address_to_include) |
||||
|> with_block() |
||||
|
||||
insert(:internal_transaction, |
||||
transaction: tx, |
||||
from_address_hash: address_to_include.hash, |
||||
from_address: address_to_include, |
||||
block_hash: tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
|
||||
insert(:token_transfer, |
||||
from_address_hash: address_to_include.hash, |
||||
from_address: address_to_include, |
||||
transaction: tx, |
||||
block_number: tx.block_number, |
||||
log_index: i |
||||
) |
||||
else |
||||
insert(:internal_transaction, |
||||
transaction: tx, |
||||
block_hash: tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
|
||||
insert(:token_transfer, transaction: tx, block_number: tx.block_number, log_index: i) |
||||
end |
||||
end |
||||
|
||||
request = |
||||
get(conn, "/api/v2/advanced-filters", %{ |
||||
"from_address_hashes_to_include" => to_string(address_to_include.hash), |
||||
"from_address_hashes_to_exclude" => to_string(address_to_exclude.hash) |
||||
}) |
||||
|
||||
assert response = json_response(request, 200) |
||||
|
||||
assert Enum.count(response["items"]) == 9 |
||||
end |
||||
|
||||
test "filter by to address include", %{conn: conn} do |
||||
address = insert(:address) |
||||
|
||||
for i <- 0..4 do |
||||
tx = :transaction |> insert() |> with_block() |
||||
|
||||
if i < 2 do |
||||
:transaction |> insert(to_address_hash: address.hash, to_address: address) |> with_block() |
||||
|
||||
insert(:internal_transaction, |
||||
transaction: tx, |
||||
to_address_hash: address.hash, |
||||
to_address: address, |
||||
block_hash: tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
|
||||
insert(:token_transfer, |
||||
to_address_hash: address.hash, |
||||
to_address: address, |
||||
transaction: tx, |
||||
block_number: tx.block_number, |
||||
log_index: i |
||||
) |
||||
else |
||||
insert(:internal_transaction, |
||||
transaction: tx, |
||||
block_hash: tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
|
||||
insert(:token_transfer, transaction: tx, block_number: tx.block_number, log_index: i) |
||||
end |
||||
end |
||||
|
||||
request = get(conn, "/api/v2/advanced-filters", %{"to_address_hashes_to_include" => to_string(address.hash)}) |
||||
|
||||
assert response = json_response(request, 200) |
||||
|
||||
assert Enum.count(response["items"]) == 6 |
||||
end |
||||
|
||||
test "filter by to address exclude", %{conn: conn} do |
||||
address = insert(:address) |
||||
|
||||
for i <- 0..4 do |
||||
tx = :transaction |> insert() |> with_block() |
||||
|
||||
if i < 4 do |
||||
:transaction |> insert(to_address_hash: address.hash, to_address: address) |> with_block() |
||||
|
||||
insert(:internal_transaction, |
||||
transaction: tx, |
||||
to_address_hash: address.hash, |
||||
to_address: address, |
||||
block_hash: tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
|
||||
insert(:token_transfer, |
||||
to_address_hash: address.hash, |
||||
to_address: address, |
||||
transaction: tx, |
||||
block_number: tx.block_number, |
||||
log_index: i |
||||
) |
||||
else |
||||
insert(:internal_transaction, |
||||
transaction: tx, |
||||
block_hash: tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
|
||||
insert(:token_transfer, transaction: tx, block_number: tx.block_number, log_index: i) |
||||
end |
||||
end |
||||
|
||||
request = get(conn, "/api/v2/advanced-filters", %{"to_address_hashes_to_exclude" => to_string(address.hash)}) |
||||
|
||||
assert response = json_response(request, 200) |
||||
|
||||
assert Enum.count(response["items"]) == 7 |
||||
end |
||||
|
||||
test "filter by to address include and exclude", %{conn: conn} do |
||||
address_to_include = insert(:address) |
||||
address_to_exclude = insert(:address) |
||||
|
||||
for i <- 0..2 do |
||||
tx = |
||||
:transaction |
||||
|> insert(to_address_hash: address_to_exclude.hash, to_address: address_to_exclude) |
||||
|> with_block() |
||||
|
||||
if i < 4 do |
||||
:transaction |
||||
|> insert(to_address_hash: address_to_include.hash, to_address: address_to_include) |
||||
|> with_block() |
||||
|
||||
insert(:internal_transaction, |
||||
transaction: tx, |
||||
to_address_hash: address_to_include.hash, |
||||
to_address: address_to_include, |
||||
block_hash: tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
|
||||
insert(:token_transfer, |
||||
to_address_hash: address_to_include.hash, |
||||
to_address: address_to_include, |
||||
transaction: tx, |
||||
block_number: tx.block_number, |
||||
log_index: i |
||||
) |
||||
else |
||||
insert(:internal_transaction, |
||||
transaction: tx, |
||||
block_hash: tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
|
||||
insert(:token_transfer, transaction: tx, block_number: tx.block_number, log_index: i) |
||||
end |
||||
end |
||||
|
||||
request = |
||||
get(conn, "/api/v2/advanced-filters", %{ |
||||
"to_address_hashes_to_include" => to_string(address_to_include.hash), |
||||
"to_address_hashes_to_exclude" => to_string(address_to_exclude.hash) |
||||
}) |
||||
|
||||
assert response = json_response(request, 200) |
||||
|
||||
assert Enum.count(response["items"]) == 9 |
||||
end |
||||
|
||||
test "filter by from and to address", %{conn: conn} do |
||||
from_address = insert(:address) |
||||
to_address = insert(:address) |
||||
|
||||
for i <- 0..8 do |
||||
tx = :transaction |> insert() |> with_block() |
||||
|
||||
cond do |
||||
i < 2 -> |
||||
:transaction |> insert(from_address_hash: from_address.hash, from_address: from_address) |> with_block() |
||||
|
||||
insert(:internal_transaction, |
||||
transaction: tx, |
||||
from_address_hash: from_address.hash, |
||||
from_address: from_address, |
||||
block_hash: tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
|
||||
insert(:token_transfer, |
||||
from_address_hash: from_address.hash, |
||||
from_address: from_address, |
||||
transaction: tx, |
||||
block_number: tx.block_number, |
||||
log_index: i |
||||
) |
||||
|
||||
i < 4 -> |
||||
:transaction |> insert(to_address_hash: to_address.hash, to_address: to_address) |> with_block() |
||||
|
||||
insert(:internal_transaction, |
||||
transaction: tx, |
||||
to_address_hash: to_address.hash, |
||||
to_address: to_address, |
||||
block_hash: tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
|
||||
insert(:token_transfer, |
||||
to_address_hash: to_address.hash, |
||||
to_address: to_address, |
||||
transaction: tx, |
||||
block_number: tx.block_number, |
||||
log_index: i |
||||
) |
||||
|
||||
i < 6 -> |
||||
:transaction |
||||
|> insert( |
||||
to_address_hash: to_address.hash, |
||||
to_address: to_address, |
||||
from_address_hash: from_address.hash, |
||||
from_address: from_address |
||||
) |
||||
|> with_block() |
||||
|
||||
insert(:internal_transaction, |
||||
transaction: tx, |
||||
to_address_hash: to_address.hash, |
||||
to_address: to_address, |
||||
from_address_hash: from_address.hash, |
||||
from_address: from_address, |
||||
block_hash: tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
|
||||
insert(:token_transfer, |
||||
to_address_hash: to_address.hash, |
||||
to_address: to_address, |
||||
from_address_hash: from_address.hash, |
||||
from_address: from_address, |
||||
transaction: tx, |
||||
block_number: tx.block_number, |
||||
log_index: i |
||||
) |
||||
|
||||
true -> |
||||
insert(:internal_transaction, |
||||
transaction: tx, |
||||
block_hash: tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
|
||||
insert(:token_transfer, transaction: tx, block_number: tx.block_number, log_index: i) |
||||
end |
||||
end |
||||
|
||||
request = |
||||
get(conn, "/api/v2/advanced-filters", %{ |
||||
"from_address_hashes_to_include" => to_string(from_address.hash), |
||||
"to_address_hashes_to_include" => to_string(to_address.hash), |
||||
"address_relation" => "AnD" |
||||
}) |
||||
|
||||
assert response = json_response(request, 200) |
||||
|
||||
assert Enum.count(response["items"]) == 6 |
||||
end |
||||
|
||||
test "filter by from or to address", %{conn: conn} do |
||||
from_address = insert(:address) |
||||
to_address = insert(:address) |
||||
|
||||
for i <- 0..8 do |
||||
tx = :transaction |> insert() |> with_block() |
||||
|
||||
cond do |
||||
i < 2 -> |
||||
:transaction |> insert(from_address_hash: from_address.hash, from_address: from_address) |> with_block() |
||||
|
||||
insert(:internal_transaction, |
||||
transaction: tx, |
||||
from_address_hash: from_address.hash, |
||||
from_address: from_address, |
||||
block_hash: tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
|
||||
insert(:token_transfer, |
||||
from_address_hash: from_address.hash, |
||||
from_address: from_address, |
||||
transaction: tx, |
||||
block_number: tx.block_number, |
||||
log_index: i |
||||
) |
||||
|
||||
i < 4 -> |
||||
:transaction |> insert(to_address_hash: to_address.hash, to_address: to_address) |> with_block() |
||||
|
||||
insert(:internal_transaction, |
||||
transaction: tx, |
||||
to_address_hash: to_address.hash, |
||||
to_address: to_address, |
||||
block_hash: tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
|
||||
insert(:token_transfer, |
||||
to_address_hash: to_address.hash, |
||||
to_address: to_address, |
||||
transaction: tx, |
||||
block_number: tx.block_number, |
||||
log_index: i |
||||
) |
||||
|
||||
i < 6 -> |
||||
:transaction |
||||
|> insert( |
||||
to_address_hash: to_address.hash, |
||||
to_address: to_address, |
||||
from_address_hash: from_address.hash, |
||||
from_address: from_address |
||||
) |
||||
|> with_block() |
||||
|
||||
insert(:internal_transaction, |
||||
transaction: tx, |
||||
to_address_hash: to_address.hash, |
||||
to_address: to_address, |
||||
from_address_hash: from_address.hash, |
||||
from_address: from_address, |
||||
block_hash: tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
|
||||
insert(:token_transfer, |
||||
to_address_hash: to_address.hash, |
||||
to_address: to_address, |
||||
from_address_hash: from_address.hash, |
||||
from_address: from_address, |
||||
transaction: tx, |
||||
block_number: tx.block_number, |
||||
log_index: i |
||||
) |
||||
|
||||
true -> |
||||
insert(:internal_transaction, |
||||
transaction: tx, |
||||
block_hash: tx.block_hash, |
||||
index: i, |
||||
block_index: i |
||||
) |
||||
|
||||
insert(:token_transfer, transaction: tx, block_number: tx.block_number, log_index: i) |
||||
end |
||||
end |
||||
|
||||
request = |
||||
get(conn, "/api/v2/advanced-filters", %{ |
||||
"from_address_hashes_to_include" => to_string(from_address.hash), |
||||
"to_address_hashes_to_include" => to_string(to_address.hash) |
||||
}) |
||||
|
||||
assert response = json_response(request, 200) |
||||
|
||||
assert Enum.count(response["items"]) == 18 |
||||
end |
||||
|
||||
test "filter by amount", %{conn: conn} do |
||||
for i <- 0..4 do |
||||
tx = :transaction |> insert(value: i * 10 ** 18) |> with_block() |
||||
|
||||
insert(:internal_transaction, |
||||
transaction: tx, |
||||
block_hash: tx.block_hash, |
||||
index: 0, |
||||
block_index: 0, |
||||
value: i * 10 ** 18 |
||||
) |
||||
|
||||
token = insert(:token, decimals: 10) |
||||
|
||||
insert(:token_transfer, |
||||
amount: i * 10 ** 10, |
||||
token_contract_address: token.contract_address, |
||||
transaction: tx, |
||||
block_number: tx.block_number, |
||||
log_index: 0 |
||||
) |
||||
end |
||||
|
||||
request = get(conn, "/api/v2/advanced-filters", %{"amount_from" => "0.5", "amount_to" => "2.99"}) |
||||
assert response = json_response(request, 200) |
||||
|
||||
assert Enum.count(response["items"]) == 6 |
||||
end |
||||
|
||||
test "filter by token contract address include", %{conn: conn} do |
||||
token_a = insert(:token) |
||||
token_b = insert(:token) |
||||
token_c = insert(:token) |
||||
|
||||
tx = :transaction |> insert() |> with_block() |
||||
|
||||
for token <- [token_a, token_b, token_c, token_a, token_b, token_c, token_a, token_b, token_c] do |
||||
insert(:token_transfer, |
||||
token_contract_address: token.contract_address, |
||||
transaction: tx, |
||||
block_number: tx.block_number, |
||||
log_index: 0 |
||||
) |
||||
end |
||||
|
||||
request = |
||||
get(conn, "/api/v2/advanced-filters", %{ |
||||
"token_contract_address_hashes_to_include" => |
||||
"#{token_b.contract_address_hash},#{token_c.contract_address_hash}" |
||||
}) |
||||
|
||||
assert response = json_response(request, 200) |
||||
|
||||
assert Enum.count(response["items"]) == 6 |
||||
end |
||||
|
||||
test "filter by token contract address exclude", %{conn: conn} do |
||||
token_a = insert(:token) |
||||
token_b = insert(:token) |
||||
token_c = insert(:token) |
||||
|
||||
tx = :transaction |> insert() |> with_block() |
||||
|
||||
for token <- [token_a, token_b, token_c, token_a, token_b, token_c, token_a, token_b, token_c] do |
||||
insert(:token_transfer, |
||||
token_contract_address: token.contract_address, |
||||
transaction: tx, |
||||
block_number: tx.block_number, |
||||
log_index: 0 |
||||
) |
||||
end |
||||
|
||||
request = |
||||
get(conn, "/api/v2/advanced-filters", %{ |
||||
"token_contract_address_hashes_to_exclude" => |
||||
"#{token_b.contract_address_hash},#{token_c.contract_address_hash}" |
||||
}) |
||||
|
||||
assert response = json_response(request, 200) |
||||
|
||||
assert Enum.count(response["items"]) == 4 |
||||
end |
||||
|
||||
test "filter by token contract address include with native", %{conn: conn} do |
||||
token_a = insert(:token) |
||||
token_b = insert(:token) |
||||
token_c = insert(:token) |
||||
|
||||
tx = :transaction |> insert() |> with_block() |
||||
|
||||
for token <- [token_a, token_b, token_c, token_a, token_b, token_c, token_a, token_b, token_c] do |
||||
insert(:token_transfer, |
||||
token_contract_address: token.contract_address, |
||||
transaction: tx, |
||||
block_number: tx.block_number, |
||||
log_index: 0 |
||||
) |
||||
end |
||||
|
||||
request = |
||||
get(conn, "/api/v2/advanced-filters", %{ |
||||
"token_contract_address_hashes_to_include" => |
||||
"#{token_b.contract_address_hash},#{token_c.contract_address_hash},native" |
||||
}) |
||||
|
||||
assert response = json_response(request, 200) |
||||
|
||||
assert Enum.count(response["items"]) == 7 |
||||
end |
||||
|
||||
test "filter by token contract address exclude with native", %{conn: conn} do |
||||
token_a = insert(:token) |
||||
token_b = insert(:token) |
||||
token_c = insert(:token) |
||||
|
||||
tx = :transaction |> insert() |> with_block() |
||||
|
||||
for token <- [token_a, token_b, token_c, token_a, token_b, token_c, token_a, token_b, token_c] do |
||||
insert(:token_transfer, |
||||
token_contract_address: token.contract_address, |
||||
transaction: tx, |
||||
block_number: tx.block_number, |
||||
log_index: 0 |
||||
) |
||||
end |
||||
|
||||
request = |
||||
get(conn, "/api/v2/advanced-filters", %{ |
||||
"token_contract_address_hashes_to_exclude" => |
||||
"#{token_b.contract_address_hash},#{token_c.contract_address_hash},native" |
||||
}) |
||||
|
||||
assert response = json_response(request, 200) |
||||
|
||||
assert Enum.count(response["items"]) == 3 |
||||
end |
||||
end |
||||
|
||||
describe "/advanced_filters/methods?q=" do |
||||
test "returns 404 if method does not exist", %{conn: conn} do |
||||
request = get(conn, "/api/v2/advanced-filters/methods", %{"q" => "foo"}) |
||||
assert response = json_response(request, 404) |
||||
assert response["message"] == "Not found" |
||||
end |
||||
|
||||
test "finds method by name", %{conn: conn} do |
||||
insert(:contract_method) |
||||
request = get(conn, "/api/v2/advanced-filters/methods", %{"q" => "set"}) |
||||
assert response = json_response(request, 200) |
||||
assert response == [%{"method_id" => "0x60fe47b1", "name" => "set"}] |
||||
end |
||||
|
||||
test "finds method by id", %{conn: conn} do |
||||
insert(:contract_method) |
||||
request = get(conn, "/api/v2/advanced-filters/methods", %{"q" => "0x60fe47b1"}) |
||||
assert response = json_response(request, 200) |
||||
assert response == [%{"method_id" => "0x60fe47b1", "name" => "set"}] |
||||
end |
||||
end |
||||
|
||||
defp check_paginated_response(all_advanced_filters, first_page, second_page) do |
||||
assert all_advanced_filters |
||||
|> Enum.map( |
||||
&{&1.block_number, &1.transaction_index, &1.internal_transaction_index, &1.token_transfer_index, |
||||
&1.token_transfer_batch_index} |
||||
) == |
||||
Enum.map( |
||||
first_page ++ second_page, |
||||
&{&1["block_number"], &1["transaction_index"], &1["internal_transaction_index"], |
||||
&1["token_transfer_index"], &1["token_transfer_batch_index"]} |
||||
) |
||||
end |
||||
end |
@ -0,0 +1,706 @@ |
||||
defmodule Explorer.Chain.AdvancedFilter do |
||||
@moduledoc """ |
||||
Models an advanced filter. |
||||
""" |
||||
|
||||
use Explorer.Schema |
||||
|
||||
import Ecto.Query |
||||
|
||||
alias Explorer.{Chain, Helper, PagingOptions} |
||||
alias Explorer.Chain.{Address, Data, Hash, InternalTransaction, TokenTransfer, Transaction} |
||||
|
||||
@primary_key false |
||||
typed_embedded_schema null: false do |
||||
field(:hash, Hash.Full) |
||||
field(:type, :string) |
||||
field(:input, Data) |
||||
field(:timestamp, :utc_datetime_usec) |
||||
|
||||
belongs_to( |
||||
:from_address, |
||||
Address, |
||||
foreign_key: :from_address_hash, |
||||
references: :hash, |
||||
type: Hash.Address |
||||
) |
||||
|
||||
belongs_to( |
||||
:to_address, |
||||
Address, |
||||
foreign_key: :to_address_hash, |
||||
references: :hash, |
||||
type: Hash.Address |
||||
) |
||||
|
||||
field(:value, :decimal, null: true) |
||||
|
||||
has_one(:token_transfer, TokenTransfer, foreign_key: :transaction_hash, references: :hash, null: true) |
||||
|
||||
field(:fee, :decimal) |
||||
|
||||
field(:block_number, :integer) |
||||
field(:transaction_index, :integer) |
||||
field(:internal_transaction_index, :integer, null: true) |
||||
field(:token_transfer_index, :integer, null: true) |
||||
field(:token_transfer_batch_index, :integer, null: true) |
||||
end |
||||
|
||||
@typep tx_types :: {:tx_types, [String.t()] | nil} |
||||
@typep methods :: {:methods, [String.t()] | nil} |
||||
@typep age :: {:age, [{:from, DateTime.t() | nil} | {:to, DateTime.t() | nil}] | nil} |
||||
@typep from_address_hashes :: {:from_address_hashes, [Hash.Address.t()] | nil} |
||||
@typep to_address_hashes :: {:to_address_hashes, [Hash.Address.t()] | nil} |
||||
@typep address_relation :: {:address_relation, :or | :and | nil} |
||||
@typep amount :: {:amount, [{:from, Decimal.t()} | {:to, Decimal.t()}] | nil} |
||||
@typep token_contract_address_hashes :: |
||||
{:token_contract_address_hashes, [{:include, [Hash.Address.t()]} | {:include, [Hash.Address.t()]}] | nil} |
||||
@type options :: [ |
||||
tx_types() |
||||
| methods() |
||||
| age() |
||||
| from_address_hashes() |
||||
| to_address_hashes() |
||||
| address_relation() |
||||
| amount() |
||||
| token_contract_address_hashes() |
||||
| Chain.paging_options() |
||||
| Chain.api?() |
||||
] |
||||
|
||||
@spec list(options()) :: [__MODULE__.t()] |
||||
def list(options \\ []) do |
||||
paging_options = Keyword.get(options, :paging_options) |
||||
|
||||
tasks = |
||||
options |
||||
|> queries(paging_options) |
||||
|> Enum.map(fn query -> Task.async(fn -> Chain.select_repo(options).all(query) end) end) |
||||
|
||||
tasks |
||||
|> Task.yield_many(:timer.seconds(60)) |
||||
|> Enum.flat_map(fn {_task, res} -> |
||||
case res do |
||||
{:ok, result} -> |
||||
result |
||||
|
||||
{:exit, reason} -> |
||||
raise "Query fetching advanced filters terminated: #{inspect(reason)}" |
||||
|
||||
nil -> |
||||
raise "Query fetching advanced filters timed out." |
||||
end |
||||
end) |
||||
|> Enum.map(&to_advanced_filter/1) |
||||
|> Enum.sort(&sort_function/2) |
||||
|> take_page_size(paging_options) |
||||
end |
||||
|
||||
defp queries(options, paging_options) do |
||||
cond do |
||||
only_transactions?(options) -> |
||||
[transactions_query(paging_options, options), internal_transactions_query(paging_options, options)] |
||||
|
||||
only_token_transfers?(options) -> |
||||
[token_transfers_query(paging_options, options)] |
||||
|
||||
true -> |
||||
[ |
||||
transactions_query(paging_options, options), |
||||
internal_transactions_query(paging_options, options), |
||||
token_transfers_query(paging_options, options) |
||||
] |
||||
end |
||||
end |
||||
|
||||
defp only_transactions?(options) do |
||||
transaction_types = options[:tx_types] |
||||
tokens_to_include = options[:token_contract_address_hashes][:include] |
||||
|
||||
transaction_types == ["COIN_TRANSFER"] or tokens_to_include == ["native"] |
||||
end |
||||
|
||||
defp only_token_transfers?(options) do |
||||
transaction_types = options[:tx_types] |
||||
tokens_to_include = options[:token_contract_address_hashes][:include] |
||||
tokens_to_exclude = options[:token_contract_address_hashes][:exclude] |
||||
|
||||
(is_list(transaction_types) and length(transaction_types) > 0 and "COIN_TRANSFER" not in transaction_types) or |
||||
(is_list(tokens_to_include) and length(tokens_to_include) > 0 and "native" not in tokens_to_include) or |
||||
(is_list(tokens_to_exclude) and "native" in tokens_to_exclude) |
||||
end |
||||
|
||||
defp to_advanced_filter(%Transaction{} = transaction) do |
||||
%__MODULE__{ |
||||
hash: transaction.hash, |
||||
type: "coin_transfer", |
||||
input: transaction.input, |
||||
timestamp: transaction.block_timestamp, |
||||
from_address: transaction.from_address, |
||||
to_address: transaction.to_address, |
||||
value: transaction.value.value, |
||||
fee: transaction |> Transaction.fee(:wei) |> elem(1), |
||||
block_number: transaction.block_number, |
||||
transaction_index: transaction.index |
||||
} |
||||
end |
||||
|
||||
defp to_advanced_filter(%InternalTransaction{} = internal_transaction) do |
||||
%__MODULE__{ |
||||
hash: internal_transaction.transaction.hash, |
||||
type: "coin_transfer", |
||||
input: internal_transaction.input, |
||||
timestamp: internal_transaction.transaction.block_timestamp, |
||||
from_address: internal_transaction.from_address, |
||||
to_address: internal_transaction.to_address, |
||||
value: internal_transaction.value.value, |
||||
fee: |
||||
internal_transaction.transaction.gas_price && internal_transaction.gas_used && |
||||
Decimal.mult(internal_transaction.transaction.gas_price.value, internal_transaction.gas_used), |
||||
block_number: internal_transaction.transaction.block_number, |
||||
transaction_index: internal_transaction.transaction.index, |
||||
internal_transaction_index: internal_transaction.index |
||||
} |
||||
end |
||||
|
||||
defp to_advanced_filter(%TokenTransfer{} = token_transfer) do |
||||
%__MODULE__{ |
||||
hash: token_transfer.transaction.hash, |
||||
type: token_transfer.token_type, |
||||
input: token_transfer.transaction.input, |
||||
timestamp: token_transfer.transaction.block_timestamp, |
||||
from_address: token_transfer.from_address, |
||||
to_address: token_transfer.to_address, |
||||
fee: token_transfer.transaction |> Transaction.fee(:wei) |> elem(1), |
||||
token_transfer: %TokenTransfer{ |
||||
token_transfer |
||||
| amounts: [token_transfer.amount], |
||||
token_ids: token_transfer.token_id && [token_transfer.token_id] |
||||
}, |
||||
block_number: token_transfer.block_number, |
||||
transaction_index: token_transfer.transaction.index, |
||||
token_transfer_index: token_transfer.log_index, |
||||
token_transfer_batch_index: token_transfer.reverse_index_in_batch |
||||
} |
||||
end |
||||
|
||||
defp sort_function(a, b) do |
||||
case { |
||||
Helper.compare(a.block_number, b.block_number), |
||||
Helper.compare(a.transaction_index, b.transaction_index), |
||||
Helper.compare(a.token_transfer_index, b.token_transfer_index), |
||||
Helper.compare(a.token_transfer_batch_index, b.token_transfer_batch_index), |
||||
Helper.compare(a.internal_transaction_index, b.internal_transaction_index) |
||||
} do |
||||
{:lt, _, _, _, _} -> |
||||
false |
||||
|
||||
{:eq, :lt, _, _, _} -> |
||||
false |
||||
|
||||
{:eq, :eq, _, _, _} -> |
||||
case {a.token_transfer_index, a.token_transfer_batch_index, a.internal_transaction_index, |
||||
b.token_transfer_index, b.token_transfer_batch_index, b.internal_transaction_index} do |
||||
{nil, _, nil, _, _, _} -> |
||||
true |
||||
|
||||
{a_tt_index, a_tt_batch_index, nil, b_tt_index, b_tt_batch_index, _} when not is_nil(b_tt_index) -> |
||||
{a_tt_index, a_tt_batch_index} > {b_tt_index, b_tt_batch_index} |
||||
|
||||
{nil, _, a_it_index, _, _, b_it_index} -> |
||||
a_it_index > b_it_index |
||||
|
||||
{_, _, _, _, _, _} -> |
||||
false |
||||
end |
||||
|
||||
_ -> |
||||
true |
||||
end |
||||
end |
||||
|
||||
defp take_page_size(list, %PagingOptions{page_size: page_size}) when is_integer(page_size) do |
||||
Enum.take(list, page_size) |
||||
end |
||||
|
||||
defp take_page_size(list, _), do: list |
||||
|
||||
defp transactions_query(paging_options, options) do |
||||
query = |
||||
from(transaction in Transaction, |
||||
as: :transaction, |
||||
preload: [ |
||||
:block, |
||||
from_address: [:names, :smart_contract, :proxy_implementations], |
||||
to_address: [:names, :smart_contract, :proxy_implementations] |
||||
], |
||||
order_by: [ |
||||
desc: transaction.block_number, |
||||
desc: transaction.index |
||||
] |
||||
) |
||||
|
||||
query |
||||
|> page_transactions(paging_options) |
||||
|> limit_query(paging_options) |
||||
|> apply_transactions_filters(options) |
||||
end |
||||
|
||||
defp page_transactions(query, %PagingOptions{ |
||||
key: %{ |
||||
block_number: block_number, |
||||
transaction_index: tx_index |
||||
} |
||||
}) do |
||||
dynamic_condition = |
||||
dynamic(^page_block_number_dynamic(:transaction, block_number) or ^page_tx_index_dynamic(block_number, tx_index)) |
||||
|
||||
query |> where(^dynamic_condition) |
||||
end |
||||
|
||||
defp page_transactions(query, _), do: query |
||||
|
||||
defp internal_transactions_query(paging_options, options) do |
||||
query = |
||||
from(internal_transaction in InternalTransaction, |
||||
as: :internal_transaction, |
||||
join: transaction in assoc(internal_transaction, :transaction), |
||||
as: :transaction, |
||||
preload: [ |
||||
from_address: [:names, :smart_contract, :proxy_implementations], |
||||
to_address: [:names, :smart_contract, :proxy_implementations], |
||||
transaction: transaction |
||||
], |
||||
order_by: [ |
||||
desc: transaction.block_number, |
||||
desc: transaction.index, |
||||
desc: internal_transaction.index |
||||
] |
||||
) |
||||
|
||||
query |
||||
|> page_internal_transactions(paging_options) |
||||
|> limit_query(paging_options) |
||||
|> apply_transactions_filters(options) |
||||
end |
||||
|
||||
defp page_internal_transactions(query, %PagingOptions{ |
||||
key: %{ |
||||
block_number: block_number, |
||||
transaction_index: tx_index, |
||||
internal_transaction_index: nil |
||||
} |
||||
}) do |
||||
case {block_number, tx_index} do |
||||
{0, 0} -> |
||||
query |> where(as(:transaction).block_number == ^block_number and as(:transaction).index == ^tx_index) |
||||
|
||||
{0, tx_index} -> |
||||
query |
||||
|> where(as(:transaction).block_number == ^block_number and as(:transaction).index <= ^tx_index) |
||||
|
||||
{block_number, 0} -> |
||||
query |> where(as(:transaction).block_number < ^block_number) |
||||
|
||||
_ -> |
||||
query |
||||
|> where( |
||||
as(:transaction).block_number < ^block_number or |
||||
(as(:transaction).block_number == ^block_number and as(:transaction).index <= ^tx_index) |
||||
) |
||||
end |
||||
end |
||||
|
||||
defp page_internal_transactions(query, %PagingOptions{ |
||||
key: %{ |
||||
block_number: block_number, |
||||
transaction_index: tx_index, |
||||
internal_transaction_index: it_index |
||||
} |
||||
}) do |
||||
dynamic_condition = |
||||
dynamic( |
||||
^page_block_number_dynamic(:transaction, block_number) or ^page_tx_index_dynamic(block_number, tx_index) or |
||||
^page_it_index_dynamic(block_number, tx_index, it_index) |
||||
) |
||||
|
||||
query |
||||
|> where(^dynamic_condition) |
||||
end |
||||
|
||||
defp page_internal_transactions(query, _), do: query |
||||
|
||||
defp token_transfers_query(paging_options, options) do |
||||
token_transfer_query = |
||||
from(token_transfer in TokenTransfer, |
||||
as: :token_transfer, |
||||
join: transaction in assoc(token_transfer, :transaction), |
||||
as: :transaction, |
||||
join: token in assoc(token_transfer, :token), |
||||
as: :token, |
||||
select: %TokenTransfer{ |
||||
token_transfer |
||||
| token_id: fragment("UNNEST(?)", token_transfer.token_ids), |
||||
amount: |
||||
fragment("UNNEST(COALESCE(?, ARRAY[COALESCE(?, 1)]))", token_transfer.amounts, token_transfer.amount), |
||||
reverse_index_in_batch: |
||||
fragment("GENERATE_SERIES(COALESCE(ARRAY_LENGTH(?, 1), 1), 1, -1)", token_transfer.amounts), |
||||
token_decimals: token.decimals |
||||
}, |
||||
order_by: [ |
||||
desc: token_transfer.block_number, |
||||
desc: token_transfer.log_index |
||||
] |
||||
) |
||||
|
||||
token_transfer_query |
||||
|> apply_token_transfers_filters(options) |
||||
|> page_token_transfers(paging_options) |
||||
|> filter_token_transfers_by_amount(options[:amount][:from], options[:amount][:to]) |
||||
|> make_token_transfer_query_unnested() |
||||
|> limit_query(paging_options) |
||||
end |
||||
|
||||
defp page_token_transfers(query, %PagingOptions{ |
||||
key: %{ |
||||
block_number: block_number, |
||||
transaction_index: tx_index, |
||||
token_transfer_index: nil, |
||||
internal_transaction_index: nil |
||||
} |
||||
}) do |
||||
case {block_number, tx_index} do |
||||
{0, 0} -> |
||||
query |> where(as(:transaction).block_number == ^block_number and as(:transaction).index == ^tx_index) |
||||
|
||||
{0, tx_index} -> |
||||
query |
||||
|> where([token_transfer], token_transfer.block_number == ^block_number and as(:transaction).index < ^tx_index) |
||||
|
||||
{block_number, 0} -> |
||||
query |> where([token_transfer], token_transfer.block_number < ^block_number) |
||||
|
||||
{block_number, tx_index} -> |
||||
query |
||||
|> where( |
||||
[token_transfer], |
||||
token_transfer.block_number < ^block_number or |
||||
(token_transfer.block_number == ^block_number and as(:transaction).index <= ^tx_index) |
||||
) |
||||
end |
||||
end |
||||
|
||||
defp page_token_transfers(query, %PagingOptions{ |
||||
key: %{ |
||||
block_number: block_number, |
||||
transaction_index: tx_index, |
||||
token_transfer_index: nil |
||||
} |
||||
}) do |
||||
dynamic_condition = |
||||
dynamic( |
||||
^page_block_number_dynamic(:token_transfer, block_number) or ^page_tx_index_dynamic(block_number, tx_index) |
||||
) |
||||
|
||||
query |> where(^dynamic_condition) |
||||
end |
||||
|
||||
defp page_token_transfers(query, %PagingOptions{ |
||||
key: %{ |
||||
block_number: block_number, |
||||
token_transfer_index: tt_index, |
||||
token_transfer_batch_index: tt_batch_index |
||||
} |
||||
}) do |
||||
dynamic_condition = |
||||
dynamic( |
||||
^page_block_number_dynamic(:token_transfer, block_number) or |
||||
^page_tt_index_dynamic(:token_transfer, block_number, tt_index, tt_batch_index) |
||||
) |
||||
|
||||
paged_query = query |> where(^dynamic_condition) |
||||
|
||||
paged_query |
||||
|> make_token_transfer_query_unnested() |
||||
|> where( |
||||
^page_tt_batch_index_dynamic( |
||||
block_number, |
||||
tt_index, |
||||
tt_batch_index |
||||
) |
||||
) |
||||
end |
||||
|
||||
defp page_token_transfers(query, _), do: query |
||||
|
||||
defp page_block_number_dynamic(binding, block_number) when block_number > 0 do |
||||
dynamic(as(^binding).block_number < ^block_number) |
||||
end |
||||
|
||||
defp page_block_number_dynamic(_, _) do |
||||
dynamic(false) |
||||
end |
||||
|
||||
defp page_tx_index_dynamic(block_number, tx_index) when tx_index > 0 do |
||||
dynamic([transaction: tx], tx.block_number == ^block_number and tx.index < ^tx_index) |
||||
end |
||||
|
||||
defp page_tx_index_dynamic(_, _) do |
||||
dynamic(false) |
||||
end |
||||
|
||||
defp page_it_index_dynamic(block_number, tx_index, it_index) when it_index > 0 do |
||||
dynamic( |
||||
[transaction: tx, internal_transaction: it], |
||||
tx.block_number == ^block_number and tx.index == ^tx_index and |
||||
it.index < ^it_index |
||||
) |
||||
end |
||||
|
||||
defp page_it_index_dynamic(_, _, _) do |
||||
dynamic(false) |
||||
end |
||||
|
||||
defp page_tt_index_dynamic(binding, block_number, tt_index, tt_batch_index) |
||||
when tt_index > 0 and tt_batch_index > 1 do |
||||
dynamic(as(^binding).block_number == ^block_number and as(^binding).log_index <= ^tt_index) |
||||
end |
||||
|
||||
defp page_tt_index_dynamic(binding, block_number, tt_index, _tt_batch_index) when tt_index > 0 do |
||||
dynamic(as(^binding).block_number == ^block_number and as(^binding).log_index < ^tt_index) |
||||
end |
||||
|
||||
defp page_tt_index_dynamic(_, _, _, _) do |
||||
dynamic(false) |
||||
end |
||||
|
||||
defp page_tt_batch_index_dynamic(block_number, tt_index, tt_batch_index) when tt_batch_index > 1 do |
||||
dynamic( |
||||
[unnested_token_transfer: tt], |
||||
^page_block_number_dynamic(:unnested_token_transfer, block_number) or |
||||
^page_tt_index_dynamic( |
||||
:unnested_token_transfer, |
||||
block_number, |
||||
tt_index, |
||||
0 |
||||
) or |
||||
(tt.block_number == ^block_number and tt.log_index == ^tt_index and tt.reverse_index_in_batch < ^tt_batch_index) |
||||
) |
||||
end |
||||
|
||||
defp page_tt_batch_index_dynamic(_, _, _) do |
||||
dynamic(true) |
||||
end |
||||
|
||||
defp limit_query(query, %PagingOptions{page_size: limit}) when is_integer(limit), do: limit(query, ^limit) |
||||
|
||||
defp limit_query(query, _), do: query |
||||
|
||||
defp apply_token_transfers_filters(query, options) do |
||||
query |
||||
|> filter_by_tx_type(options[:tx_types]) |
||||
|> filter_token_transfers_by_methods(options[:methods]) |
||||
|> filter_by_token(options[:token_contract_address_hashes][:include], :include) |
||||
|> filter_by_token(options[:token_contract_address_hashes][:exclude], :exclude) |
||||
|> apply_common_filters(options) |
||||
end |
||||
|
||||
defp apply_transactions_filters(query, options) do |
||||
query |
||||
|> filter_transactions_by_amount(options[:amount][:from], options[:amount][:to]) |
||||
|> filter_transactions_by_methods(options[:methods]) |
||||
|> apply_common_filters(options) |
||||
end |
||||
|
||||
defp apply_common_filters(query, options) do |
||||
query |
||||
|> only_collated_transactions() |
||||
|> filter_by_timestamp(options[:age][:from], options[:age][:to]) |
||||
|> filter_by_addresses(options[:from_address_hashes], options[:to_address_hashes], options[:address_relation]) |
||||
end |
||||
|
||||
defp only_collated_transactions(query) do |
||||
query |> where(not is_nil(as(:transaction).block_number) and not is_nil(as(:transaction).index)) |
||||
end |
||||
|
||||
defp filter_by_tx_type(query, [_ | _] = tx_types) do |
||||
query |> where([token_transfer], token_transfer.token_type in ^tx_types) |
||||
end |
||||
|
||||
defp filter_by_tx_type(query, _), do: query |
||||
|
||||
defp filter_transactions_by_methods(query, [_ | _] = methods) do |
||||
prepared_methods = prepare_methods(methods) |
||||
|
||||
query |> where([t], fragment("substring(? FOR 4)", t.input) in ^prepared_methods) |
||||
end |
||||
|
||||
defp filter_transactions_by_methods(query, _), do: query |
||||
|
||||
defp filter_token_transfers_by_methods(query, [_ | _] = methods) do |
||||
prepared_methods = prepare_methods(methods) |
||||
|
||||
query |> where(fragment("substring(? FOR 4)", as(:transaction).input) in ^prepared_methods) |
||||
end |
||||
|
||||
defp filter_token_transfers_by_methods(query, _), do: query |
||||
|
||||
defp prepare_methods(methods) do |
||||
methods |
||||
|> Enum.flat_map(fn |
||||
method -> |
||||
case Data.cast(method) do |
||||
{:ok, method} -> [method.bytes] |
||||
_ -> [] |
||||
end |
||||
end) |
||||
end |
||||
|
||||
defp filter_by_timestamp(query, %DateTime{} = from, %DateTime{} = to) do |
||||
query |> where(as(:transaction).block_timestamp >= ^from and as(:transaction).block_timestamp <= ^to) |
||||
end |
||||
|
||||
defp filter_by_timestamp(query, %DateTime{} = from, _to) do |
||||
query |> where(as(:transaction).block_timestamp >= ^from) |
||||
end |
||||
|
||||
defp filter_by_timestamp(query, _from, %DateTime{} = to) do |
||||
query |> where(as(:transaction).block_timestamp <= ^to) |
||||
end |
||||
|
||||
defp filter_by_timestamp(query, _, _), do: query |
||||
|
||||
defp filter_by_addresses(query, from_addresses, to_addresses, relation) do |
||||
to_address_dynamic = do_filter_by_addresses(:to_address_hash, to_addresses) |
||||
|
||||
from_address_dynamic = do_filter_by_addresses(:from_address_hash, from_addresses) |
||||
|
||||
final_condition = |
||||
case {to_address_dynamic, from_address_dynamic} do |
||||
{not_nil_to_address, not_nil_from_address} when nil not in [not_nil_to_address, not_nil_from_address] -> |
||||
combine_filter_by_addresses(not_nil_to_address, not_nil_from_address, relation) |
||||
|
||||
_ -> |
||||
to_address_dynamic || from_address_dynamic |
||||
end |
||||
|
||||
case final_condition do |
||||
not_nil when not is_nil(not_nil) -> query |> where(^not_nil) |
||||
_ -> query |
||||
end |
||||
end |
||||
|
||||
defp do_filter_by_addresses(field, addresses) do |
||||
to_include_dynamic = do_filter_by_addresses_inclusion(field, addresses && Keyword.get(addresses, :include)) |
||||
to_exclude_dynamic = do_filter_by_addresses_exclusion(field, addresses && Keyword.get(addresses, :exclude)) |
||||
|
||||
case {to_include_dynamic, to_exclude_dynamic} do |
||||
{not_nil_include, not_nil_exclude} when nil not in [not_nil_include, not_nil_exclude] -> |
||||
dynamic([t], ^not_nil_include and ^not_nil_exclude) |
||||
|
||||
_ -> |
||||
to_include_dynamic || to_exclude_dynamic |
||||
end |
||||
end |
||||
|
||||
defp do_filter_by_addresses_inclusion(field, [_ | _] = addresses) do |
||||
dynamic([t], field(t, ^field) in ^addresses) |
||||
end |
||||
|
||||
defp do_filter_by_addresses_inclusion(_, _), do: nil |
||||
|
||||
defp do_filter_by_addresses_exclusion(field, [_ | _] = addresses) do |
||||
dynamic([t], field(t, ^field) not in ^addresses) |
||||
end |
||||
|
||||
defp do_filter_by_addresses_exclusion(_, _), do: nil |
||||
|
||||
defp combine_filter_by_addresses(from_addresses_dynamic, to_addresses_dynamic, :or) do |
||||
dynamic([t], ^from_addresses_dynamic or ^to_addresses_dynamic) |
||||
end |
||||
|
||||
defp combine_filter_by_addresses(from_addresses_dynamic, to_addresses_dynamic, _) do |
||||
dynamic([t], ^from_addresses_dynamic and ^to_addresses_dynamic) |
||||
end |
||||
|
||||
@eth_decimals 1000_000_000_000_000_000 |
||||
|
||||
defp filter_transactions_by_amount(query, from, to) when not is_nil(from) and not is_nil(to) and from < to do |
||||
query |> where([t], t.value / @eth_decimals >= ^from and t.value / @eth_decimals <= ^to) |
||||
end |
||||
|
||||
defp filter_transactions_by_amount(query, _from, to) when not is_nil(to) do |
||||
query |> where([t], t.value / @eth_decimals <= ^to) |
||||
end |
||||
|
||||
defp filter_transactions_by_amount(query, from, _to) when not is_nil(from) do |
||||
query |> where([t], t.value / @eth_decimals >= ^from) |
||||
end |
||||
|
||||
defp filter_transactions_by_amount(query, _, _), do: query |
||||
|
||||
defp filter_token_transfers_by_amount(query, from, to) when not is_nil(from) and not is_nil(to) and from < to do |
||||
unnested_query = make_token_transfer_query_unnested(query) |
||||
|
||||
unnested_query |
||||
|> where( |
||||
[unnested_token_transfer: tt], |
||||
tt.amount / fragment("10 ^ COALESCE(?, 0)", tt.token_decimals) >= ^from and |
||||
tt.amount / fragment("10 ^ COALESCE(?, 0)", tt.token_decimals) <= ^to |
||||
) |
||||
end |
||||
|
||||
defp filter_token_transfers_by_amount(query, _from, to) when not is_nil(to) do |
||||
unnested_query = make_token_transfer_query_unnested(query) |
||||
|
||||
unnested_query |
||||
|> where( |
||||
[unnested_token_transfer: tt], |
||||
tt.amount / fragment("10 ^ COALESCE(?, 0)", tt.token_decimals) <= ^to |
||||
) |
||||
end |
||||
|
||||
defp filter_token_transfers_by_amount(query, from, _to) when not is_nil(from) do |
||||
unnested_query = make_token_transfer_query_unnested(query) |
||||
|
||||
unnested_query |
||||
|> where( |
||||
[unnested_token_transfer: tt], |
||||
tt.amount / fragment("10 ^ COALESCE(?, 0)", tt.token_decimals) >= ^from |
||||
) |
||||
end |
||||
|
||||
defp filter_token_transfers_by_amount(query, _, _), do: query |
||||
|
||||
defp make_token_transfer_query_unnested(query) do |
||||
if has_named_binding?(query, :unnested_token_transfer) do |
||||
query |
||||
else |
||||
from(token_transfer in subquery(query), |
||||
as: :unnested_token_transfer, |
||||
preload: [ |
||||
:transaction, |
||||
:token, |
||||
from_address: [:names, :smart_contract, :proxy_implementations], |
||||
to_address: [:names, :smart_contract, :proxy_implementations] |
||||
], |
||||
select_merge: %{ |
||||
token_ids: [token_transfer.token_id], |
||||
amounts: [token_transfer.amount] |
||||
} |
||||
) |
||||
end |
||||
end |
||||
|
||||
defp filter_by_token(query, [_ | _] = token_contract_address_hashes, :include) do |
||||
filtered = token_contract_address_hashes |> Enum.reject(&(&1 == "native")) |
||||
query |> where([token_transfer], token_transfer.token_contract_address_hash in ^filtered) |
||||
end |
||||
|
||||
defp filter_by_token(query, [_ | _] = token_contract_address_hashes, :exclude) do |
||||
filtered = token_contract_address_hashes |> Enum.reject(&(&1 == "native")) |
||||
query |> where([token_transfer], token_transfer.token_contract_address_hash not in ^filtered) |
||||
end |
||||
|
||||
defp filter_by_token(query, _, _), do: query |
||||
end |
Loading…
Reference in new issue