Merge branch 'master' into split-js-logic-into-multiple-files

pull/2944/head
Victor Baranov 5 years ago committed by GitHub
commit ebcdb1aacb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .dialyzer-ignore
  2. 24
      CHANGELOG.md
  3. 35
      apps/block_scout_web/assets/__tests__/pages/chain.js
  4. 20
      apps/block_scout_web/assets/js/pages/chain.js
  5. 3
      apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_controller.ex
  6. 3
      apps/block_scout_web/lib/block_scout_web/controllers/address_read_contract_controller.ex
  7. 3
      apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex
  8. 5
      apps/block_scout_web/lib/block_scout_web/controllers/address_token_transfer_controller.ex
  9. 23
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_raw_trace_controller.ex
  10. 2
      apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex
  11. 2
      apps/block_scout_web/lib/block_scout_web/views/address_view.ex
  12. 11
      apps/block_scout_web/lib/block_scout_web/views/layout_view.ex
  13. 2
      apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex
  14. 2
      apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex
  15. 6
      apps/block_scout_web/test/block_scout_web/views/layout_view_test.exs
  16. 40
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity.ex
  17. 4
      apps/explorer/config/config.exs
  18. 5
      apps/explorer/config/dev.exs
  19. 2
      apps/explorer/config/dev/ganache.exs
  20. 2
      apps/explorer/config/dev/geth.exs
  21. 2
      apps/explorer/config/dev/parity.exs
  22. 2
      apps/explorer/config/dev/rsk.exs
  23. 4
      apps/explorer/config/prod/ganache.exs
  24. 4
      apps/explorer/config/prod/geth.exs
  25. 54
      apps/explorer/lib/explorer/chain.ex
  26. 2
      apps/explorer/lib/explorer/chain/address.ex
  27. 9
      apps/explorer/lib/explorer/chain/events/listener.ex
  28. 159
      apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex
  29. 2
      apps/explorer/lib/explorer/smart_contract/reader.ex
  30. 171
      apps/explorer/test/explorer/chain/import/runner/internal_transactions_test.exs
  31. 232
      apps/explorer/test/explorer/chain/import_test.exs
  32. 31
      apps/explorer/test/explorer/chain_test.exs
  33. 4
      apps/indexer/config/dev/ganache.exs
  34. 2
      apps/indexer/config/dev/geth.exs
  35. 2
      apps/indexer/config/dev/parity.exs
  36. 2
      apps/indexer/config/dev/rsk.exs
  37. 4
      apps/indexer/config/prod/ganache.exs
  38. 2
      apps/indexer/config/prod/geth.exs
  39. 14
      apps/indexer/config/test.exs
  40. 8
      apps/indexer/config/test/ganache.exs
  41. 8
      apps/indexer/config/test/geth.exs
  42. 8
      apps/indexer/config/test/parity.exs
  43. 8
      apps/indexer/config/test/rsk.exs
  44. 4
      apps/indexer/lib/indexer/block/realtime/fetcher.ex
  45. 19
      apps/indexer/test/indexer/fetcher/internal_transaction_test.exs

@ -12,4 +12,4 @@ apps/explorer/lib/explorer/smart_contract/publisher_worker.ex:6: The pattern 'fa
apps/explorer/lib/explorer/smart_contract/publisher_worker.ex:6: The test 5 == 'infinity' can never evaluate to 'true' apps/explorer/lib/explorer/smart_contract/publisher_worker.ex:6: The test 5 == 'infinity' can never evaluate to 'true'
lib/block_scout_web/router.ex:1 lib/block_scout_web/router.ex:1
lib/phoenix/router.ex:324 lib/phoenix/router.ex:324
lib/block_scout_web/views/layout_view.ex:142 lib/block_scout_web/views/layout_view.ex:143

@ -1,18 +1,32 @@
## Current ## Current
### Features ### Features
- [#3013](https://github.com/poanetwork/blockscout/pull/3013) - Raw trace of transaction on-demand
### Fixes
### Chore
## 3.1.0-beta
### Features
- [#3013](https://github.com/poanetwork/blockscout/pull/3013), [#3026](https://github.com/poanetwork/blockscout/pull/3026), [#3031](https://github.com/poanetwork/blockscout/pull/3031) - Raw trace of transaction on-demand
- [#3000](https://github.com/poanetwork/blockscout/pull/3000) - Get rid of storing of first trace for all types of transactions for Parity variant
- [#2875](https://github.com/poanetwork/blockscout/pull/2875) - Save contract code from Parity genesis file - [#2875](https://github.com/poanetwork/blockscout/pull/2875) - Save contract code from Parity genesis file
- [#2834](https://github.com/poanetwork/blockscout/pull/2834) - always redirect to checksummed hash - [#2834](https://github.com/poanetwork/blockscout/pull/2834), [#3009](https://github.com/poanetwork/blockscout/pull/3009), [#3014](https://github.com/poanetwork/blockscout/pull/3014), [#3033](https://github.com/poanetwork/blockscout/pull/3033) - always redirect to checksummed hash
### Fixes ### Fixes
- [#3037](https://github.com/poanetwork/blockscout/pull/3037) - Make buttons color at verification page consistent
- [#3034](https://github.com/poanetwork/blockscout/pull/3034) - Support stateMutability=view to define reading functions in smart-contracts
- [#3029](https://github.com/poanetwork/blockscout/pull/3029) - Fix transactions and blocks appearance on the main page
- [#3028](https://github.com/poanetwork/blockscout/pull/3028) - Decrease polling period value for realtime fetcher
- [#3027](https://github.com/poanetwork/blockscout/pull/3027) - Rescue for SUPPORTED_CHAINS env var parsing
- [#3025](https://github.com/poanetwork/blockscout/pull/3025) - Fix splitting of indexer/web components setup
- [#3024](https://github.com/poanetwork/blockscout/pull/3024) - Fix pool size default value in config - [#3024](https://github.com/poanetwork/blockscout/pull/3024) - Fix pool size default value in config
- [#3021](https://github.com/poanetwork/blockscout/pull/3021), [#3022](https://github.com/poanetwork/blockscout/pull/3022) - Refine dev/test config - [#3021](https://github.com/poanetwork/blockscout/pull/3021), [#3022](https://github.com/poanetwork/blockscout/pull/3022) - Refine dev/test config
- [#3016](https://github.com/poanetwork/blockscout/pull/3016), [#3017](https://github.com/poanetwork/blockscout/pull/3017) - Fix token instance QR code data - [#3016](https://github.com/poanetwork/blockscout/pull/3016), [#3017](https://github.com/poanetwork/blockscout/pull/3017) - Fix token instance QR code data
- [#3014](https://github.com/poanetwork/blockscout/pull/3014) - Fix checksum address feature for tokens pages
- [#3012](https://github.com/poanetwork/blockscout/pull/3012) - Speedup token transfers list query - [#3012](https://github.com/poanetwork/blockscout/pull/3012) - Speedup token transfers list query
- [#3011](https://github.com/poanetwork/blockscout/pull/3011) - Revert realtime fetcher small skips feature - [#3011](https://github.com/poanetwork/blockscout/pull/3011) - Revert realtime fetcher small skips feature
- [#3009](https://github.com/poanetwork/blockscout/pull/3009) - Fix broken export to CSV
- [#3007](https://github.com/poanetwork/blockscout/pull/3007) - Fix copy UTF8 tx input action - [#3007](https://github.com/poanetwork/blockscout/pull/3007) - Fix copy UTF8 tx input action
- [#2996](https://github.com/poanetwork/blockscout/pull/2996) - Fix awesomplete lib loading in Firefox - [#2996](https://github.com/poanetwork/blockscout/pull/2996) - Fix awesomplete lib loading in Firefox
- [#2993](https://github.com/poanetwork/blockscout/pull/2993) - Fix path definition for contract verification endpoint - [#2993](https://github.com/poanetwork/blockscout/pull/2993) - Fix path definition for contract verification endpoint
@ -26,6 +40,8 @@
- [#2883](https://github.com/poanetwork/blockscout/pull/2883) - Fix long contracts names - [#2883](https://github.com/poanetwork/blockscout/pull/2883) - Fix long contracts names
### Chore ### Chore
- [#3032](https://github.com/poanetwork/blockscout/pull/3032) - Remove indexing status alert for Ganache variant
- [#3030](https://github.com/poanetwork/blockscout/pull/3030) - Remove default websockets URL from config
- [#2995](https://github.com/poanetwork/blockscout/pull/2995) - Support API_PATH env var in Docker file - [#2995](https://github.com/poanetwork/blockscout/pull/2995) - Support API_PATH env var in Docker file

@ -61,6 +61,36 @@ describe('RECEIVED_NEW_BLOCK', () => {
expect(output.averageBlockTime).toEqual('5 seconds') expect(output.averageBlockTime).toEqual('5 seconds')
expect(output.blocks).toEqual([ expect(output.blocks).toEqual([
{ blockNumber: 2, chainBlockHtml: 'new block', averageBlockTime: '5 seconds' }, { blockNumber: 2, chainBlockHtml: 'new block', averageBlockTime: '5 seconds' },
{ blockNumber: 1, chainBlockHtml: 'test 1' },
{ blockNumber: 0, chainBlockHtml: 'test 0' }
])
})
test('receives new block if >= 4 blocks', () => {
const state = Object.assign({}, initialState, {
averageBlockTime: '6 seconds',
blocks: [
{ blockNumber: 3, chainBlockHtml: 'test 3' },
{ blockNumber: 2, chainBlockHtml: 'test 2' },
{ blockNumber: 1, chainBlockHtml: 'test 1' },
{ blockNumber: 0, chainBlockHtml: 'test 0' }
]
})
const action = {
type: 'RECEIVED_NEW_BLOCK',
msg: {
averageBlockTime: '5 seconds',
blockNumber: 4,
chainBlockHtml: 'new block'
}
}
const output = reducer(state, action)
expect(output.averageBlockTime).toEqual('5 seconds')
expect(output.blocks).toEqual([
{ blockNumber: 4, chainBlockHtml: 'new block', averageBlockTime: '5 seconds' },
{ blockNumber: 3, chainBlockHtml: 'test 3' },
{ blockNumber: 2, chainBlockHtml: 'test 2' },
{ blockNumber: 1, chainBlockHtml: 'test 1' } { blockNumber: 1, chainBlockHtml: 'test 1' }
]) ])
}) })
@ -318,7 +348,8 @@ describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
}) })
test('single transaction after large batch of transactions', () => { test('single transaction after large batch of transactions', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
transactionsBatch: [1,2,3,4,5,6,7,8,9,10,11] transactionsBatch: [6,7,8,9,10,11,12,13,14,15,16],
transactions: [1,2,3,4,5]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH', type: 'RECEIVED_NEW_TRANSACTION_BATCH',
@ -328,7 +359,7 @@ describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.transactions).toEqual([]) expect(output.transactions).toEqual([1,2,3,4,5])
expect(output.transactionsBatch.length).toEqual(12) expect(output.transactionsBatch.length).toEqual(12)
}) })
test('large batch of transactions after large batch of transactions', () => { test('large batch of transactions after large batch of transactions', () => {

@ -14,6 +14,7 @@ import listMorph from '../lib/list_morph'
import '../app' import '../app'
const BATCH_THRESHOLD = 6 const BATCH_THRESHOLD = 6
const BLOCKS_PER_PAGE = 4
export const initialState = { export const initialState = {
addressCount: null, addressCount: null,
@ -46,11 +47,17 @@ function baseReducer (state = initialState, action) {
} }
case 'RECEIVED_NEW_BLOCK': { case 'RECEIVED_NEW_BLOCK': {
if (!state.blocks.length || state.blocks[0].blockNumber < action.msg.blockNumber) { if (!state.blocks.length || state.blocks[0].blockNumber < action.msg.blockNumber) {
let pastBlocks
if (state.blocks.length < BLOCKS_PER_PAGE) {
pastBlocks = state.blocks
} else {
pastBlocks = state.blocks.slice(0, -1)
}
return Object.assign({}, state, { return Object.assign({}, state, {
averageBlockTime: action.msg.averageBlockTime, averageBlockTime: action.msg.averageBlockTime,
blocks: [ blocks: [
action.msg, action.msg,
...state.blocks.slice(0, -1) ...pastBlocks
], ],
blockCount: action.msg.blockNumber + 1 blockCount: action.msg.blockNumber + 1
}) })
@ -89,7 +96,16 @@ function baseReducer (state = initialState, action) {
return Object.assign({}, state, { transactionCount }) return Object.assign({}, state, { transactionCount })
} }
if (!state.transactionsBatch.length && action.msgs.length < BATCH_THRESHOLD) { const transactionsLength = state.transactions.length + action.msgs.length
if (transactionsLength < BATCH_THRESHOLD) {
return Object.assign({}, state, {
transactions: [
...action.msgs.reverse(),
...state.transactions
],
transactionCount
})
} else if (!state.transactionsBatch.length && action.msgs.length < BATCH_THRESHOLD) {
return Object.assign({}, state, { return Object.assign({}, state, {
transactions: [ transactions: [
...action.msgs.reverse(), ...action.msgs.reverse(),

@ -9,6 +9,7 @@ defmodule BlockScoutWeb.AddressCoinBalanceController do
alias BlockScoutWeb.AddressCoinBalanceView alias BlockScoutWeb.AddressCoinBalanceView
alias Explorer.{Chain, Market} alias Explorer.{Chain, Market}
alias Explorer.Chain.Address
alias Explorer.ExchangeRates.Token alias Explorer.ExchangeRates.Token
alias Indexer.Fetcher.CoinBalanceOnDemand alias Indexer.Fetcher.CoinBalanceOnDemand
alias Phoenix.View alias Phoenix.View
@ -64,7 +65,7 @@ defmodule BlockScoutWeb.AddressCoinBalanceController do
coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address), coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
current_path: current_path(conn), current_path: current_path(conn),
counters_path: address_path(conn, :address_counters, %{"id" => to_string(address_hash)}) counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)})
) )
else else
:error -> :error ->

@ -9,6 +9,7 @@ defmodule BlockScoutWeb.AddressReadContractController do
use BlockScoutWeb, :controller use BlockScoutWeb, :controller
alias Explorer.{Chain, Market} alias Explorer.{Chain, Market}
alias Explorer.Chain.Address
alias Explorer.ExchangeRates.Token alias Explorer.ExchangeRates.Token
alias Indexer.Fetcher.CoinBalanceOnDemand alias Indexer.Fetcher.CoinBalanceOnDemand
@ -32,7 +33,7 @@ defmodule BlockScoutWeb.AddressReadContractController do
address: address, address: address,
coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address), coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
counters_path: address_path(conn, :address_counters, %{"id" => to_string(address_hash)}) counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)})
) )
else else
_ -> _ ->

@ -5,6 +5,7 @@ defmodule BlockScoutWeb.AddressTokenController do
alias BlockScoutWeb.AddressTokenView alias BlockScoutWeb.AddressTokenView
alias Explorer.{Chain, Market} alias Explorer.{Chain, Market}
alias Explorer.Chain.Address
alias Explorer.ExchangeRates.Token alias Explorer.ExchangeRates.Token
alias Indexer.Fetcher.CoinBalanceOnDemand alias Indexer.Fetcher.CoinBalanceOnDemand
alias Phoenix.View alias Phoenix.View
@ -63,7 +64,7 @@ defmodule BlockScoutWeb.AddressTokenController do
current_path: current_path(conn), current_path: current_path(conn),
coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address), coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
counters_path: address_path(conn, :address_counters, %{"id" => to_string(address_hash)}) counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)})
) )
else else
:error -> :error ->

@ -4,6 +4,7 @@ defmodule BlockScoutWeb.AddressTokenTransferController do
alias BlockScoutWeb.TransactionView alias BlockScoutWeb.TransactionView
alias Explorer.ExchangeRates.Token alias Explorer.ExchangeRates.Token
alias Explorer.{Chain, Market} alias Explorer.{Chain, Market}
alias Explorer.Chain.Address
alias Indexer.Fetcher.CoinBalanceOnDemand alias Indexer.Fetcher.CoinBalanceOnDemand
alias Phoenix.View alias Phoenix.View
@ -96,7 +97,7 @@ defmodule BlockScoutWeb.AddressTokenTransferController do
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
current_path: current_path(conn), current_path: current_path(conn),
token: token, token: token,
counters_path: address_path(conn, :address_counters, %{"id" => to_string(address_hash)}) counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)})
) )
else else
:error -> :error ->
@ -178,7 +179,7 @@ defmodule BlockScoutWeb.AddressTokenTransferController do
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
filter: params["filter"], filter: params["filter"],
current_path: current_path(conn), current_path: current_path(conn),
counters_path: address_path(conn, :address_counters, %{"id" => to_string(address_hash)}) counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)})
) )
else else
:error -> :error ->

@ -35,7 +35,7 @@ defmodule BlockScoutWeb.TransactionRawTraceController do
if first_trace_exists do if first_trace_exists do
internal_transactions internal_transactions
else else
{:ok, first_trace_params} = response =
Chain.fetch_first_trace( Chain.fetch_first_trace(
[ [
%{ %{
@ -48,13 +48,22 @@ defmodule BlockScoutWeb.TransactionRawTraceController do
json_rpc_named_arguments json_rpc_named_arguments
) )
InternalTransactions.run_insert_only(first_trace_params, %{ case response do
timeout: :infinity, {:ok, first_trace_params} ->
timestamps: Import.timestamps(), InternalTransactions.run_insert_only(first_trace_params, %{
internal_transactions: %{params: first_trace_params} timeout: :infinity,
}) timestamps: Import.timestamps(),
internal_transactions: %{params: first_trace_params}
})
Chain.all_transaction_to_internal_transactions(hash) Chain.all_transaction_to_internal_transactions(hash)
{:error, _} ->
internal_transactions
:ignore ->
internal_transactions
end
end end
render( render(

@ -61,7 +61,7 @@
<section> <section>
<div class="d-flex justify-content-between align-items-baseline"> <div class="d-flex justify-content-between align-items-baseline">
<h3><%= gettext "Contract source code" %></h3> <h3><%= gettext "Contract source code" %></h3>
<button type="button" class="button button-secondary button-sm" id="button" data-toggle="tooltip" data-placement="top" data-clipboard-text="<%= @address.smart_contract.contract_source_code %>" aria-label="Copy Contract Source Code"> <button type="button" class="btn-line" id="button" data-toggle="tooltip" data-placement="top" data-clipboard-text="<%= @address.smart_contract.contract_source_code %>" aria-label="Copy Contract Source Code">
<%= gettext "Copy Source Code" %> <%= gettext "Copy Source Code" %>
</button> </button>
</div> </div>

@ -206,7 +206,7 @@ defmodule BlockScoutWeb.AddressView do
def smart_contract_verified?(%Address{smart_contract: nil}), do: false def smart_contract_verified?(%Address{smart_contract: nil}), do: false
def smart_contract_with_read_only_functions?(%Address{smart_contract: %SmartContract{}} = address) do def smart_contract_with_read_only_functions?(%Address{smart_contract: %SmartContract{}} = address) do
Enum.any?(address.smart_contract.abi, & &1["constant"]) Enum.any?(address.smart_contract.abi, &(&1["constant"] || &1["stateMutability"] == "view"))
end end
def smart_contract_with_read_only_functions?(%Address{smart_contract: nil}), do: false def smart_contract_with_read_only_functions?(%Address{smart_contract: nil}), do: false

@ -137,9 +137,14 @@ defmodule BlockScoutWeb.LayoutView do
def other_networks do def other_networks do
get_other_networks = get_other_networks =
if Application.get_env(:block_scout_web, :other_networks) do if Application.get_env(:block_scout_web, :other_networks) do
:block_scout_web try do
|> Application.get_env(:other_networks) :block_scout_web
|> Parser.parse!(%{keys: :atoms!}) |> Application.get_env(:other_networks)
|> Parser.parse!(%{keys: :atoms!})
rescue
_ ->
[]
end
else else
@default_other_networks @default_other_networks
end end

@ -47,7 +47,7 @@ defmodule BlockScoutWeb.Tokens.Instance.OverviewView do
def smart_contract_with_read_only_functions?( def smart_contract_with_read_only_functions?(
%Token{contract_address: %Address{smart_contract: %SmartContract{}}} = token %Token{contract_address: %Address{smart_contract: %SmartContract{}}} = token
) do ) do
Enum.any?(token.contract_address.smart_contract.abi, & &1["constant"]) Enum.any?(token.contract_address.smart_contract.abi, &(&1["constant"] || &1["stateMutability"] == "view"))
end end
def smart_contract_with_read_only_functions?(%Token{contract_address: %Address{smart_contract: nil}}), do: false def smart_contract_with_read_only_functions?(%Token{contract_address: %Address{smart_contract: nil}}), do: false

@ -43,7 +43,7 @@ defmodule BlockScoutWeb.Tokens.OverviewView do
def smart_contract_with_read_only_functions?( def smart_contract_with_read_only_functions?(
%Token{contract_address: %Address{smart_contract: %SmartContract{}}} = token %Token{contract_address: %Address{smart_contract: %SmartContract{}}} = token
) do ) do
Enum.any?(token.contract_address.smart_contract.abi, & &1["constant"]) Enum.any?(token.contract_address.smart_contract.abi, &(&1["constant"] || &1["stateMutability"] == "view"))
end end
def smart_contract_with_read_only_functions?(%Token{contract_address: %Address{smart_contract: nil}}), do: false def smart_contract_with_read_only_functions?(%Token{contract_address: %Address{smart_contract: nil}}), do: false

@ -115,6 +115,12 @@ defmodule BlockScoutWeb.LayoutViewTest do
} }
] ]
end end
test "get empty networks list if SUPPORTED_CHAINS is not parsed" do
Application.put_env(:block_scout_web, :other_networks, "not a valid json")
assert LayoutView.other_networks() == []
end
end end
describe "main_nets/1" do describe "main_nets/1" do

@ -2,7 +2,7 @@ defmodule EthereumJSONRPC.Parity do
@moduledoc """ @moduledoc """
Ethereum JSONRPC methods that are only supported by [Parity](https://wiki.parity.io/). Ethereum JSONRPC methods that are only supported by [Parity](https://wiki.parity.io/).
""" """
require Logger
import EthereumJSONRPC, only: [id_to_params: 1, integer_to_quantity: 1, json_rpc: 2, request: 1] import EthereumJSONRPC, only: [id_to_params: 1, integer_to_quantity: 1, json_rpc: 2, request: 1]
alias EthereumJSONRPC.Parity.{FetchedBeneficiaries, Traces} alias EthereumJSONRPC.Parity.{FetchedBeneficiaries, Traces}
@ -53,17 +53,33 @@ defmodule EthereumJSONRPC.Parity do
def fetch_first_trace(transactions_params, json_rpc_named_arguments) when is_list(transactions_params) do def fetch_first_trace(transactions_params, json_rpc_named_arguments) when is_list(transactions_params) do
id_to_params = id_to_params(transactions_params) id_to_params = id_to_params(transactions_params)
with {:ok, responses} <- trace_replay_transaction_response =
id_to_params id_to_params
|> trace_replay_transaction_requests() |> trace_replay_transaction_requests()
|> json_rpc(json_rpc_named_arguments) do |> json_rpc(json_rpc_named_arguments)
{:ok, [first_trace]} = trace_replay_transaction_responses_to_first_trace_params(responses, id_to_params)
case trace_replay_transaction_response do
%{block_hash: block_hash} = {:ok, responses} ->
transactions_params case trace_replay_transaction_responses_to_first_trace_params(responses, id_to_params) do
|> Enum.at(0) {:ok, [first_trace]} ->
%{block_hash: block_hash} =
{:ok, [%{first_trace: first_trace, block_hash: block_hash, json_rpc_named_arguments: json_rpc_named_arguments}]} transactions_params
|> Enum.at(0)
{:ok,
[%{first_trace: first_trace, block_hash: block_hash, json_rpc_named_arguments: json_rpc_named_arguments}]}
{:error, error} ->
Logger.error(inspect(error))
{:error, error}
end
{:error, :econnrefused} ->
{:error, :econnrefused}
{:error, [error]} ->
Logger.error(inspect(error))
{:error, error}
end end
end end

@ -20,9 +20,7 @@ config :explorer,
if(System.get_env("DISABLE_WEBAPP") != "true", if(System.get_env("DISABLE_WEBAPP") != "true",
do: Explorer.Chain.Events.SimpleSender, do: Explorer.Chain.Events.SimpleSender,
else: Explorer.Chain.Events.DBSender else: Explorer.Chain.Events.DBSender
), )
index_internal_transactions_for_token_transfers:
if(System.get_env("INTERNAL_TRANSACTIONOS_FOR_TOKEN_TRANSFERS") == "true", do: true, else: false)
average_block_period = average_block_period =
case Integer.parse(System.get_env("AVERAGE_BLOCK_CACHE_PERIOD", "")) do case Integer.parse(System.get_env("AVERAGE_BLOCK_CACHE_PERIOD", "")) do

@ -1,7 +1,12 @@
use Mix.Config use Mix.Config
database = if System.get_env("DATABASE_URL"), do: nil, else: "explorer_dev"
hostname = if System.get_env("DATABASE_URL"), do: nil, else: "localhost"
# Configure your database # Configure your database
config :explorer, Explorer.Repo, config :explorer, Explorer.Repo,
database: database,
hostname: hostname,
url: System.get_env("DATABASE_URL"), url: System.get_env("DATABASE_URL"),
pool_size: String.to_integer(System.get_env("POOL_SIZE", "50")), pool_size: String.to_integer(System.get_env("POOL_SIZE", "50")),
timeout: :timer.seconds(80) timeout: :timer.seconds(80)

@ -14,7 +14,7 @@ config :explorer,
transport: EthereumJSONRPC.WebSocket, transport: EthereumJSONRPC.WebSocket,
transport_options: [ transport_options: [
web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, web_socket: EthereumJSONRPC.WebSocket.WebSocketClient,
url: System.get_env("ETHEREUM_JSONRPC_WS_URL") || "ws://localhost:7545" url: System.get_env("ETHEREUM_JSONRPC_WS_URL")
], ],
variant: EthereumJSONRPC.Ganache variant: EthereumJSONRPC.Ganache
] ]

@ -14,7 +14,7 @@ config :explorer,
transport: EthereumJSONRPC.WebSocket, transport: EthereumJSONRPC.WebSocket,
transport_options: [ transport_options: [
web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, web_socket: EthereumJSONRPC.WebSocket.WebSocketClient,
url: System.get_env("ETHEREUM_JSONRPC_WS_URL") || "wss://mainnet.infura.io/8lTvJTKmHPCHazkneJsY/ws" url: System.get_env("ETHEREUM_JSONRPC_WS_URL")
], ],
variant: EthereumJSONRPC.Geth variant: EthereumJSONRPC.Geth
] ]

@ -19,7 +19,7 @@ config :explorer,
transport: EthereumJSONRPC.WebSocket, transport: EthereumJSONRPC.WebSocket,
transport_options: [ transport_options: [
web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, web_socket: EthereumJSONRPC.WebSocket.WebSocketClient,
url: System.get_env("ETHEREUM_JSONRPC_WS_URL") || "ws://localhost:8546" url: System.get_env("ETHEREUM_JSONRPC_WS_URL")
], ],
variant: EthereumJSONRPC.Parity variant: EthereumJSONRPC.Parity
] ]

@ -19,7 +19,7 @@ config :explorer,
transport: EthereumJSONRPC.WebSocket, transport: EthereumJSONRPC.WebSocket,
transport_options: [ transport_options: [
web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, web_socket: EthereumJSONRPC.WebSocket.WebSocketClient,
url: System.get_env("ETHEREUM_JSONRPC_WS_URL") || "ws://localhost:8546" url: System.get_env("ETHEREUM_JSONRPC_WS_URL")
], ],
variant: EthereumJSONRPC.RSK variant: EthereumJSONRPC.RSK
] ]

@ -5,7 +5,7 @@ config :explorer,
transport: EthereumJSONRPC.HTTP, transport: EthereumJSONRPC.HTTP,
transport_options: [ transport_options: [
http: EthereumJSONRPC.HTTP.HTTPoison, http: EthereumJSONRPC.HTTP.HTTPoison,
url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL") || "http://localhost:7545", url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL"),
http_options: [recv_timeout: :timer.minutes(1), timeout: :timer.minutes(1), hackney: [pool: :ethereum_jsonrpc]] http_options: [recv_timeout: :timer.minutes(1), timeout: :timer.minutes(1), hackney: [pool: :ethereum_jsonrpc]]
], ],
variant: EthereumJSONRPC.Ganache variant: EthereumJSONRPC.Ganache
@ -14,7 +14,7 @@ config :explorer,
transport: EthereumJSONRPC.WebSocket, transport: EthereumJSONRPC.WebSocket,
transport_options: [ transport_options: [
web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, web_socket: EthereumJSONRPC.WebSocket.WebSocketClient,
url: System.get_env("ETHEREUM_JSONRPC_WS_URL") || "ws://localhost:7545" url: System.get_env("ETHEREUM_JSONRPC_WS_URL")
], ],
variant: EthereumJSONRPC.Ganache variant: EthereumJSONRPC.Ganache
] ]

@ -5,7 +5,7 @@ config :explorer,
transport: EthereumJSONRPC.HTTP, transport: EthereumJSONRPC.HTTP,
transport_options: [ transport_options: [
http: EthereumJSONRPC.HTTP.HTTPoison, http: EthereumJSONRPC.HTTP.HTTPoison,
url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL") || "https://mainnet.infura.io/8lTvJTKmHPCHazkneJsY", url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL"),
http_options: [recv_timeout: :timer.minutes(1), timeout: :timer.minutes(1), hackney: [pool: :ethereum_jsonrpc]] http_options: [recv_timeout: :timer.minutes(1), timeout: :timer.minutes(1), hackney: [pool: :ethereum_jsonrpc]]
], ],
variant: EthereumJSONRPC.Geth variant: EthereumJSONRPC.Geth
@ -14,7 +14,7 @@ config :explorer,
transport: EthereumJSONRPC.WebSocket, transport: EthereumJSONRPC.WebSocket,
transport_options: [ transport_options: [
web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, web_socket: EthereumJSONRPC.WebSocket.WebSocketClient,
url: System.get_env("ETHEREUM_JSONRPC_WS_URL") || "wss://mainnet.infura.io/8lTvJTKmHPCHazkneJsY/ws" url: System.get_env("ETHEREUM_JSONRPC_WS_URL")
], ],
variant: EthereumJSONRPC.Geth variant: EthereumJSONRPC.Geth
] ]

@ -846,20 +846,27 @@ defmodule Explorer.Chain do
""" """
@spec finished_indexing?() :: boolean() @spec finished_indexing?() :: boolean()
def finished_indexing? do def finished_indexing? do
with {:transactions_exist, true} <- {:transactions_exist, Repo.exists?(Transaction)}, json_rpc_named_arguments = Application.fetch_env!(:indexer, :json_rpc_named_arguments)
min_block_number when not is_nil(min_block_number) <- Repo.aggregate(Transaction, :min, :block_number) do variant = Keyword.fetch!(json_rpc_named_arguments, :variant)
query =
from(
b in Block,
join: pending_ops in assoc(b, :pending_operations),
where: pending_ops.fetch_internal_transactions,
where: b.consensus and b.number == ^min_block_number
)
!Repo.exists?(query) if variant == EthereumJSONRPC.Ganache do
true
else else
{:transactions_exist, false} -> true with {:transactions_exist, true} <- {:transactions_exist, Repo.exists?(Transaction)},
nil -> false min_block_number when not is_nil(min_block_number) <- Repo.aggregate(Transaction, :min, :block_number) do
query =
from(
b in Block,
join: pending_ops in assoc(b, :pending_operations),
where: pending_ops.fetch_internal_transactions,
where: b.consensus and b.number == ^min_block_number
)
!Repo.exists?(query)
else
{:transactions_exist, false} -> true
nil -> false
end
end end
end end
@ -4055,14 +4062,31 @@ defmodule Explorer.Chain do
defp boolean_to_check_result(false), do: :not_found defp boolean_to_check_result(false), do: :not_found
def extract_db_name(db_url) do
if db_url == nil do
""
else
db_url
|> String.split("/")
|> Enum.take(-1)
|> Enum.at(0)
end
end
@doc """ @doc """
Fetches the first trace from the Parity trace URL. Fetches the first trace from the Parity trace URL.
""" """
def fetch_first_trace(transactions_params, json_rpc_named_arguments) do def fetch_first_trace(transactions_params, json_rpc_named_arguments) do
{:ok, [%{first_trace: first_trace, block_hash: block_hash, json_rpc_named_arguments: json_rpc_named_arguments}]} = case EthereumJSONRPC.fetch_first_trace(transactions_params, json_rpc_named_arguments) do
EthereumJSONRPC.fetch_first_trace(transactions_params, json_rpc_named_arguments) {:ok, [%{first_trace: first_trace, block_hash: block_hash, json_rpc_named_arguments: json_rpc_named_arguments}]} ->
format_tx_first_trace(first_trace, block_hash, json_rpc_named_arguments)
format_tx_first_trace(first_trace, block_hash, json_rpc_named_arguments) {:error, error} ->
{:error, error}
:ignore ->
:ignore
end
end end
defp format_tx_first_trace(first_trace, block_hash, json_rpc_named_arguments) do defp format_tx_first_trace(first_trace, block_hash, json_rpc_named_arguments) do

@ -145,7 +145,6 @@ defmodule Explorer.Chain.Address do
end end
end end
# https://github.com/rsksmart/RSKIPs/blob/master/IPs/RSKIP60.md
def eth_checksum(hash) do def eth_checksum(hash) do
string_hash = string_hash =
hash hash
@ -169,6 +168,7 @@ defmodule Explorer.Chain.Address do
end) end)
end end
# https://github.com/rsksmart/RSKIPs/blob/master/IPs/RSKIP60.md
def rsk_checksum(hash) do def rsk_checksum(hash) do
chain_id = NetVersion.get_version() chain_id = NetVersion.get_version()

@ -6,15 +6,22 @@ defmodule Explorer.Chain.Events.Listener do
use GenServer use GenServer
alias Postgrex.Notifications alias Postgrex.Notifications
import Explorer.Chain, only: [extract_db_name: 1]
def start_link(_) do def start_link(_) do
GenServer.start_link(__MODULE__, "chain_event", name: __MODULE__) GenServer.start_link(__MODULE__, "chain_event", name: __MODULE__)
end end
def init(channel) do def init(channel) do
{:ok, pid} = explorer_repo =
:explorer :explorer
|> Application.get_env(Explorer.Repo) |> Application.get_env(Explorer.Repo)
db_url = explorer_repo[:url]
{:ok, pid} =
explorer_repo
|> Keyword.put(:database, extract_db_name(db_url))
|> Notifications.start_link() |> Notifications.start_link()
ref = Notifications.listen!(pid, channel) ref = Notifications.listen!(pid, channel)

@ -65,19 +65,40 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do
|> Multi.run(:invalid_block_numbers, fn _, %{acquire_transactions: transactions} -> |> Multi.run(:invalid_block_numbers, fn _, %{acquire_transactions: transactions} ->
invalid_block_numbers(transactions, internal_transactions_params) invalid_block_numbers(transactions, internal_transactions_params)
end) end)
|> Multi.run(:valid_internal_transactions_without_first_traces_of_trivial_transactions, fn _,
%{
acquire_transactions:
transactions,
invalid_block_numbers:
invalid_block_numbers
} ->
valid_internal_transactions_without_first_trace(
transactions,
internal_transactions_params,
invalid_block_numbers
)
end)
|> Multi.run(:valid_internal_transactions, fn _, |> Multi.run(:valid_internal_transactions, fn _,
%{ %{
acquire_transactions: transactions, acquire_transactions: transactions,
invalid_block_numbers: invalid_block_numbers invalid_block_numbers: invalid_block_numbers
} -> } ->
valid_internal_transactions(transactions, internal_transactions_params, invalid_block_numbers) valid_internal_transactions(
transactions,
internal_transactions_params,
invalid_block_numbers
)
end) end)
|> Multi.run(:remove_left_over_internal_transactions, fn repo, |> Multi.run(:remove_left_over_internal_transactions, fn repo,
%{valid_internal_transactions: valid_internal_transactions} -> %{valid_internal_transactions: valid_internal_transactions} ->
remove_left_over_internal_transactions(repo, valid_internal_transactions) remove_left_over_internal_transactions(repo, valid_internal_transactions)
end) end)
|> Multi.run(:internal_transactions, fn repo, %{valid_internal_transactions: valid_internal_transactions} -> |> Multi.run(:internal_transactions, fn repo,
insert(repo, valid_internal_transactions, insert_options) %{
valid_internal_transactions_without_first_traces_of_trivial_transactions:
valid_internal_transactions_without_first_traces_of_trivial_transactions
} ->
insert(repo, valid_internal_transactions_without_first_traces_of_trivial_transactions, insert_options)
end) end)
|> Multi.run(:update_transactions, fn repo, %{valid_internal_transactions: valid_internal_transactions} -> |> Multi.run(:update_transactions, fn repo, %{valid_internal_transactions: valid_internal_transactions} ->
update_transactions(repo, valid_internal_transactions, update_transactions_options) update_transactions(repo, valid_internal_transactions, update_transactions_options)
@ -291,6 +312,31 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do
{:ok, valid_internal_txs} {:ok, valid_internal_txs}
end end
defp valid_internal_transactions_without_first_trace(
transactions,
internal_transactions_params,
invalid_block_numbers
) do
with {:ok, valid_internal_txs} <-
valid_internal_transactions(transactions, internal_transactions_params, invalid_block_numbers) do
json_rpc_named_arguments = Application.fetch_env!(:indexer, :json_rpc_named_arguments)
variant = Keyword.fetch!(json_rpc_named_arguments, :variant)
# we exclude first traces from storing in the DB only in case of Parity variant (Parity/Nethermind). todo: to the same for Geth
if variant == EthereumJSONRPC.Parity do
valid_internal_txs_without_first_trace =
valid_internal_txs
|> Enum.reject(fn trace ->
trace[:index] == 0
end)
{:ok, valid_internal_txs_without_first_trace}
else
{:ok, valid_internal_txs}
end
end
end
def defer_internal_transactions_primary_key(repo) do def defer_internal_transactions_primary_key(repo) do
# Allows internal_transactions primary key to not be checked during the # Allows internal_transactions primary key to not be checked during the
# DB transactions and instead be checked only at the end of it. # DB transactions and instead be checked only at the end of it.
@ -317,7 +363,7 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do
or_where(acc, [it], it.block_hash == ^block_hash and it.block_index > ^max_index) or_where(acc, [it], it.block_hash == ^block_hash and it.block_index > ^max_index)
end) end)
# removes old recoreds with the same primary key (transaction hash, transaction index) # removes old records with the same primary key (transaction hash, transaction index)
delete_query = delete_query =
valid_internal_transactions valid_internal_transactions
|> Enum.map(fn params -> {params.transaction_hash, params.index} end) |> Enum.map(fn params -> {params.transaction_hash, params.index} end)
@ -338,49 +384,72 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do
defp update_transactions(repo, valid_internal_transactions, %{ defp update_transactions(repo, valid_internal_transactions, %{
timeout: timeout, timeout: timeout,
timestamps: timestamps timestamps: timestamps
}) }) do
when is_list(valid_internal_transactions) do valid_internal_transactions_count = Enum.count(valid_internal_transactions)
transaction_hashes =
valid_internal_transactions if valid_internal_transactions_count == 0 do
|> MapSet.new(& &1.transaction_hash) {:ok, nil}
|> MapSet.to_list() else
params =
update_query = valid_internal_transactions
from( |> Enum.filter(fn internal_tx ->
t in Transaction, internal_tx[:index] == 0
where: t.hash in ^transaction_hashes, end)
# ShareLocks order already enforced by `acquire_transactions` (see docs: sharelocks.md) |> Enum.map(fn trace ->
update: [ %{
set: [ transaction_hash: Map.get(trace, :transaction_hash),
created_contract_address_hash: created_contract_address_hash: Map.get(trace, :created_contract_address_hash),
fragment( error: Map.get(trace, :error),
"(SELECT it.created_contract_address_hash FROM internal_transactions AS it WHERE it.transaction_hash = ? ORDER BY it.index ASC LIMIT 1)", status: if(is_nil(Map.get(trace, :error)), do: :ok, else: :error)
t.hash }
), end)
error: |> Enum.filter(fn transaction_hash -> transaction_hash != nil end)
fragment(
"(SELECT it.error FROM internal_transactions AS it WHERE it.transaction_hash = ? ORDER BY it.index ASC LIMIT 1)", transaction_hashes =
t.hash valid_internal_transactions
), |> MapSet.new(& &1.transaction_hash)
status: |> MapSet.to_list()
fragment(
"CASE WHEN (SELECT it.error FROM internal_transactions AS it WHERE it.transaction_hash = ? ORDER BY it.index ASC LIMIT 1) IS NULL THEN ? ELSE ? END", result =
t.hash, Enum.reduce_while(params, 0, fn first_trace, transaction_hashes_iterator ->
type(^:ok, t.status), update_query =
type(^:error, t.status) from(
), t in Transaction,
updated_at: ^timestamps.updated_at where: t.hash == ^first_trace.transaction_hash,
] # ShareLocks order already enforced by `acquire_transactions` (see docs: sharelocks.md)
] update: [
) set: [
created_contract_address_hash: ^first_trace.created_contract_address_hash,
error: ^first_trace.error,
status: ^first_trace.status,
updated_at: ^timestamps.updated_at
]
]
)
transaction_hashes_iterator = transaction_hashes_iterator + 1
try do
{_transaction_count, result} = repo.update_all(update_query, [], timeout: timeout)
if valid_internal_transactions_count == transaction_hashes_iterator do
{:halt, result}
else
{:cont, transaction_hashes_iterator}
end
rescue
postgrex_error in Postgrex.Error ->
{:halt, %{exception: postgrex_error, transaction_hashes: transaction_hashes}}
end
end)
try do case result do
{_transaction_count, result} = repo.update_all(update_query, [], timeout: timeout) %{exception: _} ->
{:error, result}
{:ok, result} _ ->
rescue {:ok, result}
postgrex_error in Postgrex.Error -> end
{:error, %{exception: postgrex_error, transaction_hashes: transaction_hashes}}
end end
end end

@ -175,7 +175,7 @@ defmodule Explorer.SmartContract.Reader do
_ -> _ ->
abi abi
|> Enum.filter(& &1["constant"]) |> Enum.filter(&(&1["constant"] || &1["stateMutability"] == "view"))
|> Enum.map(&fetch_current_value_from_blockchain(&1, abi, contract_address_hash)) |> Enum.map(&fetch_current_value_from_blockchain(&1, abi, contract_address_hash))
end end
end end

@ -22,6 +22,133 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsTest do
assert :error == Repo.get(Transaction, transaction.hash).status assert :error == Repo.get(Transaction, transaction.hash).status
end end
test "simple coin transfer's status becomes :error when its internal_transaction has an error" do
transaction = insert(:transaction) |> with_block(status: :ok)
insert(:pending_block_operation, block_hash: transaction.block_hash, fetch_internal_transactions: true)
assert :ok == transaction.status
index = 0
error = "Out of gas"
internal_transaction_changes =
make_internal_transaction_changes_for_simple_coin_transfers(transaction, index, error)
assert {:ok, _} = run_internal_transactions([internal_transaction_changes])
assert :error == Repo.get(Transaction, transaction.hash).status
end
test "for block with 2 simple coin transfer's statuses become :error when its both internal_transactions has an error" do
a_block = insert(:block, number: 1000)
transaction1 = insert(:transaction) |> with_block(a_block, status: :ok)
transaction2 = insert(:transaction) |> with_block(a_block, status: :ok)
insert(:pending_block_operation, block_hash: a_block.hash, fetch_internal_transactions: true)
assert :ok == transaction1.status
assert :ok == transaction2.status
index = 0
error = "Out of gas"
internal_transaction_changes_1 =
make_internal_transaction_changes_for_simple_coin_transfers(transaction1, index, error)
internal_transaction_changes_2 =
make_internal_transaction_changes_for_simple_coin_transfers(transaction2, index, error)
assert {:ok, _} = run_internal_transactions([internal_transaction_changes_1, internal_transaction_changes_2])
assert :error == Repo.get(Transaction, transaction1.hash).status
assert :error == Repo.get(Transaction, transaction2.hash).status
end
test "for block with 2 simple coin transfer's only status become :error for tx where internal_transactions has an error" do
a_block = insert(:block, number: 1000)
transaction1 = insert(:transaction) |> with_block(a_block, status: :ok)
transaction2 = insert(:transaction) |> with_block(a_block, status: :ok)
insert(:pending_block_operation, block_hash: a_block.hash, fetch_internal_transactions: true)
assert :ok == transaction1.status
assert :ok == transaction2.status
index = 0
error = "Out of gas"
internal_transaction_changes_1 =
make_internal_transaction_changes_for_simple_coin_transfers(transaction1, index, error)
internal_transaction_changes_2 =
make_internal_transaction_changes_for_simple_coin_transfers(transaction2, index, nil)
assert {:ok, _} = run_internal_transactions([internal_transaction_changes_1, internal_transaction_changes_2])
assert :error == Repo.get(Transaction, transaction1.hash).status
assert :ok == Repo.get(Transaction, transaction2.hash).status
end
test "for block with simple coin transfer and method calls, method calls internal txs have correct block_index" do
a_block = insert(:block, number: 1000)
transaction0 = insert(:transaction) |> with_block(a_block, status: :ok)
transaction1 = insert(:transaction) |> with_block(a_block, status: :ok)
transaction2 = insert(:transaction) |> with_block(a_block, status: :ok)
insert(:pending_block_operation, block_hash: a_block.hash, fetch_internal_transactions: true)
assert :ok == transaction0.status
assert :ok == transaction1.status
assert :ok == transaction2.status
index = 0
internal_transaction_changes_0 = make_internal_transaction_changes(transaction0, index, nil)
internal_transaction_changes_0_1 = make_internal_transaction_changes(transaction0, 1, nil)
internal_transaction_changes_1 =
make_internal_transaction_changes_for_simple_coin_transfers(transaction1, index, nil)
internal_transaction_changes_2 = make_internal_transaction_changes(transaction2, index, nil)
internal_transaction_changes_2_1 = make_internal_transaction_changes(transaction2, 1, nil)
assert {:ok, _} =
run_internal_transactions([
internal_transaction_changes_0,
internal_transaction_changes_0_1,
internal_transaction_changes_1,
internal_transaction_changes_2,
internal_transaction_changes_2_1
])
assert from(i in InternalTransaction, where: i.transaction_hash == ^transaction0.hash, where: i.index == 0)
|> Repo.one()
|> is_nil()
assert 1 == Repo.get_by!(InternalTransaction, transaction_hash: transaction0.hash, index: 1).block_index
assert from(i in InternalTransaction, where: i.transaction_hash == ^transaction1.hash) |> Repo.one() |> is_nil()
assert from(i in InternalTransaction, where: i.transaction_hash == ^transaction2.hash, where: i.index == 0)
|> Repo.one()
|> is_nil()
assert 4 == Repo.get_by!(InternalTransaction, transaction_hash: transaction2.hash, index: 1).block_index
end
test "simple coin transfer has no internal transaction inserted" do
transaction = insert(:transaction) |> with_block(status: :ok)
insert(:pending_block_operation, block_hash: transaction.block_hash, fetch_internal_transactions: true)
assert :ok == transaction.status
index = 0
internal_transaction_changes =
make_internal_transaction_changes_for_simple_coin_transfers(transaction, index, nil)
assert {:ok, _} = run_internal_transactions([internal_transaction_changes])
assert !Repo.exists?(from(i in InternalTransaction, where: i.transaction_hash == ^transaction.hash))
end
test "pending transactions don't get updated not its internal_transactions inserted" do test "pending transactions don't get updated not its internal_transactions inserted" do
transaction = insert(:transaction) |> with_block(status: :ok) transaction = insert(:transaction) |> with_block(status: :ok)
pending = insert(:transaction) pending = insert(:transaction)
@ -31,7 +158,7 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsTest do
assert :ok == transaction.status assert :ok == transaction.status
assert is_nil(pending.block_hash) assert is_nil(pending.block_hash)
index = 0 index = 1
transaction_changes = make_internal_transaction_changes(transaction, index, nil) transaction_changes = make_internal_transaction_changes(transaction, index, nil)
pending_changes = make_internal_transaction_changes(pending, index, nil) pending_changes = make_internal_transaction_changes(pending, index, nil)
@ -62,7 +189,7 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsTest do
assert full_block.hash == inserted.block_hash assert full_block.hash == inserted.block_hash
index = 0 index = 1
pending_transaction_changes = pending_transaction_changes =
pending pending
@ -142,17 +269,23 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsTest do
assert full_block.hash == inserted.block_hash assert full_block.hash == inserted.block_hash
index = 0 transaction_changes = make_internal_transaction_changes(inserted, 0, nil)
transaction_changes_2 = make_internal_transaction_changes(inserted, 1, nil)
transaction_changes = make_internal_transaction_changes(inserted, index, nil)
empty_changes = make_empty_block_changes(empty_block.number) empty_changes = make_empty_block_changes(empty_block.number)
assert {:ok, _} = run_internal_transactions([empty_changes, transaction_changes]) assert {:ok, _} = run_internal_transactions([empty_changes, transaction_changes, transaction_changes_2])
assert %{consensus: true} = Repo.get(Block, empty_block.hash) assert %{consensus: true} = Repo.get(Block, empty_block.hash)
assert PendingBlockOperation |> Repo.get(empty_block.hash) |> is_nil() assert PendingBlockOperation |> Repo.get(empty_block.hash) |> is_nil()
assert from(i in InternalTransaction, where: i.transaction_hash == ^inserted.hash) |> Repo.one() |> is_nil() == assert from(i in InternalTransaction, where: i.transaction_hash == ^inserted.hash, where: i.index == 0)
|> Repo.one()
|> is_nil() ==
true
assert from(i in InternalTransaction, where: i.transaction_hash == ^inserted.hash, where: i.index == 1)
|> Repo.one()
|> is_nil() ==
false false
assert %{consensus: true} = Repo.get(Block, full_block.hash) assert %{consensus: true} = Repo.get(Block, full_block.hash)
@ -199,4 +332,28 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsTest do
block_number: transaction.block_number block_number: transaction.block_number
} }
end end
defp make_internal_transaction_changes_for_simple_coin_transfers(transaction, index, error) do
%{
from_address_hash: insert(:address).hash,
to_address_hash: insert(:address).hash,
call_type: :call,
gas: 0,
gas_used: nil,
input: %Data{bytes: <<>>},
output:
if is_nil(error) do
%Data{bytes: <<0>>}
else
nil
end,
index: index,
trace_address: [],
transaction_hash: transaction.hash,
type: :call,
value: Wei.from(Decimal.new(1), :wei),
error: error,
block_number: transaction.block_number
}
end
end end

@ -61,7 +61,7 @@ defmodule Explorer.Chain.ImportTest do
to_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", to_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
gas: 4_677_320, gas: 4_677_320,
gas_used: 27770, gas_used: 27770,
input: "0x", input: "0x10855269000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef",
output: "0x", output: "0x",
value: 0 value: 0
}, },
@ -77,7 +77,7 @@ defmodule Explorer.Chain.ImportTest do
to_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", to_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
gas: 4_677_320, gas: 4_677_320,
gas_used: 27770, gas_used: 27770,
input: "0x", input: "0x10855269000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef",
output: "0x", output: "0x",
value: 0 value: 0
} }
@ -252,15 +252,6 @@ defmodule Explorer.Chain.ImportTest do
} }
], ],
internal_transactions: [ internal_transactions: [
%{
index: 0,
transaction_hash: %Hash{
byte_count: 32,
bytes:
<<83, 189, 136, 72, 114, 222, 62, 72, 134, 146, 136, 27, 174, 236, 38, 46, 123, 149, 35, 77, 57,
101, 36, 140, 57, 254, 153, 47, 255, 212, 51, 229>>
}
},
%{ %{
index: 1, index: 1,
transaction_hash: %Hash{ transaction_hash: %Hash{
@ -485,8 +476,7 @@ defmodule Explorer.Chain.ImportTest do
Subscriber.to(:internal_transactions, :realtime) Subscriber.to(:internal_transactions, :realtime)
Import.all(@import_data) Import.all(@import_data)
assert_receive {:chain_event, :internal_transactions, :realtime, assert_receive {:chain_event, :internal_transactions, :realtime, [%{transaction_hash: _, index: _}]}
[%{transaction_hash: _, index: _}, %{transaction_hash: _, index: _}]}
end end
test "publishes transactions data to subscribers on insert" do test "publishes transactions data to subscribers on insert" do
@ -612,7 +602,8 @@ defmodule Explorer.Chain.ImportTest do
gas_used: 269_607, gas_used: 269_607,
hash: transaction_hash, hash: transaction_hash,
index: 0, index: 0,
input: "0x", input:
"0x6060604052341561000f57600080fd5b7fb94ae47ec9f4248692e2ecf9740b67ab493f3dcc8452bedc7d9cd911c28d1ca5426040518082815260200191505060405180910390a1609e806100546000396000f300606060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063557ed1ba146044575b600080fd5b3415604e57600080fd5b6054606a565b6040518082815260200191505060405180910390f35b6000429050905600a165627a7a7230582053883c0c39da080adc15a91094921659c200b3bb60aed9e49b79b0274da3f4010029",
nonce: 0, nonce: 0,
r: 0, r: 0,
s: 0, s: 0,
@ -639,6 +630,96 @@ defmodule Explorer.Chain.ImportTest do
to_address_hash: to_address_hash, to_address_hash: to_address_hash,
gas: 4_677_320, gas: 4_677_320,
gas_used: 27770, gas_used: 27770,
input:
"0x6060604052341561000f57600080fd5b7fb94ae47ec9f4248692e2ecf9740b67ab493f3dcc8452bedc7d9cd911c28d1ca5426040518082815260200191505060405180910390a1609e806100546000396000f300606060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063557ed1ba146044575b600080fd5b3415604e57600080fd5b6054606a565b6040518082815260200191505060405180910390f35b6000429050905600a165627a7a7230582053883c0c39da080adc15a91094921659c200b3bb60aed9e49b79b0274da3f4010029",
output: "0x",
value: 0
}
],
with: :blockless_changeset
}
}
assert {:ok, _} = Import.all(options)
assert [block_hash] = Explorer.Repo.all(PendingBlockOperation.block_hashes(:fetch_internal_transactions))
assert {:ok, _} = Import.all(internal_txs_options)
assert [] == Explorer.Repo.all(PendingBlockOperation.block_hashes(:fetch_internal_transactions))
end
test "blocks with simple coin transfers updates PendingBlockOperation status" do
block_hash = "0xe69314b702f403e00ec89cd63d5870182ed334d9d461bd042cdd6609fd6b7c17"
block_number = 13_255_416
miner_hash = from_address_hash = "0xe2ac1c6843A33f81aE4935E5EF1277a392990381"
to_address_hash = "0xDC1772b72d828ea9b7D4ded22dbef0f082578B8B"
transaction_hash = "0x8f20a5b3b79199db857323af7a5077d2dae7867368fef6f4e8cd8b14b88a9c6a"
options = %{
addresses: %{
params: [
%{hash: from_address_hash},
%{hash: to_address_hash}
]
},
blocks: %{
params: [
%{
consensus: true,
difficulty: 340_282_366_920_938_463_463_374_607_400_000_000_000,
gas_limit: 9_999_991,
gas_used: 21_000,
hash: block_hash,
miner_hash: miner_hash,
nonce: 0,
number: block_number,
parent_hash: "0x4c4505527d99e07dbcd6274ffaac629099ae6d4e1f2bc2bd3cc48d3f5a511d6f",
size: 703,
timestamp: Timex.parse!("2020-02-12 10:48:21Z", "{ISO:Extended:Z}"),
total_difficulty: 4_510_584_331_001_678_443_607_831_185_000_000_000_000_000_000
}
]
},
transactions: %{
params: [
%{
block_hash: block_hash,
block_number: block_number,
cumulative_gas_used: 21_000,
from_address_hash: from_address_hash,
gas: 21_000,
gas_price: 1,
gas_used: 21_000,
hash: transaction_hash,
index: 0,
input: "0x",
nonce: 0,
r: 0,
s: 0,
status: :ok,
v: 0,
value: 0
}
]
}
}
internal_txs_options = %{
internal_transactions: %{
params: [
%{
block_number: block_number,
transaction_index: 0,
transaction_hash: transaction_hash,
index: 0,
trace_address: [],
type: "call",
call_type: "call",
from_address_hash: from_address_hash,
to_address_hash: to_address_hash,
gas: 21_000,
gas_used: 21000,
input: "0x", input: "0x",
output: "0x", output: "0x",
value: 0 value: 0
@ -1709,7 +1790,7 @@ defmodule Explorer.Chain.ImportTest do
end end
# https://github.com/poanetwork/blockscout/issues/868 regression test # https://github.com/poanetwork/blockscout/issues/868 regression test
test "errored transactions can be forked" do test "errored simple coin transfer can be forked" do
block_number = 0 block_number = 0
miner_hash_before = address_hash() miner_hash_before = address_hash()
@ -1771,6 +1852,125 @@ defmodule Explorer.Chain.ImportTest do
status: :error status: :error
} }
] ]
}
})
%Block{consensus: true, number: ^block_number} = Repo.get(Block, block_hash_before)
transaction_before = Repo.get!(Transaction, transaction_hash)
refute transaction_before.block_hash == nil
refute transaction_before.block_number == nil
refute transaction_before.gas_used == nil
refute transaction_before.cumulative_gas_used == nil
refute transaction_before.index == nil
refute transaction_before.status == nil
miner_hash_after = address_hash()
from_address_hash_after = address_hash()
block_hash_after = block_hash()
assert {:ok, _} =
Import.all(%{
addresses: %{
params: [
%{hash: miner_hash_after},
%{hash: from_address_hash_after}
]
},
blocks: %{
params: [
%{
consensus: true,
difficulty: 1,
gas_limit: 1,
gas_used: 1,
hash: block_hash_after,
miner_hash: miner_hash_after,
nonce: 1,
number: block_number,
parent_hash: block_hash(),
size: 1,
timestamp: Timex.parse!("2019-01-01T02:00:00Z", "{ISO:Extended:Z}"),
total_difficulty: 1
}
]
}
})
transaction_after = Repo.get!(Transaction, transaction_hash)
assert transaction_after.block_hash == nil
assert transaction_after.block_number == nil
assert transaction_after.gas_used == nil
assert transaction_after.cumulative_gas_used == nil
assert transaction_after.index == nil
assert transaction_after.error == nil
assert transaction_after.status == nil
end
# https://github.com/poanetwork/blockscout/issues/868 regression test
test "errored other transactions can be forked" do
block_number = 0
miner_hash_before = address_hash()
from_address_hash_before = address_hash()
to_address_hash_before = address_hash()
block_hash_before = block_hash()
index_before = 0
error = "Reverted"
transaction_hash = transaction_hash()
assert {:ok, _} =
Import.all(%{
addresses: %{
params: [
%{hash: miner_hash_before},
%{hash: from_address_hash_before},
%{hash: to_address_hash_before}
]
},
blocks: %{
params: [
%{
consensus: true,
difficulty: 0,
gas_limit: 0,
gas_used: 0,
hash: block_hash_before,
miner_hash: miner_hash_before,
nonce: 0,
number: block_number,
parent_hash: block_hash(),
size: 0,
timestamp: Timex.parse!("2019-01-01T01:00:00Z", "{ISO:Extended:Z}"),
total_difficulty: 0
}
]
},
transactions: %{
params: [
%{
block_hash: block_hash_before,
block_number: block_number,
error: error,
from_address_hash: from_address_hash_before,
to_address_hash: to_address_hash_before,
gas: 666_000,
gas_price: 1,
gas_used: 555_000,
cumulative_gas_used: 555_000,
hash: transaction_hash,
index: index_before,
input: "0x0102",
nonce: 0,
r: 0,
s: 0,
v: 0,
value: 0,
status: :error
}
]
}, },
internal_transactions: %{ internal_transactions: %{
params: [ params: [
@ -1784,7 +1984,7 @@ defmodule Explorer.Chain.ImportTest do
to_address_hash: to_address_hash_before, to_address_hash: to_address_hash_before,
trace_address: [], trace_address: [],
value: 0, value: 0,
input: "0x", input: "0x0102",
error: error, error: error,
block_number: block_number, block_number: block_number,
transaction_index: 0 transaction_index: 0

@ -1397,7 +1397,7 @@ defmodule Explorer.ChainTest do
to_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", to_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
gas: 4_677_320, gas: 4_677_320,
gas_used: 27770, gas_used: 27770,
input: "0x", input: "0x10855269000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef",
output: "0x", output: "0x",
value: 0 value: 0
} }
@ -1566,17 +1566,7 @@ defmodule Explorer.ChainTest do
updated_at: %{} updated_at: %{}
} }
], ],
internal_transactions: [ internal_transactions: [],
%{
index: 0,
transaction_hash: %Hash{
byte_count: 32,
bytes:
<<83, 189, 136, 72, 114, 222, 62, 72, 134, 146, 136, 27, 174, 236, 38, 46, 123, 149, 35, 77, 57,
101, 36, 140, 57, 254, 153, 47, 255, 212, 51, 229>>
}
}
],
logs: [ logs: [
%Log{ %Log{
address_hash: %Hash{ address_hash: %Hash{
@ -4880,6 +4870,23 @@ defmodule Explorer.ChainTest do
end end
end end
describe "extract_db_name/1" do
test "extracts correct db name" do
db_url = "postgresql://viktor:@localhost:5432/blockscout-dev-1"
assert Chain.extract_db_name(db_url) == "blockscout-dev-1"
end
test "returns empty db name" do
db_url = ""
assert Chain.extract_db_name(db_url) == ""
end
test "returns nil db name" do
db_url = nil
assert Chain.extract_db_name(db_url) == ""
end
end
describe "fetch_first_trace/2" do describe "fetch_first_trace/2" do
test "fetched first trace", %{ test "fetched first trace", %{
json_rpc_named_arguments: json_rpc_named_arguments json_rpc_named_arguments: json_rpc_named_arguments

@ -16,9 +16,9 @@ config :indexer,
variant: EthereumJSONRPC.Ganache variant: EthereumJSONRPC.Ganache
], ],
subscribe_named_arguments: [ subscribe_named_arguments: [
transport: EthereumJSONRPC.WebSocket, transport: System.get_env("ETHEREUM_JSONRPC_WS_URL") && EthereumJSONRPC.WebSocket,
transport_options: [ transport_options: [
web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, web_socket: EthereumJSONRPC.WebSocket.WebSocketClient,
url: System.get_env("ETHEREUM_JSONRPC_WS_URL") || "ws://localhost:7545" url: System.get_env("ETHEREUM_JSONRPC_WS_URL")
] ]
] ]

@ -19,6 +19,6 @@ config :indexer,
transport: EthereumJSONRPC.WebSocket, transport: EthereumJSONRPC.WebSocket,
transport_options: [ transport_options: [
web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, web_socket: EthereumJSONRPC.WebSocket.WebSocketClient,
url: System.get_env("ETHEREUM_JSONRPC_WS_URL") || "wss://mainnet.infura.io/ws/8lTvJTKmHPCHazkneJsY" url: System.get_env("ETHEREUM_JSONRPC_WS_URL")
] ]
] ]

@ -42,6 +42,6 @@ config :indexer,
transport: System.get_env("ETHEREUM_JSONRPC_WS_URL") && EthereumJSONRPC.WebSocket, transport: System.get_env("ETHEREUM_JSONRPC_WS_URL") && EthereumJSONRPC.WebSocket,
transport_options: [ transport_options: [
web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, web_socket: EthereumJSONRPC.WebSocket.WebSocketClient,
url: System.get_env("ETHEREUM_JSONRPC_WS_URL") || "ws://localhost:8546" url: System.get_env("ETHEREUM_JSONRPC_WS_URL")
] ]
] ]

@ -26,6 +26,6 @@ config :indexer,
transport: System.get_env("ETHEREUM_JSONRPC_WS_URL") && EthereumJSONRPC.WebSocket, transport: System.get_env("ETHEREUM_JSONRPC_WS_URL") && EthereumJSONRPC.WebSocket,
transport_options: [ transport_options: [
web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, web_socket: EthereumJSONRPC.WebSocket.WebSocketClient,
url: System.get_env("ETHEREUM_JSONRPC_WS_URL") || "ws://localhost:8546" url: System.get_env("ETHEREUM_JSONRPC_WS_URL")
] ]
] ]

@ -16,9 +16,9 @@ config :indexer,
variant: EthereumJSONRPC.Ganache variant: EthereumJSONRPC.Ganache
], ],
subscribe_named_arguments: [ subscribe_named_arguments: [
transport: EthereumJSONRPC.WebSocket, transport: System.get_env("ETHEREUM_JSONRPC_WS_URL") && EthereumJSONRPC.WebSocket,
transport_options: [ transport_options: [
web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, web_socket: EthereumJSONRPC.WebSocket.WebSocketClient,
url: System.get_env("ETHEREUM_JSONRPC_WS_URL") || "ws://localhost:7545" url: System.get_env("ETHEREUM_JSONRPC_WS_URL")
] ]
] ]

@ -19,6 +19,6 @@ config :indexer,
transport: EthereumJSONRPC.WebSocket, transport: EthereumJSONRPC.WebSocket,
transport_options: [ transport_options: [
web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, web_socket: EthereumJSONRPC.WebSocket.WebSocketClient,
url: System.get_env("ETHEREUM_JSONRPC_WS_URL") || "wss://mainnet.infura.io/ws/8lTvJTKmHPCHazkneJsY" url: System.get_env("ETHEREUM_JSONRPC_WS_URL")
] ]
] ]

@ -20,3 +20,17 @@ config :logger, :addresses_without_code,
level: :debug, level: :debug,
path: Path.absname("logs/test/indexer/addresses_without_code.log"), path: Path.absname("logs/test/indexer/addresses_without_code.log"),
metadata_filter: [fetcher: :addresses_without_code] metadata_filter: [fetcher: :addresses_without_code]
variant =
if is_nil(System.get_env("ETHEREUM_JSONRPC_VARIANT")) do
"parity"
else
System.get_env("ETHEREUM_JSONRPC_VARIANT")
|> String.split(".")
|> List.last()
|> String.downcase()
end
# Import variant specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "test/#{variant}.exs"

@ -0,0 +1,8 @@
use Mix.Config
config :indexer,
json_rpc_named_arguments: [
transport: EthereumJSONRPC.Mox,
transport_options: [],
variant: EthereumJSONRPC.Ganache
]

@ -0,0 +1,8 @@
use Mix.Config
config :indexer,
json_rpc_named_arguments: [
transport: EthereumJSONRPC.Mox,
transport_options: [],
variant: EthereumJSONRPC.Geth
]

@ -0,0 +1,8 @@
use Mix.Config
config :indexer,
json_rpc_named_arguments: [
transport: EthereumJSONRPC.Mox,
transport_options: [],
variant: EthereumJSONRPC.Parity
]

@ -0,0 +1,8 @@
use Mix.Config
config :indexer,
json_rpc_named_arguments: [
transport: EthereumJSONRPC.Mox,
transport_options: [],
variant: EthereumJSONRPC.RSK
]

@ -37,7 +37,7 @@ defmodule Indexer.Block.Realtime.Fetcher do
@behaviour Block.Fetcher @behaviour Block.Fetcher
@minimum_safe_polling_period :timer.seconds(10) @minimum_safe_polling_period :timer.seconds(5)
@enforce_keys ~w(block_fetcher)a @enforce_keys ~w(block_fetcher)a
defstruct ~w(block_fetcher subscription previous_number max_number_seen timer)a defstruct ~w(block_fetcher subscription previous_number max_number_seen timer)a
@ -157,7 +157,7 @@ defmodule Indexer.Block.Realtime.Fetcher do
polling_period = polling_period =
case AverageBlockTime.average_block_time() do case AverageBlockTime.average_block_time() do
{:error, :disabled} -> 2_000 {:error, :disabled} -> 2_000
block_time -> round(Duration.to_milliseconds(block_time) * 2) block_time -> round(Duration.to_milliseconds(block_time) / 2)
end end
safe_polling_period = max(polling_period, @minimum_safe_polling_period) safe_polling_period = max(polling_period, @minimum_safe_polling_period)

@ -210,9 +210,26 @@ defmodule Indexer.Fetcher.InternalTransactionTest do
"value" => "0x174876e800" "value" => "0x174876e800"
}, },
"result" => %{"gasUsed" => "0x7d37", "output" => "0x"}, "result" => %{"gasUsed" => "0x7d37", "output" => "0x"},
"subtraces" => 0, "subtraces" => 1,
"traceAddress" => [], "traceAddress" => [],
"type" => "call" "type" => "call"
},
%{
"action" => %{
"callType" => "call",
"from" => "0xb37b428a7ddee91f39b26d79d23dc1c89e3e12a7",
"gas" => "0x32dcf",
"input" => "0x42dad49e",
"to" => "0xee4019030fb5c2b68c42105552c6268d56c6cbfe",
"value" => "0x0"
},
"result" => %{
"gasUsed" => "0xb08",
"output" => "0x"
},
"subtraces" => 0,
"traceAddress" => [0],
"type" => "call"
} }
], ],
"transactionHash" => transaction.hash, "transactionHash" => transaction.hash,

Loading…
Cancel
Save