Merge branch 'master' into sa-add-cors

pull/1173/head
Andrew Cravenho 6 years ago committed by GitHub
commit 70d4ac8902
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 40
      apps/block_scout_web/assets/__tests__/pages/address.js
  2. 46
      apps/block_scout_web/assets/__tests__/pages/address/validations.js
  3. 1
      apps/block_scout_web/assets/js/app.js
  4. 27
      apps/block_scout_web/assets/js/pages/address.js
  5. 64
      apps/block_scout_web/assets/js/pages/address/validations.js
  6. 53
      apps/block_scout_web/lib/block_scout_web/controllers/address_validation_controller.ex
  7. 2
      apps/block_scout_web/lib/block_scout_web/etherscan.ex
  8. 23
      apps/block_scout_web/lib/block_scout_web/resolvers/internal_transaction.ex
  9. 2
      apps/block_scout_web/lib/block_scout_web/resolvers/transaction.ex
  10. 18
      apps/block_scout_web/lib/block_scout_web/schema.ex
  11. 14
      apps/block_scout_web/lib/block_scout_web/schema/scalars.ex
  12. 47
      apps/block_scout_web/lib/block_scout_web/schema/types.ex
  13. 45
      apps/block_scout_web/lib/block_scout_web/templates/address_validation/index.html.eex
  14. 10
      apps/block_scout_web/priv/gettext/default.pot
  15. 10
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  16. 22
      apps/block_scout_web/test/block_scout_web/schema/query/address_test.exs
  17. 95
      apps/block_scout_web/test/block_scout_web/schema/query/node_test.exs
  18. 394
      apps/block_scout_web/test/block_scout_web/schema/query/transaction_test.exs
  19. 50
      apps/explorer/lib/explorer/graphql.ex
  20. 94
      apps/explorer/test/explorer/graphql_test.exs

@ -1,56 +1,28 @@
import { reducer, initialState } from '../../js/pages/address' import { reducer, initialState } from '../../js/pages/address'
describe('RECEIVED_NEW_BLOCK', () => { describe('RECEIVED_NEW_BLOCK', () => {
test('with new block', () => { test('increases validation count', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, { validationCount: 30 })
validationCount: 30,
validatedBlocks: [{ blockNumber: 1, blockHtml: 'test 1' }]
})
const action = { const action = {
type: 'RECEIVED_NEW_BLOCK', type: 'RECEIVED_NEW_BLOCK',
msg: { blockNumber: 2, blockHtml: 'test 2' } blockHtml: 'test 2'
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.validationCount).toEqual(31) expect(output.validationCount).toEqual(31)
expect(output.validatedBlocks).toEqual([
{ blockNumber: 2, blockHtml: 'test 2' },
{ blockNumber: 1, blockHtml: 'test 1' }
])
}) })
test('when channel has been disconnected', () => { test('when channel has been disconnected does not increase validation count', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
channelDisconnected: true, channelDisconnected: true,
validationCount: 30, validationCount: 30
validatedBlocks: [{ blockNumber: 1, blockHtml: 'test 1' }]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_BLOCK', type: 'RECEIVED_NEW_BLOCK',
msg: { blockNumber: 2, blockHtml: 'test 2' } blockHtml: 'test 2'
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.validationCount).toEqual(30) expect(output.validationCount).toEqual(30)
expect(output.validatedBlocks).toEqual([
{ blockNumber: 1, blockHtml: 'test 1' }
])
})
test('beyond page one', () => {
const state = Object.assign({}, initialState, {
beyondPageOne: true,
validationCount: 30,
validatedBlocks: [{ blockNumber: 1, blockHtml: 'test 1' }]
})
const action = {
type: 'RECEIVED_NEW_BLOCK',
msg: { blockNumber: 2, blockHtml: 'test 2' }
}
const output = reducer(state, action)
expect(output.validationCount).toEqual(31)
expect(output.validatedBlocks).toEqual([
{ blockNumber: 1, blockHtml: 'test 1' }
])
}) })
}) })

@ -0,0 +1,46 @@
import { reducer, initialState } from '../../../js/pages/address/validations'
describe('RECEIVED_NEW_BLOCK', () => {
test('adds new block to the top of the list', () => {
const state = Object.assign({}, initialState, {
items: ['test 1']
})
const action = {
type: 'RECEIVED_NEW_BLOCK',
blockHtml: 'test 2'
}
const output = reducer(state, action)
expect(output.items).toEqual(['test 2', 'test 1'])
})
test('does nothing beyond page one', () => {
const state = Object.assign({}, initialState, {
beyondPageOne: true,
channelDisconnected: false,
items: ['test 1']
})
const action = {
type: 'RECEIVED_NEW_BLOCK',
blockHtml: 'test 2'
}
const output = reducer(state, action)
expect(output.items).toEqual(['test 1'])
})
test('does nothing when channel has been disconnected', () => {
const state = Object.assign({}, initialState, {
channelDisconnected: true,
items: ['test 1']
})
const action = {
type: 'RECEIVED_NEW_BLOCK',
blockHtml: 'test 2'
}
const output = reducer(state, action)
expect(output.items).toEqual(['test 1'])
})
})

@ -21,6 +21,7 @@ import 'bootstrap'
import './locale' import './locale'
import './pages/address' import './pages/address'
import './pages/address/validations'
import './pages/blocks' import './pages/blocks'
import './pages/chain' import './pages/chain'
import './pages/pending_transactions' import './pages/pending_transactions'

@ -26,7 +26,6 @@ export const initialState = {
transactions: [], transactions: [],
internalTransactions: [], internalTransactions: [],
internalTransactionsBatch: [], internalTransactionsBatch: [],
validatedBlocks: [],
beyondPageOne: null, beyondPageOne: null,
@ -53,15 +52,7 @@ function baseReducer (state = initialState, action) {
if (state.channelDisconnected) return state if (state.channelDisconnected) return state
const validationCount = state.validationCount + 1 const validationCount = state.validationCount + 1
return Object.assign({}, state, { validationCount })
if (state.beyondPageOne) return Object.assign({}, state, { validationCount })
return Object.assign({}, state, {
validatedBlocks: [
action.msg,
...state.validatedBlocks
],
validationCount
})
} }
case 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH': { case 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH': {
if (state.channelDisconnected || state.beyondPageOne) return state if (state.channelDisconnected || state.beyondPageOne) return state
@ -209,22 +200,6 @@ const elements = {
$channelBatching.show() $channelBatching.show()
$el[0].innerHTML = numeral(state.internalTransactionsBatch.length).format() $el[0].innerHTML = numeral(state.internalTransactionsBatch.length).format()
} }
},
'[data-selector="validations-list"]': {
load ($el) {
return {
validatedBlocks: $el.children().map((index, el) => ({
blockNumber: parseInt(el.dataset.blockNumber),
blockHtml: el.outerHTML
})).toArray()
}
},
render ($el, state, oldState) {
if (oldState.validatedBlocks === state.validatedBlocks) return
const container = $el[0]
const newElements = _.map(state.validatedBlocks, ({ blockHtml }) => $(blockHtml)[0])
listMorph(container, newElements, { key: 'dataset.blockNumber' })
}
} }
} }

@ -0,0 +1,64 @@
import $ from 'jquery'
import _ from 'lodash'
import humps from 'humps'
import socket from '../../socket'
import { connectElements } from '../../lib/redux_helpers.js'
import { createAsyncLoadStore } from '../../lib/async_listing_load.js'
export const initialState = {
addressHash: null,
channelDisconnected: false
}
export function reducer (state = initialState, action) {
switch (action.type) {
case 'PAGE_LOAD':
case 'ELEMENTS_LOAD': {
return Object.assign({}, state, _.omit(action, 'type'))
}
case 'CHANNEL_DISCONNECTED': {
return Object.assign({}, state, { channelDisconnected: true })
}
case 'RECEIVED_NEW_BLOCK': {
if (state.channelDisconnected) return state
if (state.beyondPageOne) return state
return Object.assign({}, state, {
items: [
action.blockHtml,
...state.items
]
})
}
default:
return state
}
}
const elements = {
'[data-selector="channel-disconnected-message"]': {
render ($el, state) {
if (state.channelDisconnected) $el.show()
}
}
}
if ($('[data-page="blocks-validated"]').length) {
const store = createAsyncLoadStore(reducer, initialState, 'dataset.blockNumber')
connectElements({ store, elements })
const addressHash = $('[data-page="address-details"]')[0].dataset.pageAddressHash
store.dispatch({
type: 'PAGE_LOAD',
addressHash
})
const blocksChannel = socket.channel(`blocks:${addressHash}`, {})
blocksChannel.join()
blocksChannel.onError(() => store.dispatch({
type: 'CHANNEL_DISCONNECTED'
}))
blocksChannel.on('new_block', (msg) => store.dispatch({
type: 'RECEIVED_NEW_BLOCK',
blockHtml: humps.camelizeKeys(msg).blockHtml
}))
}

@ -5,12 +5,16 @@ defmodule BlockScoutWeb.AddressValidationController do
use BlockScoutWeb, :controller use BlockScoutWeb, :controller
import BlockScoutWeb.AddressController, only: [transaction_count: 1, validation_count: 1] import BlockScoutWeb.AddressController, only: [transaction_count: 1, validation_count: 1]
import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1]
alias Explorer.{Chain, Market} import BlockScoutWeb.Chain,
only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1]
alias BlockScoutWeb.BlockView
alias Explorer.ExchangeRates.Token alias Explorer.ExchangeRates.Token
alias Explorer.{Chain, Market}
alias Phoenix.View
def index(conn, %{"address_id" => address_hash_string} = params) do def index(conn, %{"address_id" => address_hash_string, "type" => "JSON"} = params) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.find_or_insert_address_from_hash(address_hash) do {:ok, address} <- Chain.find_or_insert_address_from_hash(address_hash) do
full_options = full_options =
@ -28,15 +32,52 @@ defmodule BlockScoutWeb.AddressValidationController do
blocks_plus_one = Chain.get_blocks_validated_by_address(full_options, address) blocks_plus_one = Chain.get_blocks_validated_by_address(full_options, address)
{blocks, next_page} = split_list_by_page(blocks_plus_one) {blocks, next_page} = split_list_by_page(blocks_plus_one)
next_page_path =
case next_page_params(next_page, blocks, params) do
nil ->
nil
next_page_params ->
address_validation_path(
conn,
:index,
address_hash_string,
Map.delete(next_page_params, "type")
)
end
items =
Enum.map(blocks, fn block ->
View.render_to_string(
BlockView,
"_tile.html",
conn: conn,
block: block,
block_type: BlockView.block_type(block)
)
end)
json(conn, %{items: items, next_page_path: next_page_path})
else
:error ->
unprocessable_entity(conn)
{:error, :not_found} ->
not_found(conn)
end
end
def index(conn, %{"address_id" => address_hash_string}) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.find_or_insert_address_from_hash(address_hash) do
render( render(
conn, conn,
"index.html", "index.html",
address: address, address: address,
blocks: blocks, current_path: current_path(conn),
transaction_count: transaction_count(address), transaction_count: transaction_count(address),
validation_count: validation_count(address), validation_count: validation_count(address),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null()
next_page_params: next_page_params(next_page, blocks, params)
) )
else else
:error -> :error ->

@ -1012,7 +1012,7 @@ defmodule BlockScoutWeb.Etherscan do
@account_txlistinternal_action %{ @account_txlistinternal_action %{
name: "txlistinternal", name: "txlistinternal",
description: description:
"Get internal transactions by transaction or address hash. Up to a maximum of 10,000 internal transactions.", "Get internal transactions by transaction or address hash. Up to a maximum of 10,000 internal transactions. Also available through a GraphQL 'transaction' query.",
required_params: [ required_params: [
%{ %{
key: "txhash", key: "txhash",

@ -0,0 +1,23 @@
defmodule BlockScoutWeb.Resolvers.InternalTransaction do
@moduledoc false
alias Absinthe.Relay.Connection
alias Explorer.Chain.Transaction
alias Explorer.{GraphQL, Repo}
def get_by(%{transaction_hash: _, index: _} = args) do
GraphQL.get_internal_transaction(args)
end
def get_by(%Transaction{} = transaction, args, _) do
transaction
|> GraphQL.transaction_to_internal_transactions_query()
|> Connection.from_query(&Repo.all/1, args, options(args))
end
defp options(%{before: _}), do: []
defp options(%{count: count}), do: [count: count]
defp options(_), do: []
end

@ -8,7 +8,7 @@ defmodule BlockScoutWeb.Resolvers.Transaction do
def get_by(_, %{hash: hash}, _) do def get_by(_, %{hash: hash}, _) do
case Chain.hash_to_transaction(hash) do case Chain.hash_to_transaction(hash) do
{:ok, transaction} -> {:ok, transaction} {:ok, transaction} -> {:ok, transaction}
{:error, :not_found} -> {:error, "Transaction hash #{hash} was not found."} {:error, :not_found} -> {:error, "Transaction not found."}
end end
end end

@ -6,8 +6,16 @@ defmodule BlockScoutWeb.Schema do
alias Absinthe.Middleware.Dataloader, as: AbsintheMiddlewareDataloader alias Absinthe.Middleware.Dataloader, as: AbsintheMiddlewareDataloader
alias Absinthe.Plugin, as: AbsinthePlugin alias Absinthe.Plugin, as: AbsinthePlugin
alias BlockScoutWeb.Resolvers.{Address, Block, Transaction}
alias BlockScoutWeb.Resolvers.{
Address,
Block,
InternalTransaction,
Transaction
}
alias Explorer.Chain alias Explorer.Chain
alias Explorer.Chain.InternalTransaction, as: ExplorerChainInternalTransaction
alias Explorer.Chain.Transaction, as: ExplorerChainTransaction alias Explorer.Chain.Transaction, as: ExplorerChainTransaction
import_types(BlockScoutWeb.Schema.Types) import_types(BlockScoutWeb.Schema.Types)
@ -17,6 +25,9 @@ defmodule BlockScoutWeb.Schema do
%ExplorerChainTransaction{}, _ -> %ExplorerChainTransaction{}, _ ->
:transaction :transaction
%ExplorerChainInternalTransaction{}, _ ->
:internal_transaction
_, _ -> _, _ ->
nil nil
end) end)
@ -29,6 +40,11 @@ defmodule BlockScoutWeb.Schema do
{:ok, hash} = Chain.string_to_transaction_hash(transaction_hash_string) {:ok, hash} = Chain.string_to_transaction_hash(transaction_hash_string)
Transaction.get_by(%{}, %{hash: hash}, %{}) Transaction.get_by(%{}, %{hash: hash}, %{})
%{type: :internal_transaction, id: id}, _ ->
%{"transaction_hash" => transaction_hash_string, "index" => index} = Jason.decode!(id)
{:ok, transaction_hash} = Chain.string_to_transaction_hash(transaction_hash_string)
InternalTransaction.get_by(%{transaction_hash: transaction_hash, index: index})
_, _ -> _, _ ->
{:error, "Unknown node"} {:error, "Unknown node"}
end) end)

@ -99,4 +99,18 @@ defmodule BlockScoutWeb.Schema.Scalars do
value(:ok) value(:ok)
value(:error) value(:error)
end end
enum :call_type do
value(:call)
value(:callcode)
value(:delegatecall)
value(:staticcall)
end
enum :type do
value(:call)
value(:create)
value(:reward)
value(:selfdestruct)
end
end end

@ -6,12 +6,16 @@ defmodule BlockScoutWeb.Schema.Types do
import Absinthe.Resolution.Helpers import Absinthe.Resolution.Helpers
alias BlockScoutWeb.Resolvers.Transaction alias BlockScoutWeb.Resolvers.{
InternalTransaction,
Transaction
}
import_types(Absinthe.Type.Custom) import_types(Absinthe.Type.Custom)
import_types(BlockScoutWeb.Schema.Scalars) import_types(BlockScoutWeb.Schema.Scalars)
connection(node_type: :transaction) connection(node_type: :transaction)
connection(node_type: :internal_transaction)
@desc """ @desc """
A stored representation of a Web3 address. A stored representation of a Web3 address.
@ -60,6 +64,30 @@ defmodule BlockScoutWeb.Schema.Types do
field(:parent_hash, :full_hash) field(:parent_hash, :full_hash)
end end
@desc """
Models internal transactions.
"""
node object(:internal_transaction, id_fetcher: &internal_transaction_id_fetcher/2) do
field(:call_type, :call_type)
field(:created_contract_code, :data)
field(:error, :string)
field(:gas, :decimal)
field(:gas_used, :decimal)
field(:index, :integer)
field(:init, :data)
field(:input, :data)
field(:output, :data)
field(:trace_address, :json)
field(:type, :type)
field(:value, :wei)
field(:block_number, :integer)
field(:transaction_index, :integer)
field(:created_contract_address_hash, :address_hash)
field(:from_address_hash, :address_hash)
field(:to_address_hash, :address_hash)
field(:transaction_hash, :full_hash)
end
@desc """ @desc """
The representation of a verified Smart Contract. The representation of a verified Smart Contract.
@ -99,6 +127,19 @@ defmodule BlockScoutWeb.Schema.Types do
field(:from_address_hash, :address_hash) field(:from_address_hash, :address_hash)
field(:to_address_hash, :address_hash) field(:to_address_hash, :address_hash)
field(:created_contract_address_hash, :address_hash) field(:created_contract_address_hash, :address_hash)
connection field(:internal_transactions, node_type: :internal_transaction) do
arg(:count, :integer)
resolve(&InternalTransaction.get_by/3)
complexity(fn
%{first: first}, child_complexity ->
first * child_complexity
%{last: last}, child_complexity ->
last * child_complexity
end)
end
end end
@desc """ @desc """
@ -113,4 +154,8 @@ defmodule BlockScoutWeb.Schema.Types do
end end
def transaction_id_fetcher(%{hash: hash}, _), do: to_string(hash) def transaction_id_fetcher(%{hash: hash}, _), do: to_string(hash)
def internal_transaction_id_fetcher(%{transaction_hash: transaction_hash, index: index}, _) do
Jason.encode!(%{transaction_hash: to_string(transaction_hash), index: index})
end
end end

@ -1,7 +1,7 @@
<section class="container"> <section class="container">
<%= render BlockScoutWeb.AddressView, "overview.html", assigns %> <%= render BlockScoutWeb.AddressView, "overview.html", assigns %>
<section> <section data-page="blocks-validated">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<!-- DESKTOP TAB NAV --> <!-- DESKTOP TAB NAV -->
@ -98,7 +98,7 @@
</ul> </ul>
</div> </div>
<div class="card-body"> <div data-async-listing="<%= @current_path %>" class="card-body">
<div data-selector="channel-disconnected-message" style="display: none;"> <div data-selector="channel-disconnected-message" style="display: none;">
<div data-selector="reload-button" class="alert alert-danger"> <div data-selector="reload-button" class="alert alert-danger">
<a href="#" class="alert-link"><%= gettext "Connection Lost, click to load newer validations" %></a> <a href="#" class="alert-link"><%= gettext "Connection Lost, click to load newer validations" %></a>
@ -106,24 +106,31 @@
</div> </div>
<h2 class="card-title"><%=gettext("Blocks Validated")%></h2> <h2 class="card-title"><%=gettext("Blocks Validated")%></h2>
<span data-selector="validations-list"> <div data-loading-message class="tile tile-muted text-center mt-3">
<%= for block <- @blocks do %> <span class="loading-spinner-small mr-2">
<%= render BlockScoutWeb.BlockView, "_tile.html", block: block, block_type: BlockScoutWeb.BlockView.block_type(block)%> <span class="loading-spinner-block-1"></span>
<% end %> <span class="loading-spinner-block-2"></span>
</span> </span>
<div> <%= gettext("Loading...") %>
<%= if @next_page_params do %> </div>
<%= link( <div data-empty-response-message class="tile tile-muted text-center" style="display: none;">
gettext("Older"), <span><%= gettext "There are no blocks validated by this address." %></span>
class: "button button-secondary button-sm float-right mt-3", </div>
to: address_validation_path( <button data-error-message class="alert alert-danger col-12 text-left" style="display: none;">
@conn, <span class="alert-link">
:index, <%= gettext "Something went wrong, click to reload." %>
@address, </span>
@next_page_params </button>
) <div data-items data-selector="validations-list"></div>
) %> <a data-next-page-button href="#" class="button button-secondary button-small float-right mt-4" style="display: none;">
<% end %> <%= gettext("Older") %>
</a>
<div data-loading-button class="button button-secondary button-small float-right mt-4" style="display: none;">
<span class="loading-spinner-small mr-2">
<span class="loading-spinner-block-1"></span>
<span class="loading-spinner-block-2"></span>
</span>
<%= gettext("Loading...") %>
</div> </div>
</div> <!-- Card Body --> </div> <!-- Card Body -->
</div> <!-- Card --> </div> <!-- Card -->

@ -639,7 +639,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:76 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:76
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:31 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:31
#: lib/block_scout_web/templates/address_validation/index.html.eex:117 #: lib/block_scout_web/templates/address_validation/index.html.eex:126
#: lib/block_scout_web/templates/block/index.html.eex:30 #: lib/block_scout_web/templates/block/index.html.eex:30
#: lib/block_scout_web/templates/block_transaction/index.html.eex:50 #: lib/block_scout_web/templates/block_transaction/index.html.eex:50
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:41 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:41
@ -1188,6 +1188,8 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address_read_contract/index.html.eex:17 #: lib/block_scout_web/templates/address_read_contract/index.html.eex:17
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:19 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:19
#: lib/block_scout_web/templates/address_validation/index.html.eex:114
#: lib/block_scout_web/templates/address_validation/index.html.eex:133
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:25 #: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:25
msgid "Loading..." msgid "Loading..."
msgstr "" msgstr ""
@ -1417,7 +1419,13 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:60 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:60
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:26 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:26
#: lib/block_scout_web/templates/address_validation/index.html.eex:121
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:23 #: lib/block_scout_web/templates/tokens/holder/index.html.eex:23
#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:21 #: lib/block_scout_web/templates/tokens/transfer/index.html.eex:21
msgid "Something went wrong, click to reload." msgid "Something went wrong, click to reload."
msgstr "" msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_validation/index.html.eex:117
msgid "There are no blocks validated by this address."
msgstr ""

@ -639,7 +639,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:76 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:76
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:31 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:31
#: lib/block_scout_web/templates/address_validation/index.html.eex:117 #: lib/block_scout_web/templates/address_validation/index.html.eex:126
#: lib/block_scout_web/templates/block/index.html.eex:30 #: lib/block_scout_web/templates/block/index.html.eex:30
#: lib/block_scout_web/templates/block_transaction/index.html.eex:50 #: lib/block_scout_web/templates/block_transaction/index.html.eex:50
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:41 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:41
@ -1188,6 +1188,8 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address_read_contract/index.html.eex:17 #: lib/block_scout_web/templates/address_read_contract/index.html.eex:17
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:19 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:19
#: lib/block_scout_web/templates/address_validation/index.html.eex:114
#: lib/block_scout_web/templates/address_validation/index.html.eex:133
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:25 #: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:25
msgid "Loading..." msgid "Loading..."
msgstr "" msgstr ""
@ -1417,7 +1419,13 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:60 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:60
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:26 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:26
#: lib/block_scout_web/templates/address_validation/index.html.eex:121
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:23 #: lib/block_scout_web/templates/tokens/holder/index.html.eex:23
#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:21 #: lib/block_scout_web/templates/tokens/transfer/index.html.eex:21
msgid "Something went wrong, click to reload." msgid "Something went wrong, click to reload."
msgstr "" msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_validation/index.html.eex:117
msgid "There are no blocks validated by this address."
msgstr ""

@ -515,33 +515,13 @@ defmodule BlockScoutWeb.Schema.Query.AddressTest do
|> List.last() |> List.last()
|> Map.get("cursor") |> Map.get("cursor")
query3 = """
query ($hash: AddressHash!, $first: Int!, $after: Int!) {
address(hash: $hash) {
transactions(first: $first, after: $after) {
page_info {
has_next_page
has_previous_page
}
edges {
node {
hash
block_number
}
cursor
}
}
}
}
"""
variables3 = %{ variables3 = %{
"hash" => to_string(address.hash), "hash" => to_string(address.hash),
"first" => 3, "first" => 3,
"after" => last_cursor_page2 "after" => last_cursor_page2
} }
conn = get(conn, "/graphql", query: query3, variables: variables3) conn = get(conn, "/graphql", query: query2, variables: variables3)
%{"data" => %{"address" => %{"transactions" => page3}}} = json_response(conn, 200) %{"data" => %{"address" => %{"transactions" => page3}}} = json_response(conn, 200)

@ -31,5 +31,100 @@ defmodule BlockScoutWeb.Schema.Query.NodeTest do
} }
} }
end end
test "with 'id' for non-existent transaction", %{conn: conn} do
transaction = build(:transaction)
query = """
query($id: ID!) {
node(id: $id) {
... on Transaction {
id
hash
}
}
}
"""
id = Base.encode64("Transaction:#{transaction.hash}")
variables = %{"id" => id}
conn = get(conn, "/graphql", query: query, variables: variables)
%{"errors" => [error]} = json_response(conn, 200)
assert error["message"] == "Transaction not found."
end
test "with valid argument 'id' for an internal transaction", %{conn: conn} do
transaction = insert(:transaction)
internal_transaction = insert(:internal_transaction, transaction: transaction, index: 0)
query = """
query($id: ID!) {
node(id: $id) {
... on InternalTransaction {
id
transaction_hash
index
}
}
}
"""
id =
%{transaction_hash: to_string(transaction.hash), index: internal_transaction.index}
|> Jason.encode!()
|> (fn unique_id -> "InternalTransaction:#{unique_id}" end).()
|> Base.encode64()
variables = %{"id" => id}
conn = get(conn, "/graphql", query: query, variables: variables)
assert json_response(conn, 200) == %{
"data" => %{
"node" => %{
"id" => id,
"transaction_hash" => to_string(transaction.hash),
"index" => internal_transaction.index
}
}
}
end
test "with 'id' for non-existent internal transaction", %{conn: conn} do
transaction = build(:transaction)
internal_transaction = build(:internal_transaction, transaction: transaction, index: 0)
query = """
query($id: ID!) {
node(id: $id) {
... on InternalTransaction {
id
transaction_hash
index
}
}
}
"""
id =
%{transaction_hash: to_string(transaction.hash), index: internal_transaction.index}
|> Jason.encode!()
|> (fn unique_id -> "InternalTransaction:#{unique_id}" end).()
|> Base.encode64()
variables = %{"id" => id}
conn = get(conn, "/graphql", query: query, variables: variables)
%{"errors" => [error]} = json_response(conn, 200)
assert error["message"] == "Internal transaction not found."
end
end end
end end

@ -81,7 +81,7 @@ defmodule BlockScoutWeb.Schema.Query.TransactionTest do
conn = get(conn, "/graphql", query: query, variables: variables) conn = get(conn, "/graphql", query: query, variables: variables)
assert %{"errors" => [error]} = json_response(conn, 200) assert %{"errors" => [error]} = json_response(conn, 200)
assert error["message"] =~ ~s(Transaction hash #{transaction.hash} was not found) assert error["message"] == "Transaction not found."
end end
test "errors if argument 'hash' is missing", %{conn: conn} do test "errors if argument 'hash' is missing", %{conn: conn} do
@ -116,4 +116,396 @@ defmodule BlockScoutWeb.Schema.Query.TransactionTest do
assert error["message"] =~ ~s(Argument "hash" has invalid value) assert error["message"] =~ ~s(Argument "hash" has invalid value)
end end
end end
describe "transaction internal_transactions field" do
test "returns all expected internal_transaction fields", %{conn: conn} do
address = insert(:address)
contract_address = insert(:contract_address)
block = insert(:block)
transaction =
:transaction
|> insert(from_address: address)
|> with_contract_creation(contract_address)
|> with_block(block)
internal_transaction_attributes = %{
transaction: transaction,
index: 0,
from_address: address,
call_type: :call
}
internal_transaction =
:internal_transaction_create
|> insert(internal_transaction_attributes)
|> with_contract_creation(contract_address)
query = """
query ($hash: FullHash!, $first: Int!) {
transaction(hash: $hash) {
internal_transactions(first: $first) {
edges {
node {
call_type
created_contract_code
error
gas
gas_used
index
init
input
output
trace_address
type
value
block_number
transaction_index
created_contract_address_hash
from_address_hash
to_address_hash
transaction_hash
}
}
}
}
}
"""
variables = %{
"hash" => to_string(transaction.hash),
"first" => 1
}
conn = get(conn, "/graphql", query: query, variables: variables)
assert json_response(conn, 200) == %{
"data" => %{
"transaction" => %{
"internal_transactions" => %{
"edges" => [
%{
"node" => %{
"call_type" => internal_transaction.call_type |> to_string() |> String.upcase(),
"created_contract_code" => to_string(internal_transaction.created_contract_code),
"error" => internal_transaction.error,
"gas" => to_string(internal_transaction.gas),
"gas_used" => to_string(internal_transaction.gas_used),
"index" => internal_transaction.index,
"init" => to_string(internal_transaction.init),
"input" => nil,
"output" => nil,
"trace_address" => Jason.encode!(internal_transaction.trace_address),
"type" => internal_transaction.type |> to_string() |> String.upcase(),
"value" => to_string(internal_transaction.value.value),
"block_number" => internal_transaction.block_number,
"transaction_index" => internal_transaction.transaction_index,
"created_contract_address_hash" =>
to_string(internal_transaction.created_contract_address_hash),
"from_address_hash" => to_string(internal_transaction.from_address_hash),
"to_address_hash" => nil,
"transaction_hash" => to_string(internal_transaction.transaction_hash)
}
}
]
}
}
}
}
end
test "with transaction with zero internal transactions", %{conn: conn} do
address = insert(:address)
block = insert(:block)
transaction =
:transaction
|> insert(from_address: address)
|> with_block(block)
query = """
query ($hash: FullHash!, $first: Int!) {
transaction(hash: $hash) {
internal_transactions(first: $first) {
edges {
node {
index
transaction_hash
}
}
}
}
}
"""
variables = %{
"hash" => to_string(transaction.hash),
"first" => 1
}
conn = get(conn, "/graphql", query: query, variables: variables)
assert json_response(conn, 200) == %{
"data" => %{
"transaction" => %{
"internal_transactions" => %{
"edges" => []
}
}
}
}
end
test "internal transactions are ordered by ascending index", %{conn: conn} do
transaction = insert(:transaction)
insert(:internal_transaction, transaction: transaction, index: 2)
insert(:internal_transaction, transaction: transaction, index: 0)
insert(:internal_transaction, transaction: transaction, index: 1)
query = """
query ($hash: FullHash!, $first: Int!) {
transaction(hash: $hash) {
internal_transactions(first: $first) {
edges {
node {
index
transaction_hash
}
}
}
}
}
"""
variables = %{
"hash" => to_string(transaction.hash),
"first" => 3
}
response =
conn
|> get("/graphql", query: query, variables: variables)
|> json_response(200)
internal_transactions = get_in(response, ["data", "transaction", "internal_transactions", "edges"])
index_order = Enum.map(internal_transactions, & &1["node"]["index"])
assert index_order == Enum.sort(index_order)
end
test "complexity correlates to first or last argument", %{conn: conn} do
transaction = insert(:transaction)
query1 = """
query ($hash: FullHash!, $first: Int!) {
transaction(hash: $hash) {
internal_transactions(first: $first) {
edges {
node {
index
transaction_hash
}
}
}
}
}
"""
variables1 = %{
"hash" => to_string(transaction.hash),
"first" => 55
}
response1 =
conn
|> get("/graphql", query: query1, variables: variables1)
|> json_response(200)
assert %{"errors" => [error1, error2, error3]} = response1
assert error1["message"] =~ ~s(Field internal_transactions is too complex)
assert error2["message"] =~ ~s(Field transaction is too complex)
assert error3["message"] =~ ~s(Operation is too complex)
query2 = """
query ($hash: FullHash!, $last: Int!, $count: Int!) {
transaction(hash: $hash) {
internal_transactions(last: $last, count: $count) {
edges {
node {
index
transaction_hash
}
}
}
}
}
"""
variables2 = %{
"hash" => to_string(transaction.hash),
"last" => 55,
"count" => 100
}
response2 =
conn
|> get("/graphql", query: query2, variables: variables2)
|> json_response(200)
assert %{"errors" => [error1, error2, error3]} = response2
assert error1["message"] =~ ~s(Field internal_transactions is too complex)
assert error2["message"] =~ ~s(Field transaction is too complex)
assert error3["message"] =~ ~s(Operation is too complex)
end
test "with 'last' and 'count' arguments", %{conn: conn} do
# "`last: N` must always be acompanied by either a `before:` argument to
# the query, or an explicit `count:` option to the `from_query` call.
# Otherwise it is impossible to derive the required offset."
# https://hexdocs.pm/absinthe_relay/Absinthe.Relay.Connection.html#from_query/4
#
# This test ensures support for a 'count' argument.
transaction = insert(:transaction)
insert(:internal_transaction, transaction: transaction, index: 2)
insert(:internal_transaction, transaction: transaction, index: 0)
insert(:internal_transaction, transaction: transaction, index: 1)
query = """
query ($hash: FullHash!, $last: Int!, $count: Int!) {
transaction(hash: $hash) {
internal_transactions(last: $last, count: $count) {
edges {
node {
index
transaction_hash
}
}
}
}
}
"""
variables = %{
"hash" => to_string(transaction.hash),
"last" => 1,
"count" => 3
}
[internal_transaction] =
conn
|> get("/graphql", query: query, variables: variables)
|> json_response(200)
|> get_in(["data", "transaction", "internal_transactions", "edges"])
assert internal_transaction["node"]["index"] == 2
end
test "pagination support with 'first' and 'after' arguments", %{conn: conn} do
transaction = insert(:transaction)
for index <- 0..5 do
insert(:internal_transaction_create, transaction: transaction, index: index)
end
query1 = """
query ($hash: AddressHash!, $first: Int!) {
transaction(hash: $hash) {
internal_transactions(first: $first) {
page_info {
has_next_page
has_previous_page
}
edges {
node {
index
transaction_hash
}
cursor
}
}
}
}
"""
variables1 = %{
"hash" => to_string(transaction.hash),
"first" => 2
}
conn = get(conn, "/graphql", query: query1, variables: variables1)
%{"data" => %{"transaction" => %{"internal_transactions" => page1}}} = json_response(conn, 200)
assert page1["page_info"] == %{"has_next_page" => true, "has_previous_page" => false}
assert Enum.all?(page1["edges"], &(&1["node"]["index"] in 0..1))
last_cursor_page1 =
page1
|> Map.get("edges")
|> List.last()
|> Map.get("cursor")
query2 = """
query ($hash: AddressHash!, $first: Int!, $after: ID!) {
transaction(hash: $hash) {
internal_transactions(first: $first, after: $after) {
page_info {
has_next_page
has_previous_page
}
edges {
node {
index
transaction_hash
}
cursor
}
}
}
}
"""
variables2 = %{
"hash" => to_string(transaction.hash),
"first" => 2,
"after" => last_cursor_page1
}
page2 =
conn
|> get("/graphql", query: query2, variables: variables2)
|> json_response(200)
|> get_in(["data", "transaction", "internal_transactions"])
assert page2["page_info"] == %{"has_next_page" => true, "has_previous_page" => true}
assert Enum.all?(page2["edges"], &(&1["node"]["index"] in 2..3))
last_cursor_page2 =
page2
|> Map.get("edges")
|> List.last()
|> Map.get("cursor")
variables3 = %{
"hash" => to_string(transaction.hash),
"first" => 2,
"after" => last_cursor_page2
}
page3 =
conn
|> get("/graphql", query: query2, variables: variables3)
|> json_response(200)
|> get_in(["data", "transaction", "internal_transactions"])
assert page3["page_info"] == %{"has_next_page" => false, "has_previous_page" => true}
assert Enum.all?(page3["edges"], &(&1["node"]["index"] in 4..5))
end
end
end end

@ -5,13 +5,28 @@ defmodule Explorer.GraphQL do
import Ecto.Query, import Ecto.Query,
only: [ only: [
from: 2,
order_by: 3, order_by: 3,
or_where: 3, or_where: 3,
where: 3 where: 3
] ]
alias Explorer.Chain.{Address, Hash, Transaction} alias Explorer.Chain.{
Address,
Hash,
InternalTransaction,
Transaction
}
alias Explorer.{Chain, Repo}
@doc """
Returns a query to fetch transactions with a matching `to_address_hash`,
`from_address_hash`, or `created_contract_address_hash` field for a given address.
Orders transactions by descending block number and index.
"""
@spec address_to_transactions_query(Address.t()) :: Ecto.Query.t()
def address_to_transactions_query(%Address{hash: %Hash{byte_count: unquote(Hash.Address.byte_count())} = address_hash}) do def address_to_transactions_query(%Address{hash: %Hash{byte_count: unquote(Hash.Address.byte_count())} = address_hash}) do
Transaction Transaction
|> order_by([transaction], desc: transaction.block_number, desc: transaction.index) |> order_by([transaction], desc: transaction.block_number, desc: transaction.index)
@ -19,4 +34,37 @@ defmodule Explorer.GraphQL do
|> or_where([transaction], transaction.from_address_hash == ^address_hash) |> or_where([transaction], transaction.from_address_hash == ^address_hash)
|> or_where([transaction], transaction.created_contract_address_hash == ^address_hash) |> or_where([transaction], transaction.created_contract_address_hash == ^address_hash)
end end
@doc """
Returns an internal transaction for a given transaction hash and index.
"""
@spec get_internal_transaction(map()) :: {:ok, InternalTransaction.t()} | {:error, String.t()}
def get_internal_transaction(%{transaction_hash: _, index: _} = clauses) do
if internal_transaction = Repo.get_by(InternalTransaction, clauses) do
{:ok, internal_transaction}
else
{:error, "Internal transaction not found."}
end
end
@doc """
Returns a query to fetch internal transactions for a given transaction.
Orders internal transactions by ascending index.
"""
@spec transaction_to_internal_transactions_query(Transaction.t()) :: Ecto.Query.t()
def transaction_to_internal_transactions_query(%Transaction{
hash: %Hash{byte_count: unquote(Hash.Full.byte_count())} = hash
}) do
query =
from(
it in InternalTransaction,
inner_join: t in assoc(it, :transaction),
order_by: [asc: it.index],
where: it.transaction_hash == ^hash,
select: it
)
Chain.where_transaction_has_multiple_internal_transactions(query)
end
end end

@ -87,4 +87,98 @@ defmodule Explorer.GraphQLTest do
assert block_number_and_index_order == Enum.sort(block_number_and_index_order, &(&1 >= &2)) assert block_number_and_index_order == Enum.sort(block_number_and_index_order, &(&1 >= &2))
end end
end end
describe "get_internal_transaction/1" do
test "returns existing internal transaction" do
transaction = insert(:transaction)
internal_transaction = insert(:internal_transaction, transaction: transaction, index: 0)
clauses = %{transaction_hash: transaction.hash, index: internal_transaction.index}
{:ok, found_internal_transaction} = GraphQL.get_internal_transaction(clauses)
assert found_internal_transaction.transaction_hash == transaction.hash
assert found_internal_transaction.index == internal_transaction.index
end
test "returns error tuple for non-existent internal transaction" do
transaction = build(:transaction)
internal_transaction = build(:internal_transaction, transaction: transaction, index: 0)
clauses = %{transaction_hash: transaction.hash, index: internal_transaction.index}
assert GraphQL.get_internal_transaction(clauses) == {:error, "Internal transaction not found."}
end
end
describe "transcation_to_internal_transactions_query/1" do
test "with transaction with one internal transaction" do
transaction1 = insert(:transaction)
transaction2 = insert(:transaction)
internal_transaction = insert(:internal_transaction_create, transaction: transaction1, index: 0)
insert(:internal_transaction_create, transaction: transaction2, index: 0)
[found_internal_transaction] =
transaction1
|> GraphQL.transaction_to_internal_transactions_query()
|> Repo.all()
assert found_internal_transaction.transaction_hash == transaction1.hash
assert found_internal_transaction.index == internal_transaction.index
end
test "with transaction with multiple internal transactions" do
transaction1 = insert(:transaction)
transaction2 = insert(:transaction)
for index <- 0..2 do
insert(:internal_transaction_create, transaction: transaction1, index: index)
end
insert(:internal_transaction_create, transaction: transaction2, index: 0)
found_internal_transactions =
transaction1
|> GraphQL.transaction_to_internal_transactions_query()
|> Repo.all()
assert length(found_internal_transactions) == 3
for found_internal_transaction <- found_internal_transactions do
assert found_internal_transaction.transaction_hash == transaction1.hash
end
end
test "orders internal transactions by ascending index" do
transaction = insert(:transaction)
insert(:internal_transaction_create, transaction: transaction, index: 2)
insert(:internal_transaction_create, transaction: transaction, index: 0)
insert(:internal_transaction_create, transaction: transaction, index: 1)
found_internal_transactions =
transaction
|> GraphQL.transaction_to_internal_transactions_query()
|> Repo.all()
index_order = Enum.map(found_internal_transactions, & &1.index)
assert index_order == Enum.sort(index_order)
end
# Note that `transaction_to_internal_transactions_query/1` relies on
# `Explorer.Chain.where_transaction_has_multiple_transactions/1` to ensure the
# following behavior:
#
# * exclude internal transactions of type call with no siblings in the
# transaction
#
# * include internal transactions of type create, reward, or suicide
# even when they are alone in the parent transaction
#
# These two requirements are tested in `Explorer.ChainTest`.
end
end end

Loading…
Cancel
Save