Fix pagination; Add same token_id squashing; Add tests

pull/6990/head
Nikita Pozdniakov 2 years ago
parent 8c67142103
commit 876aa3782e
No known key found for this signature in database
GPG Key ID: F344106F9804FE5F
  1. 8
      apps/block_scout_web/lib/block_scout_web/chain.ex
  2. 6
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex
  3. 6
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex
  4. 4
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex
  5. 239
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs
  6. 314
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs
  7. 232
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs
  8. 16
      apps/explorer/lib/explorer/chain.ex
  9. 4
      apps/explorer/lib/explorer/chain/token_transfer.ex

@ -477,9 +477,9 @@ defmodule BlockScoutWeb.Chain do
Map.merge(params, paging_params(List.last(list)))
end
def token_tranfers_next_page_params([], _list, _params), do: nil
def token_transfers_next_page_params([], _list, _params), do: nil
def token_tranfers_next_page_params(next_page, list, params) do
def token_transfers_next_page_params(next_page, list, params) do
next_token_transfer = List.first(next_page)
current_token_transfer = List.last(list)
@ -488,7 +488,7 @@ defmodule BlockScoutWeb.Chain do
next_token_transfer.transaction_hash == current_token_transfer.transaction_hash do
new_params =
list
|> last_token_trasfer_before_current(current_token_transfer)
|> last_token_transfer_before_current(current_token_transfer)
|> (&if(is_nil(&1), do: %{}, else: paging_params(&1))).()
params
@ -504,7 +504,7 @@ defmodule BlockScoutWeb.Chain do
end
end
defp last_token_trasfer_before_current(list, current_token_transfer) do
defp last_token_transfer_before_current(list, current_token_transfer) do
Enum.reduce_while(list, nil, fn tt, acc ->
if tt.log_index == current_token_transfer.log_index and tt.block_hash == current_token_transfer.block_hash and
tt.transaction_hash == current_token_transfer.transaction_hash do

@ -4,7 +4,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do
import BlockScoutWeb.Chain,
only: [
next_page_params: 3,
token_tranfers_next_page_params: 3,
token_transfers_next_page_params: 3,
paging_options: 1,
split_list_by_page: 1,
current_filter: 1
@ -154,7 +154,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do
next_page_params =
next_page
|> token_tranfers_next_page_params(token_transfers, params)
|> token_transfers_next_page_params(token_transfers, params)
|> delete_parameters_from_next_page_params()
conn
@ -186,7 +186,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do
next_page_params =
next_page
|> token_tranfers_next_page_params(token_transfers, params)
|> token_transfers_next_page_params(token_transfers, params)
|> delete_parameters_from_next_page_params()
conn

@ -11,7 +11,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do
split_list_by_page: 1,
paging_options: 1,
next_page_params: 3,
token_tranfers_next_page_params: 3,
token_transfers_next_page_params: 3,
unique_tokens_paging_options: 1,
unique_tokens_next_page: 3
]
@ -59,7 +59,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do
next_page_params =
next_page
|> token_tranfers_next_page_params(token_transfers, params)
|> token_transfers_next_page_params(token_transfers, params)
|> delete_parameters_from_next_page_params()
conn
@ -135,7 +135,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do
next_page_params =
next_page
|> token_tranfers_next_page_params(token_transfers, params)
|> token_transfers_next_page_params(token_transfers, params)
|> delete_parameters_from_next_page_params()
conn

@ -2,7 +2,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
use BlockScoutWeb, :controller
import BlockScoutWeb.Chain,
only: [next_page_params: 3, token_tranfers_next_page_params: 3, paging_options: 1, split_list_by_page: 1]
only: [next_page_params: 3, token_transfers_next_page_params: 3, paging_options: 1, split_list_by_page: 1]
import BlockScoutWeb.PagingHelper,
only: [
@ -153,7 +153,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
next_page_params =
next_page
|> token_tranfers_next_page_params(token_transfers, params)
|> token_transfers_next_page_params(token_transfers, params)
|> delete_parameters_from_next_page_params()
conn

@ -838,6 +838,228 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
check_paginated_response(response, response_2nd_page, erc_20_tt)
end
test "check that same token_ids within batch squashes", %{conn: conn} do
address = insert(:address)
token = insert(:token, type: "ERC-1155")
id = 0
insert(:token_instance, token_id: id, token_contract_address_hash: token.contract_address_hash)
tt =
for _ <- 0..50 do
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
insert(:token_transfer,
to_address: address,
transaction: tx,
block: tx.block,
block_number: tx.block_number,
token_contract_address: token.contract_address,
token_ids: Enum.map(0..50, fn _x -> id end),
amounts: Enum.map(0..50, fn x -> x end)
)
end
token_transfers =
for i <- tt do
%TokenTransfer{i | token_ids: [id], amount: Decimal.new(1275)}
end
request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers")
assert response = json_response(request, 200)
request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", response["next_page_params"])
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, token_transfers)
end
test "check that pagination works for 721 tokens", %{conn: conn} do
address = insert(:address)
token = insert(:token, type: "ERC-721")
token_transfers =
for i <- 0..50 do
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
insert(:token_transfer,
transaction: tx,
to_address: address,
block: tx.block,
block_number: tx.block_number,
token_contract_address: token.contract_address,
token_ids: [i]
)
end
request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers")
assert response = json_response(request, 200)
request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", response["next_page_params"])
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, token_transfers)
end
test "check that pagination works fine with 1155 batches #1 (large batch) + check filters", %{conn: conn} do
address = insert(:address)
token = insert(:token, type: "ERC-1155")
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
tt =
insert(:token_transfer,
transaction: tx,
to_address: address,
block: tx.block,
block_number: tx.block_number,
token_contract_address: token.contract_address,
token_ids: Enum.map(0..50, fn x -> x end),
amounts: Enum.map(0..50, fn x -> x end)
)
token_transfers =
for i <- 0..50 do
%TokenTransfer{tt | token_ids: [i], amount: i}
end
filter = %{"type" => "ERC-1155", "filter" => "to"}
request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter)
assert response = json_response(request, 200)
request_2nd_page =
get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter))
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, token_transfers)
filter = %{"type" => "ERC-1155", "filter" => "from"}
request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter)
assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200)
end
test "check that pagination works fine with 1155 batches #2 some batches on the first page and one on the second",
%{conn: conn} do
address = insert(:address)
token = insert(:token, type: "ERC-1155")
tx_1 = insert(:transaction, input: "0xabcd010203040506") |> with_block()
tt_1 =
insert(:token_transfer,
transaction: tx_1,
to_address: address,
block: tx_1.block,
block_number: tx_1.block_number,
token_contract_address: token.contract_address,
token_ids: Enum.map(0..24, fn x -> x end),
amounts: Enum.map(0..24, fn x -> x end)
)
token_transfers_1 =
for i <- 0..24 do
%TokenTransfer{tt_1 | token_ids: [i], amount: i}
end
tx_2 = insert(:transaction, input: "0xabcd010203040506") |> with_block()
tt_2 =
insert(:token_transfer,
transaction: tx_2,
to_address: address,
block: tx_2.block,
block_number: tx_2.block_number,
token_contract_address: token.contract_address,
token_ids: Enum.map(25..49, fn x -> x end),
amounts: Enum.map(25..49, fn x -> x end)
)
token_transfers_2 =
for i <- 25..49 do
%TokenTransfer{tt_2 | token_ids: [i], amount: i}
end
tt_3 =
insert(:token_transfer,
transaction: tx_2,
from_address: address,
block: tx_2.block,
block_number: tx_2.block_number,
token_contract_address: token.contract_address,
token_ids: [50],
amounts: [50]
)
request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers")
assert response = json_response(request, 200)
request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", response["next_page_params"])
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, token_transfers_1 ++ token_transfers_2 ++ [tt_3])
end
test "check that pagination works fine with 1155 batches #3", %{conn: conn} do
address = insert(:address)
token = insert(:token, type: "ERC-1155")
tx_1 = insert(:transaction, input: "0xabcd010203040506") |> with_block()
tt_1 =
insert(:token_transfer,
transaction: tx_1,
from_address: address,
block: tx_1.block,
block_number: tx_1.block_number,
token_contract_address: token.contract_address,
token_ids: Enum.map(0..24, fn x -> x end),
amounts: Enum.map(0..24, fn x -> x end)
)
token_transfers_1 =
for i <- 0..24 do
%TokenTransfer{tt_1 | token_ids: [i], amount: i}
end
tx_2 = insert(:transaction, input: "0xabcd010203040506") |> with_block()
tt_2 =
insert(:token_transfer,
transaction: tx_2,
to_address: address,
block: tx_2.block,
block_number: tx_2.block_number,
token_contract_address: token.contract_address,
token_ids: Enum.map(25..50, fn x -> x end),
amounts: Enum.map(25..50, fn x -> x end)
)
token_transfers_2 =
for i <- 25..50 do
%TokenTransfer{tt_2 | token_ids: [i], amount: i}
end
request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers")
assert response = json_response(request, 200)
request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", response["next_page_params"])
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, token_transfers_1 ++ token_transfers_2)
end
end
describe "/addresses/{address_hash}/internal-transactions" do
@ -1394,6 +1616,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
assert json["method"] != nil
assert to_string(token_transfer.block_hash) == json["block_hash"]
assert to_string(token_transfer.log_index) == json["log_index"]
assert check_total(Repo.preload(token_transfer, [{:token, :contract_address}]).token, json["total"], token_transfer)
end
defp compare_item(%InternalTransaction{} = internal_tx, json) do
@ -1451,4 +1674,20 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
assert second_page_resp["next_page_params"] == nil
compare_item(Enum.at(list, 0), Enum.at(second_page_resp["items"], 0))
end
def check_total(%Token{type: nft}, json, token_transfer) when nft in ["ERC-1155"] do
json["token_id"] in Enum.map(token_transfer.token_ids, fn x -> to_string(x) end) and
json["value"] == to_string(token_transfer.amount)
end
def check_total(%Token{type: nft}, json, token_transfer) when nft in ["ERC-721"] do
json["token_id"] in Enum.map(token_transfer.token_ids, fn x -> to_string(x) end)
end
# with the current implementation no transfers should come with list in totals
def check_total(%Token{type: nft}, json, _token_transfer) when nft in ["ERC-721", "ERC-1155"] and is_list(json) do
false
end
def check_total(_, _, _), do: true
end

@ -142,6 +142,207 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do
check_paginated_response(response, response_2nd_page, token_transfers)
end
test "check that same token_ids within batch squashes", %{conn: conn} do
token = insert(:token, type: "ERC-1155")
id = 0
insert(:token_instance, token_id: id, token_contract_address_hash: token.contract_address_hash)
tt =
for _ <- 0..50 do
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
insert(:token_transfer,
transaction: tx,
block: tx.block,
block_number: tx.block_number,
token_contract_address: token.contract_address,
token_ids: Enum.map(0..50, fn _x -> id end),
amounts: Enum.map(0..50, fn x -> x end)
)
end
token_transfers =
for i <- tt do
%TokenTransfer{i | token_ids: [id], amount: Decimal.new(1275)}
end
request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers")
assert response = json_response(request, 200)
request_2nd_page =
get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers", response["next_page_params"])
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, token_transfers)
end
test "check that pagination works for 721 tokens", %{conn: conn} do
token = insert(:token, type: "ERC-721")
token_transfers =
for i <- 0..50 do
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
insert(:token_transfer,
transaction: tx,
block: tx.block,
block_number: tx.block_number,
token_contract_address: token.contract_address,
token_ids: [i]
)
end
request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers")
assert response = json_response(request, 200)
request_2nd_page =
get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers", response["next_page_params"])
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, token_transfers)
end
test "check that pagination works fine with 1155 batches #1 (large batch)", %{conn: conn} do
token = insert(:token, type: "ERC-1155")
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
tt =
insert(:token_transfer,
transaction: tx,
block: tx.block,
block_number: tx.block_number,
token_contract_address: token.contract_address,
token_ids: Enum.map(0..50, fn x -> x end),
amounts: Enum.map(0..50, fn x -> x end)
)
token_transfers =
for i <- 0..50 do
%TokenTransfer{tt | token_ids: [i], amount: i}
end
request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers")
assert response = json_response(request, 200)
request_2nd_page =
get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers", response["next_page_params"])
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, token_transfers)
end
test "check that pagination works fine with 1155 batches #2 some batches on the first page and one on the second",
%{conn: conn} do
token = insert(:token, type: "ERC-1155")
tx_1 = insert(:transaction, input: "0xabcd010203040506") |> with_block()
tt_1 =
insert(:token_transfer,
transaction: tx_1,
block: tx_1.block,
block_number: tx_1.block_number,
token_contract_address: token.contract_address,
token_ids: Enum.map(0..24, fn x -> x end),
amounts: Enum.map(0..24, fn x -> x end)
)
token_transfers_1 =
for i <- 0..24 do
%TokenTransfer{tt_1 | token_ids: [i], amount: i}
end
tx_2 = insert(:transaction, input: "0xabcd010203040506") |> with_block()
tt_2 =
insert(:token_transfer,
transaction: tx_2,
block: tx_2.block,
block_number: tx_2.block_number,
token_contract_address: token.contract_address,
token_ids: Enum.map(25..49, fn x -> x end),
amounts: Enum.map(25..49, fn x -> x end)
)
token_transfers_2 =
for i <- 25..49 do
%TokenTransfer{tt_2 | token_ids: [i], amount: i}
end
tt_3 =
insert(:token_transfer,
transaction: tx_2,
block: tx_2.block,
block_number: tx_2.block_number,
token_contract_address: token.contract_address,
token_ids: [50],
amounts: [50]
)
request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers")
assert response = json_response(request, 200)
request_2nd_page =
get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers", response["next_page_params"])
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, token_transfers_1 ++ token_transfers_2 ++ [tt_3])
end
test "check that pagination works fine with 1155 batches #3", %{conn: conn} do
token = insert(:token, type: "ERC-1155")
tx_1 = insert(:transaction, input: "0xabcd010203040506") |> with_block()
tt_1 =
insert(:token_transfer,
transaction: tx_1,
block: tx_1.block,
block_number: tx_1.block_number,
token_contract_address: token.contract_address,
token_ids: Enum.map(0..24, fn x -> x end),
amounts: Enum.map(0..24, fn x -> x end)
)
token_transfers_1 =
for i <- 0..24 do
%TokenTransfer{tt_1 | token_ids: [i], amount: i}
end
tx_2 = insert(:transaction, input: "0xabcd010203040506") |> with_block()
tt_2 =
insert(:token_transfer,
transaction: tx_2,
block: tx_2.block,
block_number: tx_2.block_number,
token_contract_address: token.contract_address,
token_ids: Enum.map(25..50, fn x -> x end),
amounts: Enum.map(25..50, fn x -> x end)
)
token_transfers_2 =
for i <- 25..50 do
%TokenTransfer{tt_2 | token_ids: [i], amount: i}
end
request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers")
assert response = json_response(request, 200)
request_2nd_page =
get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers", response["next_page_params"])
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, token_transfers_1 ++ token_transfers_2)
end
end
describe "/tokens/{address_hash}/holders" do
@ -393,6 +594,107 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, transfers_0 ++ transfers_1)
end
test "check that pagination works for 721 tokens", %{conn: conn} do
token = insert(:token, type: "ERC-721")
id = 0
insert(:token_instance, token_id: id, token_contract_address_hash: token.contract_address_hash)
token_transfers =
for _i <- 0..50 do
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
insert(:token_transfer,
transaction: tx,
block: tx.block,
block_number: tx.block_number,
token_contract_address: token.contract_address,
token_ids: [id]
)
end
request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{id}/transfers")
assert response = json_response(request, 200)
request_2nd_page =
get(
conn,
"/api/v2/tokens/#{token.contract_address.hash}/instances/#{id}/transfers",
response["next_page_params"]
)
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, token_transfers)
end
test "check that same token_ids within batch squashes", %{conn: conn} do
token = insert(:token, type: "ERC-1155")
id = 0
insert(:token_instance, token_id: id, token_contract_address_hash: token.contract_address_hash)
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
tt =
insert(:token_transfer,
transaction: tx,
block: tx.block,
block_number: tx.block_number,
token_contract_address: token.contract_address,
token_ids: Enum.map(0..50, fn _x -> id end),
amounts: Enum.map(0..50, fn x -> x end)
)
token_transfer = %TokenTransfer{tt | token_ids: [id], amount: Decimal.new(1275)}
request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{id}/transfers")
assert %{"next_page_params" => nil, "items" => [item]} = json_response(request, 200)
compare_item(token_transfer, item)
end
test "check that pagination works fine with 1155 batches #1 (51 batch with twice repeated id. Repeated id squashed into one element)",
%{conn: conn} do
token = insert(:token, type: "ERC-1155")
id = 0
amount = 101
insert(:token_instance, token_id: id, token_contract_address_hash: token.contract_address_hash)
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
tt =
for _ <- 0..50 do
insert(:token_transfer,
transaction: tx,
block: tx.block,
block_number: tx.block_number,
token_contract_address: token.contract_address,
token_ids: Enum.map(0..50, fn x -> x end) ++ [id],
amounts: Enum.map(1..51, fn x -> x end) ++ [amount]
)
end
token_transfers =
for i <- tt do
%TokenTransfer{i | token_ids: [id], amount: amount + 1}
end
request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{id}/transfers")
assert response = json_response(request, 200)
request_2nd_page =
get(
conn,
"/api/v2/tokens/#{token.contract_address.hash}/instances/#{id}/transfers",
response["next_page_params"]
)
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, token_transfers)
end
end
describe "/tokens/{address_hash}/instances/{token_id}/transfers-count" do
@ -491,10 +793,20 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do
compare_item(Repo.preload(instance, [{:token, :contract_address}]).token, json["token"])
end
def check_total(%Token{type: nft}, json, token_transfer) when nft in ["ERC-721", "ERC-1155"] do
def check_total(%Token{type: nft}, json, token_transfer) when nft in ["ERC-1155"] do
json["token_id"] in Enum.map(token_transfer.token_ids, fn x -> to_string(x) end) and
json["value"] == to_string(token_transfer.amount)
end
def check_total(%Token{type: nft}, json, token_transfer) when nft in ["ERC-721"] do
json["token_id"] in Enum.map(token_transfer.token_ids, fn x -> to_string(x) end)
end
# with the current implementation no transfers should come with list in totals
def check_total(%Token{type: nft}, json, _token_transfer) when nft in ["ERC-721", "ERC-1155"] and is_list(json) do
false
end
def check_total(_, _, _), do: true
defp check_paginated_response(first_page_resp, second_page_resp, list) do

@ -4,7 +4,8 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do
import EthereumJSONRPC, only: [integer_to_quantity: 1]
import Mox
alias Explorer.Chain.{Address, InternalTransaction, Log, TokenTransfer, Transaction}
alias Explorer.Chain.{Address, InternalTransaction, Log, Token, TokenTransfer, Transaction}
alias Explorer.Repo
setup do
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.TransactionsApiV2.child_id())
@ -535,6 +536,216 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do
compare_item(Enum.at(erc_20_tt, 1), Enum.at(response_3rd_page["items"], 0))
compare_item(Enum.at(erc_20_tt, 0), Enum.at(response_3rd_page["items"], 1))
end
test "check that same token_ids within batch squashes", %{conn: conn} do
token = insert(:token, type: "ERC-1155")
id = 0
insert(:token_instance, token_id: id, token_contract_address_hash: token.contract_address_hash)
tx =
:transaction
|> insert()
|> with_block()
tt =
for _ <- 0..50 do
insert(:token_transfer,
transaction: tx,
block: tx.block,
block_number: tx.block_number,
token_contract_address: token.contract_address,
token_ids: Enum.map(0..50, fn _x -> id end),
amounts: Enum.map(0..50, fn x -> x end)
)
end
token_transfers =
for i <- tt do
%TokenTransfer{i | token_ids: [id], amount: Decimal.new(1275)}
end
request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers")
assert response = json_response(request, 200)
request_2nd_page =
get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers", response["next_page_params"])
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, Enum.reverse(token_transfers))
end
test "check that pagination works for 721 tokens", %{conn: conn} do
token = insert(:token, type: "ERC-721")
tx =
:transaction
|> insert()
|> with_block()
token_transfers =
for i <- 0..50 do
insert(:token_transfer,
transaction: tx,
block: tx.block,
block_number: tx.block_number,
token_contract_address: token.contract_address,
token_ids: [i]
)
end
request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers")
assert response = json_response(request, 200)
request_2nd_page =
get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers", response["next_page_params"])
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, Enum.reverse(token_transfers))
end
test "check that pagination works fine with 1155 batches #1 (large batch)", %{conn: conn} do
token = insert(:token, type: "ERC-1155")
tx =
:transaction
|> insert()
|> with_block()
tt =
insert(:token_transfer,
transaction: tx,
block: tx.block,
block_number: tx.block_number,
token_contract_address: token.contract_address,
token_ids: Enum.map(0..50, fn x -> x end),
amounts: Enum.map(0..50, fn x -> x end)
)
token_transfers =
for i <- 0..50 do
%TokenTransfer{tt | token_ids: [i], amount: i}
end
request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers")
assert response = json_response(request, 200)
request_2nd_page =
get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers", response["next_page_params"])
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, token_transfers)
end
test "check that pagination works fine with 1155 batches #2 some batches on the first page and one on the second",
%{conn: conn} do
token = insert(:token, type: "ERC-1155")
tx =
:transaction
|> insert()
|> with_block()
tt_1 =
insert(:token_transfer,
transaction: tx,
block: tx.block,
block_number: tx.block_number,
token_contract_address: token.contract_address,
token_ids: Enum.map(0..24, fn x -> x end),
amounts: Enum.map(0..24, fn x -> x end)
)
token_transfers_1 =
for i <- 0..24 do
%TokenTransfer{tt_1 | token_ids: [i], amount: i}
end
tt_2 =
insert(:token_transfer,
transaction: tx,
block: tx.block,
block_number: tx.block_number,
token_contract_address: token.contract_address,
token_ids: Enum.map(25..49, fn x -> x end),
amounts: Enum.map(25..49, fn x -> x end)
)
token_transfers_2 =
for i <- 25..49 do
%TokenTransfer{tt_2 | token_ids: [i], amount: i}
end
tt_3 =
insert(:token_transfer,
transaction: tx,
block: tx.block,
block_number: tx.block_number,
token_contract_address: token.contract_address,
token_ids: [50],
amounts: [50]
)
request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers")
assert response = json_response(request, 200)
request_2nd_page =
get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers", response["next_page_params"])
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, [tt_3] ++ token_transfers_2 ++ token_transfers_1)
end
test "check that pagination works fine with 1155 batches #3", %{conn: conn} do
token = insert(:token, type: "ERC-1155")
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
tt_1 =
insert(:token_transfer,
transaction: tx,
block: tx.block,
block_number: tx.block_number,
token_contract_address: token.contract_address,
token_ids: Enum.map(0..24, fn x -> x end),
amounts: Enum.map(0..24, fn x -> x end)
)
token_transfers_1 =
for i <- 0..24 do
%TokenTransfer{tt_1 | token_ids: [i], amount: i}
end
tt_2 =
insert(:token_transfer,
transaction: tx,
block: tx.block,
block_number: tx.block_number,
token_contract_address: token.contract_address,
token_ids: Enum.map(25..50, fn x -> x end),
amounts: Enum.map(25..50, fn x -> x end)
)
token_transfers_2 =
for i <- 25..50 do
%TokenTransfer{tt_2 | token_ids: [i], amount: i}
end
request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers")
assert response = json_response(request, 200)
request_2nd_page =
get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers", response["next_page_params"])
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, token_transfers_2 ++ token_transfers_1)
end
end
describe "/transactions/{tx_hash}/state-changes" do
@ -641,8 +852,11 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do
assert Address.checksum(token_transfer.from_address_hash) == json["from"]["hash"]
assert Address.checksum(token_transfer.to_address_hash) == json["to"]["hash"]
assert to_string(token_transfer.transaction_hash) == json["tx_hash"]
assert json["timestamp"] == nil
assert json["method"] == nil
assert to_string(token_transfer.block_hash) == json["block_hash"]
assert to_string(token_transfer.log_index) == json["log_index"]
assert check_total(Repo.preload(token_transfer, [{:token, :contract_address}]).token, json["total"], token_transfer)
end
defp check_paginated_response(first_page_resp, second_page_resp, txs) do
@ -655,4 +869,20 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do
assert second_page_resp["next_page_params"] == nil
compare_item(Enum.at(txs, 0), Enum.at(second_page_resp["items"], 0))
end
defp check_total(%Token{type: nft}, json, token_transfer) when nft in ["ERC-1155"] do
json["token_id"] in Enum.map(token_transfer.token_ids, fn x -> to_string(x) end) and
json["value"] == to_string(token_transfer.amount)
end
defp check_total(%Token{type: nft}, json, token_transfer) when nft in ["ERC-721"] do
json["token_id"] in Enum.map(token_transfer.token_ids, fn x -> to_string(x) end)
end
# with the current implementation no transfers should come with list in totals
defp check_total(%Token{type: nft}, json, _token_transfer) when nft in ["ERC-721", "ERC-1155"] and is_list(json) do
false
end
defp check_total(_, _, _), do: true
end

@ -6499,6 +6499,22 @@ defmodule Explorer.Chain do
end
end)
|> Enum.reject(&is_nil/1)
|> squash_token_transfers_in_batch()
end
defp squash_token_transfers_in_batch(token_transfers) do
token_transfers
|> Enum.group_by(fn tt -> {List.first(tt.token_ids), tt.from_address_hash, tt.to_address_hash} end)
|> Enum.map(fn {_k, v} -> Enum.reduce(v, nil, &group_batch_reducer/2) end)
|> Enum.sort_by(fn tt -> tt.index_in_batch end, :desc)
end
defp group_batch_reducer(transfer, nil) do
transfer
end
defp group_batch_reducer(transfer, acc) do
%TokenTransfer{acc | amount: Decimal.add(acc.amount, transfer.amount)}
end
@spec paginate_1155_batch_token_transfers([TokenTransfer.t()], [paging_options]) :: [TokenTransfer.t()]

@ -162,7 +162,7 @@ defmodule Explorer.Chain.TokenTransfer do
tt in TokenTransfer,
where: tt.token_contract_address_hash == ^token_address_hash and not is_nil(tt.block_number),
preload: [{:transaction, :block}, :token, :from_address, :to_address],
order_by: [desc: tt.block_number]
order_by: [desc: tt.block_number, desc: tt.log_index]
)
query
@ -182,7 +182,7 @@ defmodule Explorer.Chain.TokenTransfer do
where: fragment("? @> ARRAY[?::decimal]", tt.token_ids, ^Decimal.new(token_id)),
where: not is_nil(tt.block_number),
preload: [{:transaction, :block}, :token, :from_address, :to_address],
order_by: [desc: tt.block_number]
order_by: [desc: tt.block_number, desc: tt.log_index]
)
query

Loading…
Cancel
Save