Merge branch 'master' into design-touchup

pull/1035/head
Ryan Arthur 6 years ago committed by GitHub
commit f8061ba3f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      .circleci/config.yml
  2. 379
      apps/block_scout_web/assets/__tests__/pages/address.js
  3. 2
      apps/block_scout_web/assets/__tests__/pages/blocks.js
  4. 232
      apps/block_scout_web/assets/__tests__/pages/pending_transactions.js
  5. 382
      apps/block_scout_web/assets/__tests__/pages/transaction.js
  6. 160
      apps/block_scout_web/assets/__tests__/pages/transactions.js
  7. 12
      apps/block_scout_web/assets/js/app.js
  8. 5
      apps/block_scout_web/assets/js/lib/from_now.js
  9. 405
      apps/block_scout_web/assets/js/pages/address.js
  10. 0
      apps/block_scout_web/assets/js/pages/blocks.js
  11. 131
      apps/block_scout_web/assets/js/pages/pending_transactions.js
  12. 186
      apps/block_scout_web/assets/js/pages/transaction.js
  13. 100
      apps/block_scout_web/assets/js/pages/transactions.js
  14. 110
      apps/block_scout_web/assets/js/utils.js
  15. 54
      apps/block_scout_web/assets/package-lock.json
  16. 1
      apps/block_scout_web/assets/package.json
  17. 4
      apps/block_scout_web/lib/block_scout_web/chain.ex
  18. 5
      apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex
  19. 90
      apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex
  20. 34
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/transaction_controller.ex
  21. 113
      apps/block_scout_web/lib/block_scout_web/etherscan.ex
  22. 12
      apps/block_scout_web/lib/block_scout_web/resolvers/address.ex
  23. 10
      apps/block_scout_web/lib/block_scout_web/router.ex
  24. 9
      apps/block_scout_web/lib/block_scout_web/schema.ex
  25. 19
      apps/block_scout_web/lib/block_scout_web/schema/scalars.ex
  26. 11
      apps/block_scout_web/lib/block_scout_web/schema/types.ex
  27. 4
      apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex
  28. 3
      apps/block_scout_web/lib/block_scout_web/templates/address/index.html.eex
  29. 4
      apps/block_scout_web/lib/block_scout_web/templates/address_internal_transaction/index.html.eex
  30. 26
      apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex
  31. 2
      apps/block_scout_web/lib/block_scout_web/templates/address_validation/index.html.eex
  32. 2
      apps/block_scout_web/lib/block_scout_web/templates/api_docs/index.html.eex
  33. 2
      apps/block_scout_web/lib/block_scout_web/templates/internal_transaction/_tile.html.eex
  34. 20
      apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex
  35. 2
      apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_function_response.html.eex
  36. 2
      apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex
  37. 4
      apps/block_scout_web/lib/block_scout_web/templates/transaction/index.html.eex
  38. 10
      apps/block_scout_web/lib/block_scout_web/views/address_view.ex
  39. 34
      apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex
  40. 17
      apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex
  41. 34
      apps/block_scout_web/priv/gettext/default.pot
  42. 34
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  43. 4
      apps/block_scout_web/test/block_scout_web/controllers/address_controller_test.exs
  44. 38
      apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs
  45. 110
      apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs
  46. 8
      apps/block_scout_web/test/block_scout_web/controllers/tokens/holder_controller_test.exs
  47. 2
      apps/block_scout_web/test/block_scout_web/features/viewing_tokens_test.exs
  48. 142
      apps/block_scout_web/test/block_scout_web/schema/query/address_test.exs
  49. 2
      apps/block_scout_web/test/block_scout_web/schema/query/block_test.exs
  50. 36
      apps/block_scout_web/test/block_scout_web/views/tokens/smart_contract_view_test.exs
  51. 34
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex
  52. 7
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/blocks.ex
  53. 9
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/request_coordinator_test.exs
  54. 12
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/web_socket/web_socket_client_test.exs
  55. 12
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc_test.exs
  56. 22
      apps/explorer/lib/explorer/chain.ex
  57. 108
      apps/explorer/lib/explorer/chain/address/current_token_balance.ex
  58. 53
      apps/explorer/lib/explorer/chain/address/token_balance.ex
  59. 1
      apps/explorer/lib/explorer/chain/import.ex
  60. 124
      apps/explorer/lib/explorer/chain/import/address/current_token_balances.ex
  61. 32
      apps/explorer/priv/repo/migrations/20181026180921_create_address_current_token_balances.exs
  62. 149
      apps/explorer/test/explorer/chain/address/current_token_balance_test.exs
  63. 95
      apps/explorer/test/explorer/chain/import/address/address/current_token_balances_test.exs
  64. 50
      apps/explorer/test/explorer/chain/import_test.exs
  65. 171
      apps/explorer/test/explorer/chain_test.exs
  66. 11
      apps/explorer/test/support/factory.ex
  67. 5
      apps/indexer/config/config.exs
  68. 2
      apps/indexer/lib/indexer/block/fetcher.ex
  69. 1
      apps/indexer/lib/indexer/block/realtime/fetcher.ex
  70. 31
      apps/indexer/lib/indexer/block/transform.ex
  71. 14
      apps/indexer/lib/indexer/block/transform/base.ex
  72. 16
      apps/indexer/lib/indexer/block/transform/clique.ex
  73. 75
      apps/indexer/lib/indexer/block/util.ex
  74. 8
      apps/indexer/lib/indexer/token_balance/fetcher.ex
  75. 11
      apps/indexer/lib/indexer/token_balances.ex
  76. 4
      apps/indexer/mix.exs
  77. 6
      apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs
  78. 42
      apps/indexer/test/indexer/block/transform/base_test.exs
  79. 43
      apps/indexer/test/indexer/block/transform/clique_test.exs
  80. 56
      apps/indexer/test/indexer/block/transform_test.exs
  81. 4
      apps/indexer/test/indexer/block/uncle/fetcher_test.exs
  82. 40
      apps/indexer/test/indexer/block/util_test.exs
  83. 27
      apps/indexer/test/indexer/token_balances_test.exs
  84. 4
      mix.lock

@ -14,6 +14,8 @@ jobs:
working_directory: ~/app
steps:
- run: sudo apt-get update; sudo apt-get -y install autoconf build-essential libgmp3-dev libtool
- checkout
- run: mix local.hex --force
@ -70,6 +72,11 @@ jobs:
- run: mix compile
# Ensure NIF is compiled for libsecp256k1
- run:
command: make
working_directory: "deps/libsecp256k1"
# `deps` needs to be cached with `_build` because `_build` will symlink into `deps`
- save_cache:

@ -1,300 +1,327 @@
import { reducer, initialState } from '../../js/pages/address'
describe('PAGE_LOAD', () => {
test('page 1 without filter', () => {
const state = initialState
describe('RECEIVED_NEW_BLOCK', () => {
test('with new block', () => {
const state = Object.assign({}, initialState, {
validationCount: 30,
validatedBlocks: [{ blockNumber: 1, blockHtml: 'test 1' }]
})
const action = {
type: 'PAGE_LOAD',
addressHash: '1234',
beyondPageOne: false,
pendingTransactionHashes: ['1']
type: 'RECEIVED_NEW_BLOCK',
msg: { blockNumber: 2, blockHtml: 'test 2' }
}
const output = reducer(state, action)
expect(output.addressHash).toBe('1234')
expect(output.beyondPageOne).toBe(false)
expect(output.filter).toBe(undefined)
expect(output.pendingTransactionHashes).toEqual(['1'])
expect(output.validationCount).toEqual(31)
expect(output.validatedBlocks).toEqual([
{ blockNumber: 2, blockHtml: 'test 2' },
{ blockNumber: 1, blockHtml: 'test 1' }
])
})
test('page 2 without filter', () => {
const state = initialState
test('when channel has been disconnected', () => {
const state = Object.assign({}, initialState, {
channelDisconnected: true,
validationCount: 30,
validatedBlocks: [{ blockNumber: 1, blockHtml: 'test 1' }]
})
const action = {
type: 'PAGE_LOAD',
addressHash: '1234',
beyondPageOne: true,
pendingTransactionHashes: ['1']
type: 'RECEIVED_NEW_BLOCK',
msg: { blockNumber: 2, blockHtml: 'test 2' }
}
const output = reducer(state, action)
expect(output.addressHash).toBe('1234')
expect(output.beyondPageOne).toBe(true)
expect(output.filter).toBe(undefined)
expect(output.pendingTransactionHashes).toEqual(['1'])
expect(output.validationCount).toEqual(30)
expect(output.validatedBlocks).toEqual([
{ blockNumber: 1, blockHtml: 'test 1' }
])
})
test('page 1 with "to" filter', () => {
const state = initialState
test('beyond page one', () => {
const state = Object.assign({}, initialState, {
beyondPageOne: true,
validationCount: 30,
validatedBlocks: [{ blockNumber: 1, blockHtml: 'test 1' }]
})
const action = {
type: 'PAGE_LOAD',
addressHash: '1234',
beyondPageOne: false,
filter: 'to'
type: 'RECEIVED_NEW_BLOCK',
msg: { blockNumber: 2, blockHtml: 'test 2' }
}
const output = reducer(state, action)
expect(output.addressHash).toBe('1234')
expect(output.beyondPageOne).toBe(false)
expect(output.filter).toBe('to')
expect(output.validationCount).toEqual(31)
expect(output.validatedBlocks).toEqual([
{ blockNumber: 1, blockHtml: 'test 1' }
])
})
test('page 2 with "to" filter', () => {
const state = initialState
})
describe('RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', () => {
test('with new internal transaction', () => {
const state = Object.assign({}, initialState, {
internalTransactions: [{ internalTransactionHtml: 'test 1' }]
})
const action = {
type: 'PAGE_LOAD',
addressHash: '1234',
beyondPageOne: true,
filter: 'to'
type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH',
msgs: [{ internalTransactionHtml: 'test 2' }]
}
const output = reducer(state, action)
expect(output.addressHash).toBe('1234')
expect(output.beyondPageOne).toBe(true)
expect(output.filter).toBe('to')
expect(output.internalTransactions).toEqual([
{ internalTransactionHtml: 'test 2' },
{ internalTransactionHtml: 'test 1' }
])
})
})
test('CHANNEL_DISCONNECTED', () => {
const state = initialState
const action = {
type: 'CHANNEL_DISCONNECTED'
}
const output = reducer(state, action)
expect(output.channelDisconnected).toBe(true)
})
test('RECEIVED_UPDATED_BALANCE', () => {
const state = initialState
const action = {
type: 'RECEIVED_UPDATED_BALANCE',
msg: {
balance: 'hello world'
test('with batch of new internal transactions', () => {
const state = Object.assign({}, initialState, {
internalTransactions: [{ internalTransactionHtml: 'test 1' }]
})
const action = {
type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH',
msgs: [
{ internalTransactionHtml: 'test 2' },
{ internalTransactionHtml: 'test 3' },
{ internalTransactionHtml: 'test 4' },
{ internalTransactionHtml: 'test 5' },
{ internalTransactionHtml: 'test 6' },
{ internalTransactionHtml: 'test 7' },
{ internalTransactionHtml: 'test 8' },
{ internalTransactionHtml: 'test 9' },
{ internalTransactionHtml: 'test 10' },
{ internalTransactionHtml: 'test 11' },
{ internalTransactionHtml: 'test 12' },
{ internalTransactionHtml: 'test 13' }
]
}
}
const output = reducer(state, action)
expect(output.balance).toBe('hello world')
})
const output = reducer(state, action)
describe('RECEIVED_NEW_PENDING_TRANSACTION', () => {
test('single transaction', () => {
const state = initialState
expect(output.internalTransactions).toEqual([
{ internalTransactionHtml: 'test 1' }
])
expect(output.internalTransactionsBatch).toEqual([
{ internalTransactionHtml: 'test 13' },
{ internalTransactionHtml: 'test 12' },
{ internalTransactionHtml: 'test 11' },
{ internalTransactionHtml: 'test 10' },
{ internalTransactionHtml: 'test 9' },
{ internalTransactionHtml: 'test 8' },
{ internalTransactionHtml: 'test 7' },
{ internalTransactionHtml: 'test 6' },
{ internalTransactionHtml: 'test 5' },
{ internalTransactionHtml: 'test 4' },
{ internalTransactionHtml: 'test 3' },
{ internalTransactionHtml: 'test 2' },
])
})
test('after batch of new internal transactions', () => {
const state = Object.assign({}, initialState, {
internalTransactionsBatch: [{ internalTransactionHtml: 'test 1' }]
})
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION',
msg: {
transactionHash: '0x00',
transactionHtml: 'test'
}
type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH',
msgs: [
{ internalTransactionHtml: 'test 2' }
]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual(['test'])
expect(output.transactionCount).toEqual(null)
expect(output.internalTransactionsBatch).toEqual([
{ internalTransactionHtml: 'test 2' },
{ internalTransactionHtml: 'test 1' }
])
})
test('single transaction after single transaction', () => {
test('when channel has been disconnected', () => {
const state = Object.assign({}, initialState, {
newPendingTransactions: ['test 1']
channelDisconnected: true,
internalTransactions: [{ internalTransactionHtml: 'test 1' }]
})
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION',
msg: {
transactionHash: '0x02',
transactionHtml: 'test 2'
}
type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH',
msgs: [{ internalTransactionHtml: 'test 2' }]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual(['test 1', 'test 2'])
expect(output.pendingTransactionHashes.length).toEqual(1)
expect(output.internalTransactions).toEqual([
{ internalTransactionHtml: 'test 1' }
])
})
test('after disconnection', () => {
test('beyond page one', () => {
const state = Object.assign({}, initialState, {
channelDisconnected: true
beyondPageOne: true,
internalTransactions: [{ internalTransactionHtml: 'test 1' }]
})
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION',
msg: {
transactionHash: '0x00',
transactionHtml: 'test'
}
type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH',
msgs: [{ internalTransactionHtml: 'test 2' }]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([])
expect(output.pendingTransactionHashes).toEqual([])
expect(output.internalTransactions).toEqual([
{ internalTransactionHtml: 'test 1' }
])
})
test('on page 2', () => {
test('with filtered out internal transaction', () => {
const state = Object.assign({}, initialState, {
beyondPageOne: true
filter: 'to'
})
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION',
msg: {
transactionHash: '0x00',
transactionHtml: 'test'
}
type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH',
msgs: [{ internalTransactionHtml: 'test 2' }]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([])
expect(output.pendingTransactionHashes).toEqual([])
expect(output.internalTransactions).toEqual([])
})
})
describe('RECEIVED_NEW_TRANSACTION', () => {
test('single transaction', () => {
describe('RECEIVED_NEW_PENDING_TRANSACTION', () => {
test('with new pending transaction', () => {
const state = Object.assign({}, initialState, {
addressHash: '0x111'
pendingTransactions: [{ transactionHash: 1, transactionHtml: 'test 1' }]
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: {
transactionHtml: 'test'
}
type: 'RECEIVED_NEW_PENDING_TRANSACTION',
msg: { transactionHash: 2, transactionHtml: 'test 2' }
}
const output = reducer(state, action)
expect(output.newTransactions).toEqual([{ transactionHtml: 'test' }])
expect(output.transactionCount).toEqual(null)
expect(output.pendingTransactions).toEqual([
{ transactionHash: 2, transactionHtml: 'test 2' },
{ transactionHash: 1, transactionHtml: 'test 1' }
])
})
test('single transaction after single transaction', () => {
test('when channel has been disconnected', () => {
const state = Object.assign({}, initialState, {
newTransactions: [{ transactionHtml: 'test 1' }]
channelDisconnected: true,
pendingTransactions: [{ transactionHash: 1, transactionHtml: 'test 1' }]
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: {
transactionHtml: 'test 2'
}
type: 'RECEIVED_NEW_PENDING_TRANSACTION',
msg: { transactionHash: 2, transactionHtml: 'test 2' }
}
const output = reducer(state, action)
expect(output.newTransactions).toEqual([
{ transactionHtml: 'test 1' },
{ transactionHtml: 'test 2' }
expect(output.pendingTransactions).toEqual([
{ transactionHash: 1, transactionHtml: 'test 1' }
])
})
test('after disconnection', () => {
test('beyond page one', () => {
const state = Object.assign({}, initialState, {
channelDisconnected: true
beyondPageOne: true,
pendingTransactions: [{ transactionHash: 1, transactionHtml: 'test 1' }]
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: {
transactionHtml: 'test'
}
type: 'RECEIVED_NEW_PENDING_TRANSACTION',
msg: { transactionHash: 2, transactionHtml: 'test 2' }
}
const output = reducer(state, action)
expect(output.newTransactions).toEqual([])
expect(output.pendingTransactions).toEqual([
{ transactionHash: 1, transactionHtml: 'test 1' }
])
})
test('on page 2', () => {
test('with filtered out pending transaction', () => {
const state = Object.assign({}, initialState, {
beyondPageOne: true,
transactionCount: 1,
addressHash: '0x111'
filter: 'to'
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: {
transactionHtml: 'test'
}
type: 'RECEIVED_NEW_PENDING_TRANSACTION',
msg: { transactionHash: 2, transactionHtml: 'test 2' }
}
const output = reducer(state, action)
expect(output.newTransactions).toEqual([])
expect(output.transactionCount).toEqual(1)
expect(output.pendingTransactions).toEqual([])
})
test('transaction from current address with "from" filter', () => {
})
describe('RECEIVED_NEW_TRANSACTION', () => {
test('with new transaction', () => {
const state = Object.assign({}, initialState, {
addressHash: '1234',
filter: 'from'
pendingTransactions: [{ transactionHash: 2, transactionHtml: 'test' }],
transactions: [{ transactionHash: 1, transactionHtml: 'test 1' }]
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: {
fromAddressHash: '1234',
transactionHtml: 'test'
}
msg: { transactionHash: 2, transactionHtml: 'test 2' }
}
const output = reducer(state, action)
expect(output.newTransactions).toEqual([
{ fromAddressHash: '1234', transactionHtml: 'test' }
expect(output.pendingTransactions).toEqual([
{ transactionHash: 2, transactionHtml: 'test 2', validated: true }
])
expect(output.transactions).toEqual([
{ transactionHash: 2, transactionHtml: 'test 2' },
{ transactionHash: 1, transactionHtml: 'test 1' }
])
})
test('transaction from current address with "to" filter', () => {
test('when channel has been disconnected', () => {
const state = Object.assign({}, initialState, {
addressHash: '1234',
filter: 'to'
channelDisconnected: true,
pendingTransactions: [{ transactionHash: 2, transactionHtml: 'test' }],
transactions: [{ transactionHash: 1, transactionHtml: 'test 1' }]
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: {
fromAddressHash: '1234',
transactionHtml: 'test'
}
msg: { transactionHash: 2, transactionHtml: 'test 2' }
}
const output = reducer(state, action)
expect(output.newTransactions).toEqual([])
expect(output.pendingTransactions).toEqual([
{ transactionHash: 2, transactionHtml: 'test' }
])
expect(output.transactions).toEqual([
{ transactionHash: 1, transactionHtml: 'test 1' }
])
})
test('transaction to current address with "to" filter', () => {
test('beyond page one', () => {
const state = Object.assign({}, initialState, {
addressHash: '1234',
filter: 'to'
beyondPageOne: true,
transactions: [{ transactionHash: 1, transactionHtml: 'test 1' }]
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: {
toAddressHash: '1234',
transactionHtml: 'test'
}
msg: { transactionHash: 2, transactionHtml: 'test 2' }
}
const output = reducer(state, action)
expect(output.newTransactions).toEqual([
{ toAddressHash: '1234', transactionHtml: 'test' }
expect(output.pendingTransactions).toEqual([])
expect(output.transactions).toEqual([
{ transactionHash: 1, transactionHtml: 'test 1' }
])
})
test('transaction to current address with "from" filter', () => {
test('with filtered out transaction', () => {
const state = Object.assign({}, initialState, {
addressHash: '1234',
filter: 'from'
filter: 'to'
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: {
toAddressHash: '1234',
transactionHtml: 'test'
}
msg: { transactionHash: 2, transactionHtml: 'test 2' }
}
const output = reducer(state, action)
expect(output.newTransactions).toEqual([])
expect(output.transactions).toEqual([])
})
test('single transaction collated from pending', () => {
const state = initialState
})
describe('RECEIVED_NEXT_TRANSACTIONS_PAGE', () => {
test('with new transaction page', () => {
const state = Object.assign({}, initialState, {
loadingNextPage: true,
nextPageUrl: '1',
transactions: [{ transactionHash: 1, transactionHtml: 'test 1' }]
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
type: 'RECEIVED_NEXT_TRANSACTIONS_PAGE',
msg: {
transactionHash: '0x00',
transactionHtml: 'test'
nextPageUrl: '2',
transactions: [{ transactionHash: 2, transactionHtml: 'test 2' }]
}
}
const output = reducer(state, action)
expect(output.newTransactions).toEqual([
{ transactionHash: '0x00', transactionHtml: 'test' }
expect(output.loadingNextPage).toEqual(false)
expect(output.nextPageUrl).toEqual('2')
expect(output.transactions).toEqual([
{ transactionHash: 1, transactionHtml: 'test 1' },
{ transactionHash: 2, transactionHtml: 'test 2' }
])
expect(output.transactionCount).toEqual(null)
})
})

@ -1,4 +1,4 @@
import { reducer, initialState } from '../../js/pages/block'
import { reducer, initialState } from '../../js/pages/blocks'
test('CHANNEL_DISCONNECTED', () => {
const state = initialState

@ -0,0 +1,232 @@
import { reducer, initialState } from '../../js/pages/pending_transactions'
test('CHANNEL_DISCONNECTED', () => {
const state = initialState
const action = {
type: 'CHANNEL_DISCONNECTED'
}
const output = reducer(state, action)
expect(output.channelDisconnected).toBe(true)
})
describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => {
test('single transaction', () => {
const state = initialState
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
msgs: [{
transactionHash: '0x00',
transactionHtml: 'test'
}]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual(['test'])
expect(output.newPendingTransactionHashesBatch.length).toEqual(0)
expect(output.pendingTransactionCount).toEqual(1)
})
test('large batch of transactions', () => {
const state = initialState
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
msgs: [{
transactionHash: '0x01',
transactionHtml: 'test 1'
},{
transactionHash: '0x02',
transactionHtml: 'test 2'
},{
transactionHash: '0x03',
transactionHtml: 'test 3'
},{
transactionHash: '0x04',
transactionHtml: 'test 4'
},{
transactionHash: '0x05',
transactionHtml: 'test 5'
},{
transactionHash: '0x06',
transactionHtml: 'test 6'
},{
transactionHash: '0x07',
transactionHtml: 'test 7'
},{
transactionHash: '0x08',
transactionHtml: 'test 8'
},{
transactionHash: '0x09',
transactionHtml: 'test 9'
},{
transactionHash: '0x10',
transactionHtml: 'test 10'
},{
transactionHash: '0x11',
transactionHtml: 'test 11'
}]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([])
expect(output.newPendingTransactionHashesBatch.length).toEqual(11)
expect(output.pendingTransactionCount).toEqual(11)
})
test('single transaction after single transaction', () => {
const state = Object.assign({}, initialState, {
newPendingTransactions: ['test 1'],
pendingTransactionCount: 1
})
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
msgs: [{
transactionHash: '0x02',
transactionHtml: 'test 2'
}]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual(['test 1', 'test 2'])
expect(output.newPendingTransactionHashesBatch.length).toEqual(0)
expect(output.pendingTransactionCount).toEqual(2)
})
test('single transaction after large batch of transactions', () => {
const state = Object.assign({}, initialState, {
newPendingTransactionHashesBatch: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']
})
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
msgs: [{
transactionHash: '0x12',
transactionHtml: 'test 12'
}]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([])
expect(output.newPendingTransactionHashesBatch.length).toEqual(12)
})
test('large batch of transactions after large batch of transactions', () => {
const state = Object.assign({}, initialState, {
newPendingTransactionHashesBatch: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']
})
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
msgs: [{
transactionHash: '0x12',
transactionHtml: 'test 12'
},{
transactionHash: '0x13',
transactionHtml: 'test 13'
},{
transactionHash: '0x14',
transactionHtml: 'test 14'
},{
transactionHash: '0x15',
transactionHtml: 'test 15'
},{
transactionHash: '0x16',
transactionHtml: 'test 16'
},{
transactionHash: '0x17',
transactionHtml: 'test 17'
},{
transactionHash: '0x18',
transactionHtml: 'test 18'
},{
transactionHash: '0x19',
transactionHtml: 'test 19'
},{
transactionHash: '0x20',
transactionHtml: 'test 20'
},{
transactionHash: '0x21',
transactionHtml: 'test 21'
},{
transactionHash: '0x22',
transactionHtml: 'test 22'
}]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([])
expect(output.newPendingTransactionHashesBatch.length).toEqual(22)
})
test('after disconnection', () => {
const state = Object.assign({}, initialState, {
channelDisconnected: true
})
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
msgs: [{
transactionHash: '0x00',
transactionHtml: 'test'
}]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([])
})
test('on page 2+', () => {
const state = Object.assign({}, initialState, {
beyondPageOne: true,
pendingTransactionCount: 1
})
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
msgs: [{
transactionHash: '0x00',
transactionHtml: 'test'
}]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([])
expect(output.pendingTransactionCount).toEqual(2)
})
})
describe('RECEIVED_NEW_TRANSACTION', () => {
test('single transaction collated', () => {
const state = { ...initialState, pendingTransactionCount: 2 }
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: {
transactionHash: '0x00'
}
}
const output = reducer(state, action)
expect(output.pendingTransactionCount).toBe(1)
expect(output.newTransactionHashes).toEqual(['0x00'])
})
test('single transaction collated after batch', () => {
const state = Object.assign({}, initialState, {
newPendingTransactionHashesBatch: ['0x01', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: {
transactionHash: '0x01'
}
}
const output = reducer(state, action)
expect(output.newPendingTransactionHashesBatch.length).toEqual(10)
expect(output.newPendingTransactionHashesBatch).not.toContain('0x01')
})
test('on page 2+', () => {
const state = Object.assign({}, initialState, {
beyondPageOne: true,
pendingTransactionCount: 2
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: {
transactionHash: '0x01'
}
}
const output = reducer(state, action)
expect(output.pendingTransactionCount).toEqual(1)
})
})

@ -1,16 +1,5 @@
import { reducer, initialState } from '../../js/pages/transaction'
test('CHANNEL_DISCONNECTED', () => {
const state = initialState
const action = {
type: 'CHANNEL_DISCONNECTED'
}
const output = reducer(state, action)
expect(output.channelDisconnected).toBe(true)
expect(output.batchCountAccumulator).toBe(0)
})
test('RECEIVED_NEW_BLOCK', () => {
const state = { ...initialState, blockNumber: 1 }
const action = {
@ -23,374 +12,3 @@ test('RECEIVED_NEW_BLOCK', () => {
expect(output.confirmations).toBe(4)
})
describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => {
test('single transaction', () => {
const state = initialState
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
msgs: [{
transactionHash: '0x00',
transactionHtml: 'test'
}]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual(['test'])
expect(output.newPendingTransactionHashesBatch.length).toEqual(0)
expect(output.pendingTransactionCount).toEqual(1)
})
test('large batch of transactions', () => {
const state = initialState
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
msgs: [{
transactionHash: '0x01',
transactionHtml: 'test 1'
},{
transactionHash: '0x02',
transactionHtml: 'test 2'
},{
transactionHash: '0x03',
transactionHtml: 'test 3'
},{
transactionHash: '0x04',
transactionHtml: 'test 4'
},{
transactionHash: '0x05',
transactionHtml: 'test 5'
},{
transactionHash: '0x06',
transactionHtml: 'test 6'
},{
transactionHash: '0x07',
transactionHtml: 'test 7'
},{
transactionHash: '0x08',
transactionHtml: 'test 8'
},{
transactionHash: '0x09',
transactionHtml: 'test 9'
},{
transactionHash: '0x10',
transactionHtml: 'test 10'
},{
transactionHash: '0x11',
transactionHtml: 'test 11'
}]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([])
expect(output.newPendingTransactionHashesBatch.length).toEqual(11)
expect(output.pendingTransactionCount).toEqual(11)
})
test('single transaction after single transaction', () => {
const state = Object.assign({}, initialState, {
newPendingTransactions: ['test 1'],
pendingTransactionCount: 1
})
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
msgs: [{
transactionHash: '0x02',
transactionHtml: 'test 2'
}]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual(['test 1', 'test 2'])
expect(output.newPendingTransactionHashesBatch.length).toEqual(0)
expect(output.pendingTransactionCount).toEqual(2)
})
test('single transaction after large batch of transactions', () => {
const state = Object.assign({}, initialState, {
newPendingTransactionHashesBatch: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']
})
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
msgs: [{
transactionHash: '0x12',
transactionHtml: 'test 12'
}]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([])
expect(output.newPendingTransactionHashesBatch.length).toEqual(12)
})
test('large batch of transactions after large batch of transactions', () => {
const state = Object.assign({}, initialState, {
newPendingTransactionHashesBatch: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']
})
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
msgs: [{
transactionHash: '0x12',
transactionHtml: 'test 12'
},{
transactionHash: '0x13',
transactionHtml: 'test 13'
},{
transactionHash: '0x14',
transactionHtml: 'test 14'
},{
transactionHash: '0x15',
transactionHtml: 'test 15'
},{
transactionHash: '0x16',
transactionHtml: 'test 16'
},{
transactionHash: '0x17',
transactionHtml: 'test 17'
},{
transactionHash: '0x18',
transactionHtml: 'test 18'
},{
transactionHash: '0x19',
transactionHtml: 'test 19'
},{
transactionHash: '0x20',
transactionHtml: 'test 20'
},{
transactionHash: '0x21',
transactionHtml: 'test 21'
},{
transactionHash: '0x22',
transactionHtml: 'test 22'
}]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([])
expect(output.newPendingTransactionHashesBatch.length).toEqual(22)
})
test('after disconnection', () => {
const state = Object.assign({}, initialState, {
channelDisconnected: true
})
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
msgs: [{
transactionHash: '0x00',
transactionHtml: 'test'
}]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([])
expect(output.batchCountAccumulator).toEqual(0)
})
test('on page 2+', () => {
const state = Object.assign({}, initialState, {
beyondPageOne: true,
pendingTransactionCount: 1
})
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
msgs: [{
transactionHash: '0x00',
transactionHtml: 'test'
}]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([])
expect(output.batchCountAccumulator).toEqual(0)
expect(output.pendingTransactionCount).toEqual(2)
})
})
describe('RECEIVED_NEW_TRANSACTION', () => {
test('single transaction collated', () => {
const state = { ...initialState, pendingTransactionCount: 2 }
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: {
transactionHash: '0x00'
}
}
const output = reducer(state, action)
expect(output.pendingTransactionCount).toBe(1)
expect(output.newTransactionHashes).toEqual(['0x00'])
})
test('single transaction collated after batch', () => {
const state = Object.assign({}, initialState, {
newPendingTransactionHashesBatch: ['0x01', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: {
transactionHash: '0x01'
}
}
const output = reducer(state, action)
expect(output.newPendingTransactionHashesBatch.length).toEqual(10)
expect(output.newPendingTransactionHashesBatch).not.toContain('0x01')
})
test('on page 2+', () => {
const state = Object.assign({}, initialState, {
beyondPageOne: true,
pendingTransactionCount: 2
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: {
transactionHash: '0x01'
}
}
const output = reducer(state, action)
expect(output.pendingTransactionCount).toEqual(1)
})
})
describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
test('single transaction', () => {
const state = initialState
const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{
transactionHtml: 'test'
}]
}
const output = reducer(state, action)
expect(output.newTransactions).toEqual(['test'])
expect(output.batchCountAccumulator).toEqual(0)
expect(output.transactionCount).toEqual(1)
})
test('large batch of transactions', () => {
const state = initialState
const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{
transactionHtml: 'test 1'
},{
transactionHtml: 'test 2'
},{
transactionHtml: 'test 3'
},{
transactionHtml: 'test 4'
},{
transactionHtml: 'test 5'
},{
transactionHtml: 'test 6'
},{
transactionHtml: 'test 7'
},{
transactionHtml: 'test 8'
},{
transactionHtml: 'test 9'
},{
transactionHtml: 'test 10'
},{
transactionHtml: 'test 11'
}]
}
const output = reducer(state, action)
expect(output.newTransactions).toEqual([])
expect(output.batchCountAccumulator).toEqual(11)
expect(output.transactionCount).toEqual(11)
})
test('single transaction after single transaction', () => {
const state = Object.assign({}, initialState, {
newTransactions: ['test 1']
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{
transactionHtml: 'test 2'
}]
}
const output = reducer(state, action)
expect(output.newTransactions).toEqual(['test 1', 'test 2'])
expect(output.batchCountAccumulator).toEqual(0)
})
test('single transaction after large batch of transactions', () => {
const state = Object.assign({}, initialState, {
batchCountAccumulator: 11
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{
transactionHtml: 'test 12'
}]
}
const output = reducer(state, action)
expect(output.newTransactions).toEqual([])
expect(output.batchCountAccumulator).toEqual(12)
})
test('large batch of transactions after large batch of transactions', () => {
const state = Object.assign({}, initialState, {
batchCountAccumulator: 11
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{
transactionHtml: 'test 12'
},{
transactionHtml: 'test 13'
},{
transactionHtml: 'test 14'
},{
transactionHtml: 'test 15'
},{
transactionHtml: 'test 16'
},{
transactionHtml: 'test 17'
},{
transactionHtml: 'test 18'
},{
transactionHtml: 'test 19'
},{
transactionHtml: 'test 20'
},{
transactionHtml: 'test 21'
},{
transactionHtml: 'test 22'
}]
}
const output = reducer(state, action)
expect(output.newTransactions).toEqual([])
expect(output.batchCountAccumulator).toEqual(22)
})
test('after disconnection', () => {
const state = Object.assign({}, initialState, {
channelDisconnected: true
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{
transactionHtml: 'test'
}]
}
const output = reducer(state, action)
expect(output.newTransactions).toEqual([])
expect(output.batchCountAccumulator).toEqual(0)
})
test('on page 2+', () => {
const state = Object.assign({}, initialState, {
beyondPageOne: true,
transactionCount: 1
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{
transactionHtml: 'test'
}]
}
const output = reducer(state, action)
expect(output.newTransactions).toEqual([])
expect(output.batchCountAccumulator).toEqual(0)
expect(output.transactionCount).toEqual(2)
})
})

@ -0,0 +1,160 @@
import { reducer, initialState } from '../../js/pages/transactions'
test('CHANNEL_DISCONNECTED', () => {
const state = initialState
const action = {
type: 'CHANNEL_DISCONNECTED'
}
const output = reducer(state, action)
expect(output.channelDisconnected).toBe(true)
expect(output.batchCountAccumulator).toBe(0)
})
describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
test('single transaction', () => {
const state = initialState
const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{
transactionHtml: 'test'
}]
}
const output = reducer(state, action)
expect(output.newTransactions).toEqual(['test'])
expect(output.batchCountAccumulator).toEqual(0)
expect(output.transactionCount).toEqual(1)
})
test('large batch of transactions', () => {
const state = initialState
const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{
transactionHtml: 'test 1'
},{
transactionHtml: 'test 2'
},{
transactionHtml: 'test 3'
},{
transactionHtml: 'test 4'
},{
transactionHtml: 'test 5'
},{
transactionHtml: 'test 6'
},{
transactionHtml: 'test 7'
},{
transactionHtml: 'test 8'
},{
transactionHtml: 'test 9'
},{
transactionHtml: 'test 10'
},{
transactionHtml: 'test 11'
}]
}
const output = reducer(state, action)
expect(output.newTransactions).toEqual([])
expect(output.batchCountAccumulator).toEqual(11)
expect(output.transactionCount).toEqual(11)
})
test('single transaction after single transaction', () => {
const state = Object.assign({}, initialState, {
newTransactions: ['test 1']
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{
transactionHtml: 'test 2'
}]
}
const output = reducer(state, action)
expect(output.newTransactions).toEqual(['test 1', 'test 2'])
expect(output.batchCountAccumulator).toEqual(0)
})
test('single transaction after large batch of transactions', () => {
const state = Object.assign({}, initialState, {
batchCountAccumulator: 11
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{
transactionHtml: 'test 12'
}]
}
const output = reducer(state, action)
expect(output.newTransactions).toEqual([])
expect(output.batchCountAccumulator).toEqual(12)
})
test('large batch of transactions after large batch of transactions', () => {
const state = Object.assign({}, initialState, {
batchCountAccumulator: 11
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{
transactionHtml: 'test 12'
},{
transactionHtml: 'test 13'
},{
transactionHtml: 'test 14'
},{
transactionHtml: 'test 15'
},{
transactionHtml: 'test 16'
},{
transactionHtml: 'test 17'
},{
transactionHtml: 'test 18'
},{
transactionHtml: 'test 19'
},{
transactionHtml: 'test 20'
},{
transactionHtml: 'test 21'
},{
transactionHtml: 'test 22'
}]
}
const output = reducer(state, action)
expect(output.newTransactions).toEqual([])
expect(output.batchCountAccumulator).toEqual(22)
})
test('after disconnection', () => {
const state = Object.assign({}, initialState, {
channelDisconnected: true
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{
transactionHtml: 'test'
}]
}
const output = reducer(state, action)
expect(output.newTransactions).toEqual([])
expect(output.batchCountAccumulator).toEqual(0)
})
test('on page 2+', () => {
const state = Object.assign({}, initialState, {
beyondPageOne: true,
transactionCount: 1
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{
transactionHtml: 'test'
}]
}
const output = reducer(state, action)
expect(output.newTransactions).toEqual([])
expect(output.batchCountAccumulator).toEqual(0)
expect(output.transactionCount).toEqual(2)
})
})

@ -20,6 +20,13 @@ import 'bootstrap'
import './locale'
import './pages/address'
import './pages/blocks'
import './pages/chain'
import './pages/pending_transactions'
import './pages/transaction'
import './pages/transactions'
import './lib/clipboard_buttons'
import './lib/currency'
import './lib/from_now'
@ -37,8 +44,3 @@ import './lib/token_balance_dropdown_search'
import './lib/token_transfers_toggle'
import './lib/tooltip'
import './lib/try_api'
import './pages/address'
import './pages/block'
import './pages/chain'
import './pages/transaction'

@ -8,8 +8,9 @@ moment.relativeTimeThreshold('m', 60)
moment.relativeTimeThreshold('s', 60)
moment.relativeTimeThreshold('ss', 1)
export function updateAllAges () {
$('[data-from-now]').each((i, el) => tryUpdateAge(el))
export function updateAllAges ($container = $(document)) {
$container.find('[data-from-now]').each((i, el) => tryUpdateAge(el))
return $container
}
function tryUpdateAge (el) {
if (!el.dataset.fromNow) return

@ -4,47 +4,48 @@ import URI from 'urijs'
import humps from 'humps'
import numeral from 'numeral'
import socket from '../socket'
import { batchChannel, initRedux, slideDownPrepend, slideUpRemove } from '../utils'
import { updateAllAges } from '../lib/from_now'
import { createStore, connectElements, batchChannel, listMorph, onScrollBottom } from '../utils'
import { updateAllCalculatedUsdValues } from '../lib/currency.js'
import { loadTokenBalanceDropdown } from '../lib/token_balance_dropdown'
const BATCH_THRESHOLD = 10
const TRANSACTION_VALIDATED_MOVE_DELAY = 1000
export const initialState = {
addressHash: null,
balance: null,
batchCountAccumulator: 0,
beyondPageOne: null,
channelDisconnected: false,
addressHash: null,
filter: null,
newBlock: null,
newInternalTransactions: [],
newPendingTransactions: [],
newTransactions: [],
pendingTransactionHashes: [],
balance: null,
transactionCount: null,
validationCount: null
validationCount: null,
pendingTransactions: [],
transactions: [],
internalTransactions: [],
internalTransactionsBatch: [],
validatedBlocks: [],
loadingNextPage: false,
pagingError: false,
nextPageUrl: null,
beyondPageOne: null
}
export function reducer (state = initialState, action) {
switch (action.type) {
case 'PAGE_LOAD': {
return Object.assign({}, state, {
addressHash: action.addressHash,
beyondPageOne: action.beyondPageOne,
filter: action.filter,
pendingTransactionHashes: action.pendingTransactionHashes,
transactionCount: numeral(action.transactionCount).value(),
validationCount: action.validationCount ? numeral(action.validationCount).value() : null
})
case 'PAGE_LOAD':
case 'ELEMENTS_LOAD': {
return Object.assign({}, state, _.omit(action, 'type'))
}
case 'CHANNEL_DISCONNECTED': {
if (state.beyondPageOne) return state
return Object.assign({}, state, {
channelDisconnected: true,
batchCountAccumulator: 0
internalTransactionsBatch: []
})
}
case 'RECEIVED_NEW_BLOCK': {
@ -54,7 +55,10 @@ export function reducer (state = initialState, action) {
if (state.beyondPageOne) return Object.assign({}, state, { validationCount })
return Object.assign({}, state, {
newBlock: action.msg.blockHtml,
validatedBlocks: [
action.msg,
...state.validatedBlocks
],
validationCount
})
}
@ -68,16 +72,19 @@ export function reducer (state = initialState, action) {
(state.filter === 'from' && fromAddressHash === state.addressHash)
))
if (!state.batchCountAccumulator && incomingInternalTransactions.length < BATCH_THRESHOLD) {
if (!state.internalTransactionsBatch.length && incomingInternalTransactions.length < BATCH_THRESHOLD) {
return Object.assign({}, state, {
newInternalTransactions: [
...state.newInternalTransactions,
..._.map(incomingInternalTransactions, 'internalTransactionHtml')
internalTransactions: [
...incomingInternalTransactions.reverse(),
...state.internalTransactions
]
})
} else {
return Object.assign({}, state, {
batchCountAccumulator: state.batchCountAccumulator + incomingInternalTransactions.length
internalTransactionsBatch: [
...incomingInternalTransactions.reverse(),
...state.internalTransactionsBatch
]
})
}
}
@ -90,16 +97,17 @@ export function reducer (state = initialState, action) {
}
return Object.assign({}, state, {
newPendingTransactions: [
...state.newPendingTransactions,
action.msg.transactionHtml
],
pendingTransactionHashes: [
...state.pendingTransactionHashes,
action.msg.transactionHash
pendingTransactions: [
action.msg,
...state.pendingTransactions
]
})
}
case 'REMOVE_PENDING_TRANSACTION': {
return Object.assign({}, state, {
pendingTransactions: state.pendingTransactions.filter((transaction) => action.msg.transactionHash !== transaction.transactionHash)
})
}
case 'RECEIVED_NEW_TRANSACTION': {
if (state.channelDisconnected) return state
@ -111,15 +119,12 @@ export function reducer (state = initialState, action) {
return Object.assign({}, state, { transactionCount })
}
const updatedPendingTransactionHashes =
_.without(state.pendingTransactionHashes, action.msg.transactionHash)
return Object.assign({}, state, {
newTransactions: [
...state.newTransactions,
action.msg
pendingTransactions: state.pendingTransactions.map((transaction) => action.msg.transactionHash === transaction.transactionHash ? Object.assign({}, action.msg, { validated: true }) : transaction),
transactions: [
action.msg,
...state.transactions
],
pendingTransactionHashes: updatedPendingTransactionHashes,
transactionCount: transactionCount
})
}
@ -128,109 +133,245 @@ export function reducer (state = initialState, action) {
balance: action.msg.balance
})
}
case 'LOADING_NEXT_PAGE': {
return Object.assign({}, state, {
loadingNextPage: true
})
}
case 'PAGING_ERROR': {
return Object.assign({}, state, {
loadingNextPage: false,
pagingError: true
})
}
case 'RECEIVED_NEXT_TRANSACTIONS_PAGE': {
return Object.assign({}, state, {
loadingNextPage: false,
nextPageUrl: action.msg.nextPageUrl,
transactions: [
...state.transactions,
...action.msg.transactions
]
})
}
default:
return state
}
}
const $addressDetailsPage = $('[data-page="address-details"]')
if ($addressDetailsPage.length) {
initRedux(reducer, {
main (store) {
const addressHash = $addressDetailsPage[0].dataset.pageAddressHash
const addressChannel = socket.channel(`addresses:${addressHash}`, {})
const { filter, blockNumber } = humps.camelizeKeys(URI(window.location).query(true))
store.dispatch({
type: 'PAGE_LOAD',
addressHash,
beyondPageOne: !!blockNumber,
filter,
pendingTransactionHashes: $('[data-selector="pending-transactions-list"]').children()
.map((index, el) => el.dataset.transactionHash).toArray(),
transactionCount: $('[data-selector="transaction-count"]').text(),
validationCount: $('[data-selector="validation-count"]') ? $('[data-selector="validation-count"]').text() : null
})
addressChannel.join()
addressChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' }))
addressChannel.on('balance', (msg) => {
store.dispatch({ type: 'RECEIVED_UPDATED_BALANCE', msg: humps.camelizeKeys(msg) })
})
addressChannel.on('internal_transaction', batchChannel((msgs) =>
store.dispatch({ type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) })
))
addressChannel.on('pending_transaction', (msg) => store.dispatch({ type: 'RECEIVED_NEW_PENDING_TRANSACTION', msg: humps.camelizeKeys(msg) }))
addressChannel.on('transaction', (msg) => store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION', msg: humps.camelizeKeys(msg) }))
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', msg: humps.camelizeKeys(msg) }))
const elements = {
'[data-selector="channel-disconnected-message"]': {
render ($el, state) {
if (state.channelDisconnected) $el.show()
}
},
'[data-selector="balance-card"]': {
load ($el) {
return { balance: $el.html() }
},
render (state, oldState) {
const $balance = $('[data-selector="balance-card"]')
const $channelBatching = $('[data-selector="channel-batching-message"]')
const $channelBatchingCount = $('[data-selector="channel-batching-count"]')
const $channelDisconnected = $('[data-selector="channel-disconnected-message"]')
const $emptyInternalTransactionsList = $('[data-selector="empty-internal-transactions-list"]')
const $emptyTransactionsList = $('[data-selector="empty-transactions-list"]')
const $internalTransactionsList = $('[data-selector="internal-transactions-list"]')
const $pendingTransactionsCount = $('[data-selector="pending-transactions-count"]')
const $pendingTransactionsList = $('[data-selector="pending-transactions-list"]')
const $transactionCount = $('[data-selector="transaction-count"]')
const $transactionsList = $('[data-selector="transactions-list"]')
const $validationCount = $('[data-selector="validation-count"]')
const $validationsList = $('[data-selector="validations-list"]')
if ($emptyInternalTransactionsList.length && state.newInternalTransactions.length) window.location.reload()
if ($emptyTransactionsList.length && state.newTransactions.length) window.location.reload()
if (state.channelDisconnected) $channelDisconnected.show()
if (oldState.balance !== state.balance) {
$balance.empty().append(state.balance)
loadTokenBalanceDropdown()
updateAllCalculatedUsdValues()
}
if (oldState.transactionCount !== state.transactionCount) $transactionCount.empty().append(numeral(state.transactionCount).format())
if (oldState.validationCount !== state.validationCount) $validationCount.empty().append(numeral(state.validationCount).format())
if (state.batchCountAccumulator) {
$channelBatching.show()
$channelBatchingCount[0].innerHTML = numeral(state.batchCountAccumulator).format()
render ($el, state, oldState) {
if (oldState.balance === state.balance) return
$el.empty().append(state.balance)
loadTokenBalanceDropdown()
updateAllCalculatedUsdValues()
}
},
'[data-selector="transaction-count"]': {
load ($el) {
return { transactionCount: numeral($el.text()).value() }
},
render ($el, state, oldState) {
if (oldState.transactionCount === state.transactionCount) return
$el.empty().append(numeral(state.transactionCount).format())
}
},
'[data-selector="validation-count"]': {
load ($el) {
return { validationCount: numeral($el.text()).value }
},
render ($el, state, oldState) {
if (oldState.validationCount === state.validationCount) return
$el.empty().append(numeral(state.validationCount).format())
}
},
'[data-selector="loading-next-page"]': {
render ($el, state) {
if (state.loadingNextPage) {
$el.show()
} else {
$channelBatching.hide()
$el.hide()
}
if (oldState.newInternalTransactions !== state.newInternalTransactions && $internalTransactionsList.length) {
slideDownPrepend($internalTransactionsList, state.newInternalTransactions.slice(oldState.newInternalTransactions.length).reverse().join(''))
updateAllAges()
}
},
'[data-selector="paging-error-message"]': {
render ($el, state) {
if (state.pagingError) {
$el.show()
}
if (oldState.pendingTransactionHashes.length !== state.pendingTransactionHashes.length && $pendingTransactionsCount.length) {
$pendingTransactionsCount[0].innerHTML = numeral(state.pendingTransactionHashes.length).format()
}
},
'[data-selector="pending-transactions-list"]': {
load ($el) {
return {
pendingTransactions: $el.children().map((index, el) => ({
transactionHash: el.dataset.transactionHash,
transactionHtml: el.outerHTML
})).toArray()
}
if (oldState.newPendingTransactions !== state.newPendingTransactions && $pendingTransactionsList.length) {
slideDownPrepend($pendingTransactionsList, state.newPendingTransactions.slice(oldState.newPendingTransactions.length).reverse().join(''))
updateAllAges()
},
render ($el, state, oldState) {
if (oldState.pendingTransactions === state.pendingTransactions) return
const container = $el[0]
const newElements = _.map(state.pendingTransactions, ({ transactionHtml }) => $(transactionHtml)[0])
listMorph(container, newElements, { key: 'dataset.transactionHash' })
}
},
'[data-selector="pending-transactions-count"]': {
render ($el, state, oldState) {
if (oldState.pendingTransactions === state.pendingTransactions) return
$el[0].innerHTML = numeral(state.pendingTransactions.filter(({ validated }) => !validated).length).format()
}
},
'[data-selector="transactions-list"]': {
load ($el, store) {
return {
transactions: $el.children().map((index, el) => ({
transactionHash: el.dataset.transactionHash,
transactionHtml: el.outerHTML
})).toArray()
}
if (oldState.newTransactions !== state.newTransactions && $transactionsList.length) {
const newlyValidatedTransactions = state.newTransactions.slice(oldState.newTransactions.length).reverse()
newlyValidatedTransactions.forEach(({ transactionHash, transactionHtml }) => {
let $transaction = $(`[data-selector="pending-transactions-list"] [data-transaction-hash="${transactionHash}"]`)
$transaction.html($(transactionHtml).html())
if ($transaction.is(':visible')) {
setTimeout(() => {
$transaction.addClass('shrink-out')
setTimeout(() => {
slideUpRemove($transaction)
slideDownPrepend($transactionsList, transactionHtml)
}, 400)
}, 1000)
} else {
$transaction.remove()
slideDownPrepend($transactionsList, transactionHtml)
}
})
updateAllAges()
},
render ($el, state, oldState) {
if (oldState.transactions === state.transactions) return
function updateTransactions () {
const container = $el[0]
const newElements = _.map(state.transactions, ({ transactionHtml }) => $(transactionHtml)[0])
listMorph(container, newElements, { key: 'dataset.transactionHash' })
}
if ($('[data-selector="pending-transactions-list"]').is(':visible')) {
setTimeout(updateTransactions, TRANSACTION_VALIDATED_MOVE_DELAY + 400)
} else {
updateTransactions()
}
}
},
'[data-selector="internal-transactions-list"]': {
load ($el) {
return {
internalTransactions: $el.children().map((index, el) => ({
internalTransactionHtml: el.outerHTML
})).toArray()
}
if (oldState.newBlock !== state.newBlock) {
slideDownPrepend($validationsList, state.newBlock)
updateAllAges()
},
render ($el, state, oldState) {
if (oldState.internalTransactions === state.internalTransactions) return
const container = $el[0]
const newElements = _.map(state.internalTransactions, ({ internalTransactionHtml }) => $(internalTransactionHtml)[0])
listMorph(container, newElements, { key: 'dataset.key' })
}
},
'[data-selector="channel-batching-count"]': {
render ($el, state, oldState) {
const $channelBatching = $('[data-selector="channel-batching-message"]')
if (!state.internalTransactionsBatch.length) return $channelBatching.hide()
$channelBatching.show()
$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' })
}
},
'[data-selector="next-page-button"]': {
load ($el) {
return {
nextPageUrl: `${$el.hide().attr('href')}&type=JSON`
}
}
}
}
const $addressDetailsPage = $('[data-page="address-details"]')
if ($addressDetailsPage.length) {
const store = createStore(reducer)
const addressHash = $addressDetailsPage[0].dataset.pageAddressHash
const { filter, blockNumber } = humps.camelizeKeys(URI(window.location).query(true))
store.dispatch({
type: 'PAGE_LOAD',
addressHash,
filter,
beyondPageOne: !!blockNumber
})
connectElements({ store, elements })
const addressChannel = socket.channel(`addresses:${addressHash}`, {})
addressChannel.join()
addressChannel.onError(() => store.dispatch({
type: 'CHANNEL_DISCONNECTED'
}))
addressChannel.on('balance', (msg) => store.dispatch({
type: 'RECEIVED_UPDATED_BALANCE',
msg: humps.camelizeKeys(msg)
}))
addressChannel.on('internal_transaction', batchChannel((msgs) => store.dispatch({
type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH',
msgs: humps.camelizeKeys(msgs)
})))
addressChannel.on('pending_transaction', (msg) => store.dispatch({
type: 'RECEIVED_NEW_PENDING_TRANSACTION',
msg: humps.camelizeKeys(msg)
}))
addressChannel.on('transaction', (msg) => {
store.dispatch({
type: 'RECEIVED_NEW_TRANSACTION',
msg: humps.camelizeKeys(msg)
})
setTimeout(() => store.dispatch({
type: 'REMOVE_PENDING_TRANSACTION',
msg: humps.camelizeKeys(msg)
}), TRANSACTION_VALIDATED_MOVE_DELAY)
})
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',
msg: humps.camelizeKeys(msg)
}))
$('[data-selector="transactions-list"]').length && onScrollBottom(function loadMoreTransactions () {
const { loadingNextPage, nextPageUrl, pagingError } = store.getState()
if (!loadingNextPage && nextPageUrl && !pagingError) {
store.dispatch({
type: 'LOADING_NEXT_PAGE'
})
$.get(nextPageUrl)
.done(msg => {
store.dispatch({
type: 'RECEIVED_NEXT_TRANSACTIONS_PAGE',
msg: humps.camelizeKeys(msg)
})
})
.fail(() => {
store.dispatch({
type: 'PAGING_ERROR'
})
})
}
})
}

@ -0,0 +1,131 @@
import $ from 'jquery'
import _ from 'lodash'
import URI from 'urijs'
import humps from 'humps'
import numeral from 'numeral'
import socket from '../socket'
import { updateAllAges } from '../lib/from_now'
import { batchChannel, initRedux, slideDownPrepend, slideUpRemove } from '../utils'
const BATCH_THRESHOLD = 10
export const initialState = {
newPendingTransactionHashesBatch: [],
beyondPageOne: null,
channelDisconnected: false,
newPendingTransactions: [],
newTransactionHashes: [],
pendingTransactionCount: null
}
export function reducer (state = initialState, action) {
switch (action.type) {
case 'PAGE_LOAD': {
return Object.assign({}, state, {
beyondPageOne: action.beyondPageOne,
pendingTransactionCount: numeral(action.pendingTransactionCount).value()
})
}
case 'CHANNEL_DISCONNECTED': {
return Object.assign({}, state, {
channelDisconnected: true
})
}
case 'RECEIVED_NEW_TRANSACTION': {
if (state.channelDisconnected) return state
return Object.assign({}, state, {
newPendingTransactionHashesBatch: _.without(state.newPendingTransactionHashesBatch, action.msg.transactionHash),
pendingTransactionCount: state.pendingTransactionCount - 1,
newTransactionHashes: [action.msg.transactionHash]
})
}
case 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH': {
if (state.channelDisconnected) return state
const pendingTransactionCount = state.pendingTransactionCount + action.msgs.length
if (state.beyondPageOne) return Object.assign({}, state, { pendingTransactionCount })
if (!state.newPendingTransactionHashesBatch.length && action.msgs.length < BATCH_THRESHOLD) {
return Object.assign({}, state, {
newPendingTransactions: [
...state.newPendingTransactions,
..._.map(action.msgs, 'transactionHtml')
],
pendingTransactionCount
})
} else {
return Object.assign({}, state, {
newPendingTransactionHashesBatch: [
...state.newPendingTransactionHashesBatch,
..._.map(action.msgs, 'transactionHash')
],
pendingTransactionCount
})
}
}
default:
return state
}
}
const $transactionPendingListPage = $('[data-page="transaction-pending-list"]')
if ($transactionPendingListPage.length) {
initRedux(reducer, {
main (store) {
store.dispatch({
type: 'PAGE_LOAD',
pendingTransactionCount: $('[data-selector="transaction-pending-count"]').text(),
beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).insertedAt
})
const transactionsChannel = socket.channel(`transactions:new_transaction`)
transactionsChannel.join()
transactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' }))
transactionsChannel.on('transaction', (msg) =>
store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION', msg: humps.camelizeKeys(msg) })
)
const pendingTransactionsChannel = socket.channel(`transactions:new_pending_transaction`)
pendingTransactionsChannel.join()
pendingTransactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' }))
pendingTransactionsChannel.on('pending_transaction', batchChannel((msgs) =>
store.dispatch({ type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) }))
)
},
render (state, oldState) {
const $channelBatching = $('[data-selector="channel-batching-message"]')
const $channelBatchingCount = $('[data-selector="channel-batching-count"]')
const $channelDisconnected = $('[data-selector="channel-disconnected-message"]')
const $pendingTransactionsList = $('[data-selector="transactions-pending-list"]')
const $pendingTransactionsCount = $('[data-selector="transaction-pending-count"]')
if (state.channelDisconnected) $channelDisconnected.show()
if (oldState.pendingTransactionCount !== state.pendingTransactionCount) {
$pendingTransactionsCount.empty().append(numeral(state.pendingTransactionCount).format())
}
if (oldState.newTransactionHashes !== state.newTransactionHashes && state.newTransactionHashes.length > 0) {
const $transaction = $(`[data-transaction-hash="${state.newTransactionHashes[0]}"]`)
$transaction.addClass('shrink-out')
setTimeout(() => {
if ($transaction.length === 1 && $transaction.siblings().length === 0 && state.pendingTransactionCount > 0) {
window.location.href = URI(window.location).removeQuery('inserted_at').removeQuery('hash').toString()
} else {
slideUpRemove($transaction)
}
}, 400)
}
if (state.newPendingTransactionHashesBatch.length) {
$channelBatching.show()
$channelBatchingCount[0].innerHTML = numeral(state.newPendingTransactionHashesBatch.length).format()
} else {
$channelBatching.hide()
}
if (oldState.newPendingTransactions !== state.newPendingTransactions) {
const newTransactionsToInsert = state.newPendingTransactions.slice(oldState.newPendingTransactions.length)
slideDownPrepend($pendingTransactionsList, newTransactionsToInsert.reverse().join(''))
updateAllAges()
}
}
})
}

@ -1,42 +1,19 @@
import $ from 'jquery'
import _ from 'lodash'
import URI from 'urijs'
import humps from 'humps'
import numeral from 'numeral'
import socket from '../socket'
import { updateAllAges } from '../lib/from_now'
import { batchChannel, initRedux, slideDownPrepend, slideUpRemove } from '../utils'
const BATCH_THRESHOLD = 10
import { initRedux } from '../utils'
export const initialState = {
batchCountAccumulator: 0,
newPendingTransactionHashesBatch: [],
beyondPageOne: null,
blockNumber: null,
channelDisconnected: false,
confirmations: null,
newPendingTransactions: [],
newTransactions: [],
newTransactionHashes: [],
transactionCount: null,
pendingTransactionCount: null
confirmations: null
}
export function reducer (state = initialState, action) {
switch (action.type) {
case 'PAGE_LOAD': {
return Object.assign({}, state, {
beyondPageOne: action.beyondPageOne,
blockNumber: parseInt(action.blockNumber, 10),
transactionCount: numeral(action.transactionCount).value(),
pendingTransactionCount: numeral(action.pendingTransactionCount).value()
})
}
case 'CHANNEL_DISCONNECTED': {
return Object.assign({}, state, {
channelDisconnected: true,
batchCountAccumulator: 0
blockNumber: parseInt(action.blockNumber, 10)
})
}
case 'RECEIVED_NEW_BLOCK': {
@ -46,62 +23,6 @@ export function reducer (state = initialState, action) {
})
} else return state
}
case 'RECEIVED_NEW_TRANSACTION': {
if (state.channelDisconnected) return state
return Object.assign({}, state, {
newPendingTransactionHashesBatch: _.without(state.newPendingTransactionHashesBatch, action.msg.transactionHash),
pendingTransactionCount: state.pendingTransactionCount - 1,
newTransactionHashes: [action.msg.transactionHash]
})
}
case 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH': {
if (state.channelDisconnected) return state
const pendingTransactionCount = state.pendingTransactionCount + action.msgs.length
if (state.beyondPageOne) return Object.assign({}, state, { pendingTransactionCount })
if (!state.newPendingTransactionHashesBatch.length && action.msgs.length < BATCH_THRESHOLD) {
return Object.assign({}, state, {
newPendingTransactions: [
...state.newPendingTransactions,
..._.map(action.msgs, 'transactionHtml')
],
pendingTransactionCount
})
} else {
return Object.assign({}, state, {
newPendingTransactionHashesBatch: [
...state.newPendingTransactionHashesBatch,
..._.map(action.msgs, 'transactionHash')
],
pendingTransactionCount
})
}
}
case 'RECEIVED_NEW_TRANSACTION_BATCH': {
if (state.channelDisconnected) return state
const transactionCount = state.transactionCount + action.msgs.length
if (state.beyondPageOne) return Object.assign({}, state, { transactionCount })
if (!state.batchCountAccumulator && action.msgs.length < BATCH_THRESHOLD) {
return Object.assign({}, state, {
newTransactions: [
...state.newTransactions,
..._.map(action.msgs, 'transactionHtml')
],
transactionCount
})
} else {
return Object.assign({}, state, {
batchCountAccumulator: state.batchCountAccumulator + action.msgs.length,
transactionCount
})
}
}
default:
return state
}
@ -134,104 +55,3 @@ if ($transactionDetailsPage.length) {
}
})
}
const $transactionPendingListPage = $('[data-page="transaction-pending-list"]')
if ($transactionPendingListPage.length) {
initRedux(reducer, {
main (store) {
store.dispatch({
type: 'PAGE_LOAD',
pendingTransactionCount: $('[data-selector="transaction-pending-count"]').text(),
beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).insertedAt
})
const transactionsChannel = socket.channel(`transactions:new_transaction`)
transactionsChannel.join()
transactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' }))
transactionsChannel.on('transaction', (msg) =>
store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION', msg: humps.camelizeKeys(msg) })
)
const pendingTransactionsChannel = socket.channel(`transactions:new_pending_transaction`)
pendingTransactionsChannel.join()
pendingTransactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' }))
pendingTransactionsChannel.on('pending_transaction', batchChannel((msgs) =>
store.dispatch({ type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) }))
)
},
render (state, oldState) {
const $channelBatching = $('[data-selector="channel-batching-message"]')
const $channelBatchingCount = $('[data-selector="channel-batching-count"]')
const $channelDisconnected = $('[data-selector="channel-disconnected-message"]')
const $pendingTransactionsList = $('[data-selector="transactions-pending-list"]')
const $pendingTransactionsCount = $('[data-selector="transaction-pending-count"]')
if (state.channelDisconnected) $channelDisconnected.show()
if (oldState.pendingTransactionCount !== state.pendingTransactionCount) {
$pendingTransactionsCount.empty().append(numeral(state.pendingTransactionCount).format())
}
if (oldState.newTransactionHashes !== state.newTransactionHashes && state.newTransactionHashes.length > 0) {
const $transaction = $(`[data-transaction-hash="${state.newTransactionHashes[0]}"]`)
$transaction.addClass('shrink-out')
setTimeout(() => {
if ($transaction.length === 1 && $transaction.siblings().length === 0 && state.pendingTransactionCount > 0) {
window.location.href = URI(window.location).removeQuery('inserted_at').removeQuery('hash').toString()
} else {
slideUpRemove($transaction)
}
}, 400)
}
if (state.newPendingTransactionHashesBatch.length) {
$channelBatching.show()
$channelBatchingCount[0].innerHTML = numeral(state.newPendingTransactionHashesBatch.length).format()
} else {
$channelBatching.hide()
}
if (oldState.newPendingTransactions !== state.newPendingTransactions) {
const newTransactionsToInsert = state.newPendingTransactions.slice(oldState.newPendingTransactions.length)
slideDownPrepend($pendingTransactionsList, newTransactionsToInsert.reverse().join(''))
updateAllAges()
}
}
})
}
const $transactionListPage = $('[data-page="transaction-list"]')
if ($transactionListPage.length) {
initRedux(reducer, {
main (store) {
store.dispatch({
type: 'PAGE_LOAD',
transactionCount: $('[data-selector="transaction-count"]').text(),
beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).index
})
const transactionsChannel = socket.channel(`transactions:new_transaction`)
transactionsChannel.join()
transactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' }))
transactionsChannel.on('transaction', batchChannel((msgs) =>
store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) }))
)
},
render (state, oldState) {
const $channelBatching = $('[data-selector="channel-batching-message"]')
const $channelBatchingCount = $('[data-selector="channel-batching-count"]')
const $channelDisconnected = $('[data-selector="channel-disconnected-message"]')
const $transactionsList = $('[data-selector="transactions-list"]')
const $transactionCount = $('[data-selector="transaction-count"]')
if (state.channelDisconnected) $channelDisconnected.show()
if (oldState.transactionCount !== state.transactionCount) $transactionCount.empty().append(numeral(state.transactionCount).format())
if (state.batchCountAccumulator) {
$channelBatching.show()
$channelBatchingCount[0].innerHTML = numeral(state.batchCountAccumulator).format()
} else {
$channelBatching.hide()
}
if (oldState.newTransactions !== state.newTransactions) {
const newTransactionsToInsert = state.newTransactions.slice(oldState.newTransactions.length)
slideDownPrepend($transactionsList, newTransactionsToInsert.reverse().join(''))
updateAllAges()
}
}
})
}

@ -0,0 +1,100 @@
import $ from 'jquery'
import _ from 'lodash'
import URI from 'urijs'
import humps from 'humps'
import numeral from 'numeral'
import socket from '../socket'
import { updateAllAges } from '../lib/from_now'
import { batchChannel, initRedux, slideDownPrepend } from '../utils'
const BATCH_THRESHOLD = 10
export const initialState = {
batchCountAccumulator: 0,
beyondPageOne: null,
channelDisconnected: false,
newTransactions: [],
transactionCount: null
}
export function reducer (state = initialState, action) {
switch (action.type) {
case 'PAGE_LOAD': {
return Object.assign({}, state, {
beyondPageOne: action.beyondPageOne,
transactionCount: numeral(action.transactionCount).value()
})
}
case 'CHANNEL_DISCONNECTED': {
return Object.assign({}, state, {
channelDisconnected: true,
batchCountAccumulator: 0
})
}
case 'RECEIVED_NEW_TRANSACTION_BATCH': {
if (state.channelDisconnected) return state
const transactionCount = state.transactionCount + action.msgs.length
if (state.beyondPageOne) return Object.assign({}, state, { transactionCount })
if (!state.batchCountAccumulator && action.msgs.length < BATCH_THRESHOLD) {
return Object.assign({}, state, {
newTransactions: [
...state.newTransactions,
..._.map(action.msgs, 'transactionHtml')
],
transactionCount
})
} else {
return Object.assign({}, state, {
batchCountAccumulator: state.batchCountAccumulator + action.msgs.length,
transactionCount
})
}
}
default:
return state
}
}
const $transactionListPage = $('[data-page="transaction-list"]')
if ($transactionListPage.length) {
initRedux(reducer, {
main (store) {
store.dispatch({
type: 'PAGE_LOAD',
transactionCount: $('[data-selector="transaction-count"]').text(),
beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).index
})
const transactionsChannel = socket.channel(`transactions:new_transaction`)
transactionsChannel.join()
transactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' }))
transactionsChannel.on('transaction', batchChannel((msgs) =>
store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) }))
)
},
render (state, oldState) {
const $channelBatching = $('[data-selector="channel-batching-message"]')
const $channelBatchingCount = $('[data-selector="channel-batching-count"]')
const $channelDisconnected = $('[data-selector="channel-disconnected-message"]')
const $transactionsList = $('[data-selector="transactions-list"]')
const $transactionCount = $('[data-selector="transaction-count"]')
if (state.channelDisconnected) $channelDisconnected.show()
if (oldState.transactionCount !== state.transactionCount) $transactionCount.empty().append(numeral(state.transactionCount).format())
if (state.batchCountAccumulator) {
$channelBatching.show()
$channelBatchingCount[0].innerHTML = numeral(state.batchCountAccumulator).format()
} else {
$channelBatching.hide()
}
if (oldState.newTransactions !== state.newTransactions) {
const newTransactionsToInsert = state.newTransactions.slice(oldState.newTransactions.length)
slideDownPrepend($transactionsList, newTransactionsToInsert.reverse().join(''))
updateAllAges()
}
}
})
}

@ -1,6 +1,8 @@
import $ from 'jquery'
import _ from 'lodash'
import { createStore } from 'redux'
import { createStore as reduxCreateStore } from 'redux'
import morph from 'nanomorph'
import { updateAllAges } from './lib/from_now'
export function batchChannel (func) {
let msgs = []
@ -40,6 +42,37 @@ export function initRedux (reducer, { main, render, debug } = {}) {
if (main) main(store)
}
export function createStore (reducer) {
return reduxCreateStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__())
}
export function connectElements ({ elements, store }) {
function loadElements () {
return _.reduce(elements, (pageLoadParams, { load }, selector) => {
if (!load) return pageLoadParams
const $el = $(selector)
if (!$el.length) return pageLoadParams
const morePageLoadParams = load($el, store)
return _.isObject(morePageLoadParams) ? Object.assign(pageLoadParams, morePageLoadParams) : pageLoadParams
}, {})
}
function renderElements (state, oldState) {
_.forIn(elements, ({ render }, selector) => {
if (!render) return
const $el = $(selector)
if (!$el.length) return
render($el, state, oldState)
})
}
store.dispatch(Object.assign(loadElements(), { type: 'ELEMENTS_LOAD' }))
let oldState = store.getState()
store.subscribe(() => {
const state = store.getState()
renderElements(state, oldState)
oldState = state
})
}
export function skippedBlockListBuilder (skippedBlockNumbers, newestBlock, oldestBlock) {
for (let i = newestBlock - 1; i > oldestBlock; i--) skippedBlockNumbers.push(i)
return skippedBlockNumbers
@ -52,6 +85,13 @@ export function slideDownPrepend ($container, content) {
}
})
}
export function slideDownAppend ($container, content) {
smarterSlideDown($(content), {
insert ($el) {
$container.append($el)
}
})
}
export function slideDownBefore ($container, content) {
smarterSlideDown($(content), {
insert ($el) {
@ -105,3 +145,71 @@ function smarterSlideUp ($el, { complete = _.noop } = {}) {
$el.slideUp({ complete, easing: 'linear' })
}
}
// The goal of this function is to DOM diff lists, so upon completion `container.innerHTML` should be
// equivalent to `newElements.join('')`.
//
// We could simply do `container.innerHTML = newElements.join('')` but that would not be efficient and
// it not animate appropriately. We could also simply use `morph` (or a similar library) on the entire
// list, however that doesn't give us the proper amount of control for animations.
//
// This function will walk though, remove items currently in `container` which are not in the new list.
// Then it will swap the contents of the items that are in both lists in case the items were updated or
// the order changed. Finally, it will add elements to `container` which are in the new list and didn't
// already exist in the DOM.
//
// Params:
// container: the DOM element which contents need replaced
// newElements: a list of elements that need to be put into the container
// options:
// key: the path to the unique identifier of each element
// horizontal: our horizontal animations are handled in CSS, so passing in `true` will not play JS
// animations
export function listMorph (container, newElements, { key, horizontal } = {}) {
if (!container) return
const oldElements = $(container).children().get()
let currentList = _.map(oldElements, (el) => ({ id: _.get(el, key), el }))
const newList = _.map(newElements, (el) => ({ id: _.get(el, key), el }))
const overlap = _.intersectionBy(newList, currentList, 'id').map(({ id, el }) => ({ id, el: updateAllAges($(el))[0] }))
// remove old items
const removals = _.differenceBy(currentList, newList, 'id')
let canAnimate = !horizontal && removals.length <= 1
removals.forEach(({ el }) => {
if (!canAnimate) return el.remove()
const $el = $(el)
$el.addClass('shrink-out')
setTimeout(() => { slideUpRemove($el) }, 400)
})
currentList = _.differenceBy(currentList, removals, 'id')
// update kept items
currentList = currentList.map(({ el }, i) => ({
id: overlap[i].id,
el: el.outerHTML === overlap[i].el.outerHTML ? el : morph(el, overlap[i].el)
}))
// add new items
const finalList = newList.map(({ id, el }) => _.get(_.find(currentList, { id }), 'el', el)).reverse()
canAnimate = !horizontal
finalList.forEach((el, i) => {
if (el.parentElement) return
if (!canAnimate) return container.insertBefore(el, _.get(finalList, `[${i - 1}]`))
canAnimate = false
if (!_.get(finalList, `[${i - 1}]`)) return slideDownAppend($(container), el)
slideDownBefore($(_.get(finalList, `[${i - 1}]`)), el)
})
}
export function onScrollBottom (callback) {
const $window = $(window)
function infiniteScrollChecker () {
const scrollHeight = $(document).height()
const scrollPosition = $window.height() + $window.scrollTop()
if ((scrollHeight - scrollPosition) / scrollHeight === 0) {
callback()
}
}
infiniteScrollChecker()
$window.on('scroll', infiniteScrollChecker)
}

@ -3789,8 +3789,7 @@
"version": "2.1.1",
"resolved": false,
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"dev": true,
"optional": true
"dev": true
},
"aproba": {
"version": "1.2.0",
@ -3814,15 +3813,13 @@
"version": "1.0.0",
"resolved": false,
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"resolved": false,
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -3839,22 +3836,19 @@
"version": "1.1.0",
"resolved": false,
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"dev": true,
"optional": true
"dev": true
},
"concat-map": {
"version": "0.0.1",
"resolved": false,
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"resolved": false,
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
"dev": true,
"optional": true
"dev": true
},
"core-util-is": {
"version": "1.0.2",
@ -3985,8 +3979,7 @@
"version": "2.0.3",
"resolved": false,
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
@ -4000,7 +3993,6 @@
"resolved": false,
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -4017,7 +4009,6 @@
"resolved": false,
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -4026,15 +4017,13 @@
"version": "0.0.8",
"resolved": false,
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"dev": true,
"optional": true
"dev": true
},
"minipass": {
"version": "2.2.4",
"resolved": false,
"integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==",
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@ -4055,7 +4044,6 @@
"resolved": false,
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -4144,8 +4132,7 @@
"version": "1.0.1",
"resolved": false,
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"dev": true,
"optional": true
"dev": true
},
"object-assign": {
"version": "4.1.1",
@ -4159,7 +4146,6 @@
"resolved": false,
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -4255,8 +4241,7 @@
"version": "5.1.1",
"resolved": false,
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
"dev": true,
"optional": true
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
@ -4298,7 +4283,6 @@
"resolved": false,
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -4320,7 +4304,6 @@
"resolved": false,
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -4369,15 +4352,13 @@
"version": "1.0.2",
"resolved": false,
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true,
"optional": true
"dev": true
},
"yallist": {
"version": "3.0.2",
"resolved": false,
"integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=",
"dev": true,
"optional": true
"dev": true
}
}
},
@ -6428,6 +6409,11 @@
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==",
"dev": true
},
"nanoassert": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-1.1.0.tgz",
"integrity": "sha1-TzFS4JVA/eKMdvRLGbvNHVpCR40="
},
"nanomatch": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@ -6455,6 +6441,14 @@
}
}
},
"nanomorph": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/nanomorph/-/nanomorph-5.1.3.tgz",
"integrity": "sha512-VydkKjFWU/DAO0R10awFASRNXQKHrZUMdMIiNcdmWm+IhuifuPOw/dDtpiQ1cNROF8f3ATPrcKRVarEayQJOqA==",
"requires": {
"nanoassert": "^1.1.0"
}
},
"natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",

@ -28,6 +28,7 @@
"jquery": "^3.3.1",
"lodash": "^4.17.10",
"moment": "^2.22.1",
"nanomorph": "^5.1.3",
"numeral": "^2.0.6",
"path-parser": "^4.1.1",
"phoenix": "file:../../../deps/phoenix",

@ -16,7 +16,7 @@ defmodule BlockScoutWeb.Chain do
alias Explorer.Chain.{
Address,
Address.TokenBalance,
Address.CurrentTokenBalance,
Block,
InternalTransaction,
Log,
@ -198,7 +198,7 @@ defmodule BlockScoutWeb.Chain do
%{"token_name" => name, "token_type" => type, "token_inserted_at" => inserted_at_datetime}
end
defp paging_params(%TokenBalance{address_hash: address_hash, value: value}) do
defp paging_params(%CurrentTokenBalance{address_hash: address_hash, value: value}) do
%{"address_hash" => to_string(address_hash), "value" => Decimal.to_integer(value)}
end

@ -7,9 +7,10 @@ defmodule BlockScoutWeb.AddressController do
def index(conn, _params) do
render(conn, "index.html",
addresses: Chain.list_top_addresses(),
address_tx_count_pairs: Chain.list_top_addresses(),
address_estimated_count: Chain.address_estimated_count(),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null()
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
total_supply: Chain.total_supply()
)
end

@ -8,34 +8,87 @@ defmodule BlockScoutWeb.AddressTransactionController do
import BlockScoutWeb.AddressController, only: [transaction_count: 1, validation_count: 1]
import BlockScoutWeb.Chain, only: [current_filter: 1, paging_options: 1, next_page_params: 3, split_list_by_page: 1]
alias BlockScoutWeb.TransactionView
alias Explorer.{Chain, Market}
alias Explorer.Chain.Hash
alias Explorer.ExchangeRates.Token
alias Phoenix.View
@transaction_necessity_by_association [
necessity_by_association: %{
[created_contract_address: :names] => :optional,
[from_address: :names] => :optional,
[to_address: :names] => :optional,
:token_transfers => :optional
}
]
def index(conn, %{"address_id" => address_hash_string, "type" => "JSON"} = params) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.hash_to_address(address_hash) do
options =
@transaction_necessity_by_association
|> put_in([:necessity_by_association, :block], :required)
|> Keyword.merge(paging_options(params))
|> Keyword.merge(current_filter(params))
{transactions, next_page} = get_transactions_and_next_page(address, options)
next_page_url =
case next_page_params(next_page, transactions, params) do
nil ->
nil
next_page_params ->
address_transaction_path(
conn,
:index,
address,
next_page_params
)
end
json(
conn,
%{
transactions:
Enum.map(transactions, fn transaction ->
%{
transaction_hash: Hash.to_string(transaction.hash),
transaction_html:
View.render_to_string(
TransactionView,
"_tile.html",
current_address: address,
transaction: transaction
)
}
end),
next_page_url: next_page_url
}
)
else
:error ->
unprocessable_entity(conn)
{:error, :not_found} ->
not_found(conn)
end
end
def index(conn, %{"address_id" => address_hash_string} = params) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.hash_to_address(address_hash) do
pending_options =
[
necessity_by_association: %{
[created_contract_address: :names] => :optional,
[from_address: :names] => :optional,
[to_address: :names] => :optional,
:token_transfers => :optional
}
]
|> Keyword.merge(paging_options(params))
@transaction_necessity_by_association
|> Keyword.merge(paging_options(%{}))
|> Keyword.merge(current_filter(params))
full_options = put_in(pending_options, [:necessity_by_association, :block], :required)
transactions_plus_one = Chain.address_to_transactions(address, full_options)
{transactions, next_page} = split_list_by_page(transactions_plus_one)
{transactions, next_page} = get_transactions_and_next_page(address, full_options)
pending_transactions =
case Map.has_key?(params, "block_number") do
true -> []
false -> Chain.address_to_pending_transactions(address, pending_options)
end
pending_transactions = Chain.address_to_pending_transactions(address, pending_options)
render(
conn,
@ -57,4 +110,9 @@ defmodule BlockScoutWeb.AddressTransactionController do
not_found(conn)
end
end
defp get_transactions_and_next_page(address, options) do
transactions_plus_one = Chain.address_to_transactions(address, options)
split_list_by_page(transactions_plus_one)
end
end

@ -3,6 +3,26 @@ defmodule BlockScoutWeb.API.RPC.TransactionController do
alias Explorer.Chain
def gettxinfo(conn, params) do
with {:txhash_param, {:ok, txhash_param}} <- fetch_txhash(params),
{:format, {:ok, transaction_hash}} <- to_transaction_hash(txhash_param),
{:transaction, {:ok, transaction}} <- transaction_from_hash(transaction_hash) do
max_block_number = max_block_number()
logs = Chain.transaction_to_logs(transaction)
render(conn, :gettxinfo, %{transaction: transaction, max_block_number: max_block_number, logs: logs})
else
{:transaction, :error} ->
render(conn, :error, error: "Transaction not found")
{:txhash_param, :error} ->
render(conn, :error, error: "Query parameter txhash is required")
{:format, :error} ->
render(conn, :error, error: "Invalid txhash format")
end
end
def gettxreceiptstatus(conn, params) do
with {:txhash_param, {:ok, txhash_param}} <- fetch_txhash(params),
{:format, {:ok, transaction_hash}} <- to_transaction_hash(txhash_param) do
@ -35,6 +55,13 @@ defmodule BlockScoutWeb.API.RPC.TransactionController do
{:txhash_param, Map.fetch(params, "txhash")}
end
defp transaction_from_hash(transaction_hash) do
case Chain.hash_to_transaction(transaction_hash, necessity_by_association: %{block: :required}) do
{:error, :not_found} -> {:transaction, :error}
{:ok, transaction} -> {:transaction, {:ok, transaction}}
end
end
defp to_transaction_hash(transaction_hash_string) do
{:format, Chain.string_to_transaction_hash(transaction_hash_string)}
end
@ -54,4 +81,11 @@ defmodule BlockScoutWeb.API.RPC.TransactionController do
_ -> ""
end
end
defp max_block_number do
case Chain.max_block_number() do
{:ok, number} -> number
{:error, :not_found} -> 0
end
end
end

@ -321,6 +321,30 @@ defmodule BlockScoutWeb.Etherscan do
"result" => nil
}
@transaction_gettxinfo_example_value %{
"status" => "1",
"result" => %{
"blockNumber" => "3",
"confirmations" => "0",
"from" => "0x000000000000000000000000000000000000000c",
"gasLimit" => "91966",
"gasUsed" => "95123",
"hash" => "0x0000000000000000000000000000000000000000000000000000000000000004",
"input" => "0x04",
"logs" => [
%{
"address" => "0x000000000000000000000000000000000000000e",
"data" => "0x00",
"topics" => ["First Topic", "Second Topic", "Third Topic", "Fourth Topic"]
}
],
"success" => true,
"timeStamp" => "1541018182",
"to" => "0x000000000000000000000000000000000000000d",
"value" => "67612"
}
}
@transaction_gettxreceiptstatus_example_value %{
"status" => "1",
"message" => "OK",
@ -428,6 +452,28 @@ defmodule BlockScoutWeb.Etherscan do
example: ~s("18")
}
@logs_details %{
name: "Log Detail",
fields: %{
address: @address_hash_type,
topics: %{
type: "topics",
definition: "An array including the topics for the log.",
example: ~s(["0xf63780e752c6a54a94fc52715dbc5518a3b4c3c2833d301a204226548a2a8545"])
},
data: %{
type: "data",
definition: "Non-indexed log parameters.",
example: ~s("0x")
},
blockNumber: %{
type: "block number",
definition: "A nonnegative number used to identify blocks.",
example: ~s("0x5c958")
}
}
}
@address_balance %{
name: "AddressBalance",
fields: %{
@ -735,6 +781,35 @@ defmodule BlockScoutWeb.Etherscan do
}
}
@transaction_info_model %{
name: "TransactionInfo",
fields: %{
hash: @transaction_hash_type,
timeStamp: %{
type: "timestamp",
definition: "The transaction's block-timestamp.",
example: ~s("1439232889")
},
blockNumber: @block_number_type,
confirmations: @confirmation_type,
success: %{
type: "boolean",
definition: "Flag for success during tx execution",
example: ~s(true)
},
from: @address_hash_type,
to: @address_hash_type,
value: @wei_type,
input: @input_type,
gasLimit: @wei_type,
gasUsed: @gas_type,
logs: %{
type: "array",
array_type: @logs_details
}
}
}
@transaction_status_model %{
name: "TransactionStatus",
fields: %{
@ -1569,6 +1644,43 @@ defmodule BlockScoutWeb.Etherscan do
]
}
@transaction_gettxinfo_action %{
name: "gettxinfo",
description: "Get transaction info.",
required_params: [
%{
key: "txhash",
placeholder: "transactionHash",
type: "string",
description: "Transaction hash. Hash of contents of the transaction."
}
],
optional_params: [],
responses: [
%{
code: "200",
description: "successful operation",
example_value: Jason.encode!(@transaction_gettxinfo_example_value),
model: %{
name: "Result",
fields: %{
status: @status_type,
message: @message_type,
result: %{
type: "model",
model: @transaction_info_model
}
}
}
},
%{
code: "200",
description: "error",
example_value: Jason.encode!(@transaction_gettxreceiptstatus_example_value_error)
}
]
}
@transaction_gettxreceiptstatus_action %{
name: "gettxreceiptstatus",
description: "Get transaction receipt status.",
@ -1692,6 +1804,7 @@ defmodule BlockScoutWeb.Etherscan do
@transaction_module %{
name: "transaction",
actions: [
@transaction_gettxinfo_action,
@transaction_gettxreceiptstatus_action,
@transaction_getstatus_action
]

@ -0,0 +1,12 @@
defmodule BlockScoutWeb.Resolvers.Address do
@moduledoc false
alias Explorer.Chain
def get_by(_, %{hashes: hashes}, _) do
case Chain.hashes_to_addresses(hashes) do
[] -> {:error, "Addresses not found."}
result -> {:ok, result}
end
end
end

@ -38,12 +38,18 @@ defmodule BlockScoutWeb.Router do
})
end
forward("/graphql", Absinthe.Plug, schema: BlockScoutWeb.Schema)
forward("/graphql", Absinthe.Plug,
schema: BlockScoutWeb.Schema,
analyze_complexity: true,
max_complexity: 50
)
forward("/graphiql", Absinthe.Plug.GraphiQL,
schema: BlockScoutWeb.Schema,
interface: :playground,
socket: BlockScoutWeb.UserSocket
socket: BlockScoutWeb.UserSocket,
analyze_complexity: true,
max_complexity: 50
)
scope "/", BlockScoutWeb do

@ -3,11 +3,18 @@ defmodule BlockScoutWeb.Schema do
use Absinthe.Schema
alias BlockScoutWeb.Resolvers.{Block, Transaction}
alias BlockScoutWeb.Resolvers.{Address, Block, Transaction}
import_types(BlockScoutWeb.Schema.Types)
query do
@desc "Gets addresses by address hash."
field :addresses, list_of(:address) do
arg(:hashes, non_null(list_of(non_null(:address_hash))))
resolve(&Address.get_by/3)
complexity(fn %{hashes: hashes}, child_complexity -> length(hashes) * child_complexity end)
end
@desc "Gets a block by number."
field :block, :block do
arg(:number, non_null(:integer))

@ -3,7 +3,7 @@ defmodule BlockScoutWeb.Schema.Scalars do
use Absinthe.Schema.Notation
alias Explorer.Chain.{Hash, Wei}
alias Explorer.Chain.{Data, Hash, Wei}
alias Explorer.Chain.Hash.{Address, Full, Nonce}
@desc """
@ -24,6 +24,23 @@ defmodule BlockScoutWeb.Schema.Scalars do
serialize(&to_string/1)
end
@desc """
An unpadded hexadecimal number with 0 or more digits. Each pair of digits
maps directly to a byte in the underlying binary representation. When
interpreted as a number, it should be treated as big-endian.
"""
scalar :data do
parse(fn
%Absinthe.Blueprint.Input.String{value: value} ->
Data.cast(value)
_ ->
:error
end)
serialize(&to_string/1)
end
@desc """
A 32-byte [KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash.
"""

@ -6,12 +6,23 @@ defmodule BlockScoutWeb.Schema.Types do
import_types(Absinthe.Type.Custom)
import_types(BlockScoutWeb.Schema.Scalars)
@desc """
A stored representation of a Web3 address.
"""
object :address do
field(:hash, :address_hash)
field(:fetched_coin_balance, :wei)
field(:fetched_coin_balance_block_number, :integer)
field(:contract_code, :data)
end
@desc """
A package of data that contains zero or more transactions, the hash of the previous block ("parent"), and optionally
other data. Because each block (except for the initial "genesis block") points to the previous block, the data
structure that they form is called a "blockchain".
"""
object :block do
field(:hash, :full_hash)
field(:consensus, :boolean)
field(:difficulty, :decimal)
field(:gas_limit, :decimal)

@ -15,7 +15,7 @@
<!-- number of txns for this address -->
<span class="mr-4">
<span data-test="transaction_count">
<%= transaction_count(@address) %>
<%= @tx_count %>
</span> <%= gettext "Transactions sent" %>
<% if validator?(@address) do %>
<span data-test="validation_count">
@ -36,7 +36,7 @@
data-usd-exchange-rate="<%= @exchange_rate.usd_value %>">
</span>
<!-- percentage of coins from total supply -->
<span class="ml-0 ml-md-2">(<%= balance_percentage(@address) %>)</span>
<span class="ml-0 ml-md-2">(<%= balance_percentage(@address, @total_supply) %>)</span>
</div>
</div>
</div>

@ -9,9 +9,10 @@
</p>
<span data-selector="top-addresses-list">
<%= for {address, index} <- Enum.with_index(@addresses, 1) do %>
<%= for {{address, tx_count}, index} <- Enum.with_index(@address_tx_count_pairs, 1) do %>
<%= render "_tile.html",
address: address, index: index, exchange_rate: @exchange_rate,
total_supply: @total_supply, tx_count: tx_count,
validation_count: validation_count(address) %>
<% end %>
</span>

@ -8,12 +8,12 @@
</div>
<div class="card-body">
<div data-selector="channel-batching-message" class="d-none">
<div data-selector="channel-batching-message" style="display: none;">
<div data-selector="reload-button" class="alert alert-info">
<a href="#" class="alert-link"><span data-selector="channel-batching-count"></span> <%= gettext "More internal transactions have come in" %></a>
</div>
</div>
<div data-selector="channel-disconnected-message" class="d-none">
<div data-selector="channel-disconnected-message" style="display: none;">
<div data-selector="reload-button" class="alert alert-danger">
<a href="#" class="alert-link"><%= gettext "Connection Lost, click to load newer internal transactions" %></a>
</div>

@ -9,7 +9,7 @@
</div>
<div class="card-body">
<div data-selector="channel-disconnected-message" class="d-none">
<div data-selector="channel-disconnected-message" style="display: none;">
<div data-selector="reload-button" class="alert alert-danger">
<a href="#" class="alert-link"><%= gettext "Connection Lost, click to load newer transactions" %></a>
</div>
@ -52,17 +52,19 @@
</div>
<h2 class="card-title"><%= gettext "Transactions" %></h2>
<div data-selector="pending-transactions-toggle">
<%= link to: "#pending-transactions", class: "d-inline-block mb-3", "data-toggle": "collapse" do %>
<%= link to: "#pending-transactions-container", class: "d-inline-block mb-3", "data-toggle": "collapse" do %>
<span data-selector="pending-transactions-open">
<%= gettext("Show") %></span>
<span data-selector="pending-transactions-close" class="d-none"><%= gettext("Hide") %></span>
<span data-selector="pending-transactions-count"><%= length(@pending_transactions) %></span>
<%= gettext("Pending Transactions") %>
<% end %>
<div class="mb-3 collapse" id="pending-transactions" data-selector="pending-transactions-list">
<%= for pending_transaction <- @pending_transactions do %>
<%= render(BlockScoutWeb.TransactionView, "_tile.html", current_address: @address, transaction: pending_transaction) %>
<% end %>
<div class="mb-3 collapse" id="pending-transactions-container">
<div data-selector="pending-transactions-list">
<%= for pending_transaction <- @pending_transactions do %>
<%= render(BlockScoutWeb.TransactionView, "_tile.html", current_address: @address, transaction: pending_transaction) %>
<% end %>
</div>
<hr />
</div>
</div>
@ -72,6 +74,17 @@
<%= render(BlockScoutWeb.TransactionView, "_tile.html", current_address: @address, transaction: transaction) %>
<% end %>
</span>
<div data-selector="loading-next-page" class="tile tile-muted text-center mt-3" 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 data-selector="paging-error-message" class="alert alert-danger text-center mt-3" style="display: none;">
<%= gettext("Error trying to fetch next page.") %>
</div>
<% else %>
<div class="tile tile-muted text-center">
<span data-selector="empty-transactions-list"><%= gettext "There are no transactions for this address." %></span>
@ -82,6 +95,7 @@
<%= link(
gettext("Older"),
class: "button button-secondary button-sm float-right mt-3",
"data-selector": "next-page-button",
to: address_transaction_path(
@conn,
:index,

@ -99,7 +99,7 @@
</div>
<div class="card-body">
<div data-selector="channel-disconnected-message" class="d-none">
<div data-selector="channel-disconnected-message" style="display: none;">
<div data-selector="reload-button" class="alert alert-danger">
<a href="#" class="alert-link"><%= gettext "Connection Lost, click to load newer validations" %></a>
</div>

@ -4,7 +4,7 @@
<h1 class="card-title mb-0">API Documentation</h2>
<small class="text-monospace text-primary" data-endpoint-url="<%= BlockScoutWeb.Endpoint.url() %>/api">[ <%= gettext "Base URL:" %> <%= @conn.host %>/api ]</small>
<p class="mt-4"><%= gettext "This API is provided for developers transitioning their applications from Etherscan to Explorer. It supports GET and POST requests." %></p>
<p class="mt-4"><%= gettext "This API is provided for developers transitioning their applications from Etherscan to BlockScout. It supports GET and POST requests." %></p>
</div>
</div>

@ -1,4 +1,4 @@
<div class="tile tile-type-internal-transaction fade-in" data-test="internal_transaction" data-internal-transaction-transaction-hash="<%= @internal_transaction.transaction_hash %>" data-internal-transaction-index="<%= @internal_transaction.index %>">
<div class="tile tile-type-internal-transaction fade-in" data-test="internal_transaction" data-key="<%= @internal_transaction.transaction_hash %>_<%= @internal_transaction.index %>" data-internal-transaction-transaction-hash="<%= @internal_transaction.transaction_hash %>" data-internal-transaction-index="<%= @internal_transaction.index %>">
<div class="row">
<div class="col-md-2 d-flex flex-row flex-md-column align-items-left justify-content-start justify-content-lg-center mb-1 mb-md-0 pl-md-4">
<%= gettext("Internal Transaction") %>

@ -56,13 +56,25 @@
<%= gettext("Accounts") %>
<% end %>
</li>
<li class="nav-item">
<%= link to: api_docs_path(@conn, :index), class: "nav-link topnav-nav-link" do %>
<li class="nav-item dropdown">
<a href="#" role="button" id="navbarAPIsDropdown" class="nav-link topnav-nav-link dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="nav-link-icon">
<%= render BlockScoutWeb.IconsView, "_api_icon.html" %>
</span>
<%= gettext("API") %>
<% end %>
<%= gettext("APIs") %>
</a>
<div class="dropdown-menu" aria-labeledby="navbarTransactionsDropdown">
<%= link(
gettext("GraphQL"),
class: "dropdown-item",
to: "/graphiql"
) %>
<%= link(
gettext("RPC"),
class: "dropdown-item",
to: api_docs_path(@conn, :index)
) %>
</div>
</li>
</ul>
<div class="search-form d-lg-flex d-inline-block">

@ -5,6 +5,6 @@
[<%= for item <- @outputs do %>
<span class="function-response-item"><%= if named_argument?(item) do %><%= item["name"] %>
<% end %>
<span class="text-muted">(<%= item["type"] %>)</span> : <%= values(item["value"]) %></span><% end %>]
<span class="text-muted">(<%= item["type"] %>)</span> : <%= values(item["value"], item["type"]) %></span><% end %>]
</pre>
</div>

@ -51,7 +51,7 @@
</span>
</div>
<% else %>
<%= output["value"] %>
<%= values(output["value"], output["type"]) %>
<% end %>
<% end %>
<% end %>

@ -9,12 +9,12 @@
<%= gettext("Validated Transactions") %>
</p>
<div data-selector="channel-batching-message" class="d-none">
<div data-selector="channel-batching-message" style="display: none;">
<div data-selector="reload-button" class="alert alert-info">
<a href="#" class="alert-link"><span data-selector="channel-batching-count"></span> <%= gettext "More transactions have come in" %></a>
</div>
</div>
<div data-selector="channel-disconnected-message" class="d-none">
<div data-selector="channel-disconnected-message" style="display: none;">
<div data-selector="reload-button" class="alert alert-danger">
<a href="#" class="alert-link"><%= gettext "Connection Lost, click to load newer transactions" %></a>
</div>

@ -1,7 +1,7 @@
defmodule BlockScoutWeb.AddressView do
use BlockScoutWeb, :view
import BlockScoutWeb.AddressController, only: [transaction_count: 1, validation_count: 1]
import BlockScoutWeb.AddressController, only: [validation_count: 1]
alias BlockScoutWeb.LayoutView
alias Explorer.Chain
@ -94,16 +94,20 @@ defmodule BlockScoutWeb.AddressView do
format_wei_value(balance, :ether)
end
def balance_percentage(%Address{fetched_coin_balance: balance}) do
def balance_percentage(%Address{fetched_coin_balance: balance}, total_supply) do
balance
|> Wei.to(:ether)
|> Decimal.div(Decimal.new(Chain.total_supply()))
|> Decimal.div(Decimal.new(total_supply))
|> Decimal.mult(100)
|> Decimal.round(4)
|> Decimal.to_string(:normal)
|> Kernel.<>("% #{gettext("Market Cap")}")
end
def balance_percentage(%Address{fetched_coin_balance: _} = address) do
balance_percentage(address, Chain.total_supply())
end
def balance_block_number(%Address{fetched_coin_balance_block_number: nil}), do: ""
def balance_block_number(%Address{fetched_coin_balance_block_number: fetched_coin_balance_block_number}) do

@ -3,6 +3,11 @@ defmodule BlockScoutWeb.API.RPC.TransactionView do
alias BlockScoutWeb.API.RPC.RPCView
def render("gettxinfo.json", %{transaction: transaction, max_block_number: max_block_number, logs: logs}) do
data = prepare_transaction(transaction, max_block_number, logs)
RPCView.render("show.json", data: data)
end
def render("gettxreceiptstatus.json", %{status: status}) do
prepared_status = prepare_tx_receipt_status(status)
RPCView.render("show.json", data: %{"status" => prepared_status})
@ -44,4 +49,33 @@ defmodule BlockScoutWeb.API.RPC.TransactionView do
"errDescription" => error |> Atom.to_string() |> String.replace("_", " ")
}
end
defp prepare_transaction(transaction, max_block_number, logs) do
%{
"hash" => "#{transaction.hash}",
"timeStamp" => "#{DateTime.to_unix(transaction.block.timestamp)}",
"blockNumber" => "#{transaction.block_number}",
"confirmations" => "#{max_block_number - transaction.block_number}",
"success" => if(transaction.status == :ok, do: true, else: false),
"from" => "#{transaction.from_address_hash}",
"to" => "#{transaction.to_address_hash}",
"value" => "#{transaction.value.value}",
"input" => "#{transaction.input}",
"gasLimit" => "#{transaction.gas}",
"gasUsed" => "#{transaction.gas_used}",
"logs" => Enum.map(logs, &prepare_log/1)
}
end
defp prepare_log(log) do
%{
"address" => "#{log.address_hash}",
"topics" => get_topics(log),
"data" => "#{log.data}"
}
end
defp get_topics(log) do
[log.first_topic, log.second_topic, log.third_topic, log.fourth_topic]
end
end

@ -3,13 +3,24 @@ defmodule BlockScoutWeb.SmartContractView do
def queryable?(inputs), do: Enum.any?(inputs)
def address?(type), do: type == "address"
def address?(type), do: type in ["address", "address payable"]
def named_argument?(%{"name" => ""}), do: false
def named_argument?(%{"name" => nil}), do: false
def named_argument?(%{"name" => _}), do: true
def named_argument?(_), do: false
def values(values) when is_list(values), do: Enum.join(values, ",")
def values(value), do: value
def values(addresses, type) when type == "address[]" do
addresses
|> Enum.map(&values(&1, "address"))
|> Enum.join(", ")
end
def values(value, type) when type in ["address", "address payable"] do
{:ok, address} = Explorer.Chain.Hash.Address.cast(value)
to_string(address)
end
def values(values, _) when is_list(values), do: Enum.join(values, ",")
def values(value, _), do: value
end

@ -83,11 +83,6 @@ msgstr ""
msgid "A string with the name of the module to be invoked."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:64
msgid "API"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/api_docs/_metatags.html.eex:4
msgid "API endpoints for the %{subnetwork}"
@ -233,7 +228,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_validation/index.html.eex:90
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:119
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:141
#: lib/block_scout_web/views/address_view.ex:210
#: lib/block_scout_web/views/address_view.ex:214
msgid "Code"
msgstr ""
@ -514,7 +509,7 @@ msgstr ""
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:14
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:43
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:10
#: lib/block_scout_web/views/address_view.ex:209
#: lib/block_scout_web/views/address_view.ex:213
#: lib/block_scout_web/views/transaction_view.ex:176
msgid "Internal Transactions"
msgstr ""
@ -639,7 +634,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:72
#: lib/block_scout_web/templates/address_transaction/index.html.eex:83
#: lib/block_scout_web/templates/address_transaction/index.html.eex:96
#: lib/block_scout_web/templates/address_validation/index.html.eex:117
#: lib/block_scout_web/templates/block/index.html.eex:20
#: lib/block_scout_web/templates/block_transaction/index.html.eex:50
@ -661,12 +656,12 @@ msgid "Owner Address"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:95
#: lib/block_scout_web/templates/layout/_topnav.html.eex:107
msgid "POA Core"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:94
#: lib/block_scout_web/templates/layout/_topnav.html.eex:106
msgid "POA Sokol"
msgstr ""
@ -731,7 +726,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_validation/index.html.eex:53
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:33
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:75
#: lib/block_scout_web/views/address_view.ex:211
#: lib/block_scout_web/views/address_view.ex:215
#: lib/block_scout_web/views/tokens/overview_view.ex:37
msgid "Read Contract"
msgstr ""
@ -757,13 +752,13 @@ msgid "Responses"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:71
#: lib/block_scout_web/templates/layout/_topnav.html.eex:78
#: lib/block_scout_web/templates/layout/_topnav.html.eex:83
#: lib/block_scout_web/templates/layout/_topnav.html.eex:90
msgid "Search"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:71
#: lib/block_scout_web/templates/layout/_topnav.html.eex:83
msgid "Search by address, transaction hash, or block number"
msgstr ""
@ -858,7 +853,7 @@ msgid "There are no tokens."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_transaction/index.html.eex:77
#: lib/block_scout_web/templates/address_transaction/index.html.eex:90
msgid "There are no transactions for this address."
msgstr ""
@ -872,11 +867,6 @@ msgstr ""
msgid "There are no transfers for this Token."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/api_docs/index.html.eex:7
msgid "This API is provided for developers transitioning their applications from Etherscan to Explorer. It supports GET and POST requests."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/overview.html.eex:23
msgid "This transaction is pending confirmation."
@ -940,7 +930,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_validation/index.html.eex:18
#: lib/block_scout_web/templates/address_validation/index.html.eex:62
#: lib/block_scout_web/templates/address_validation/index.html.eex:70
#: lib/block_scout_web/views/address_view.ex:207
#: lib/block_scout_web/views/address_view.ex:211
msgid "Tokens"
msgstr ""
@ -1001,7 +991,7 @@ msgstr ""
#: lib/block_scout_web/templates/block_transaction/index.html.eex:35
#: lib/block_scout_web/templates/chain/show.html.eex:71
#: lib/block_scout_web/templates/layout/_topnav.html.eex:35
#: lib/block_scout_web/views/address_view.ex:208
#: lib/block_scout_web/views/address_view.ex:212
msgid "Transactions"
msgstr ""

@ -83,11 +83,6 @@ msgstr ""
msgid "A string with the name of the module to be invoked."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:64
msgid "API"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/api_docs/_metatags.html.eex:4
msgid "API endpoints for the %{subnetwork}"
@ -233,7 +228,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_validation/index.html.eex:90
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:119
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:141
#: lib/block_scout_web/views/address_view.ex:210
#: lib/block_scout_web/views/address_view.ex:214
msgid "Code"
msgstr ""
@ -514,7 +509,7 @@ msgstr ""
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:14
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:43
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:10
#: lib/block_scout_web/views/address_view.ex:209
#: lib/block_scout_web/views/address_view.ex:213
#: lib/block_scout_web/views/transaction_view.ex:176
msgid "Internal Transactions"
msgstr ""
@ -639,7 +634,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:72
#: lib/block_scout_web/templates/address_transaction/index.html.eex:83
#: lib/block_scout_web/templates/address_transaction/index.html.eex:96
#: lib/block_scout_web/templates/address_validation/index.html.eex:117
#: lib/block_scout_web/templates/block/index.html.eex:20
#: lib/block_scout_web/templates/block_transaction/index.html.eex:50
@ -661,12 +656,12 @@ msgid "Owner Address"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:95
#: lib/block_scout_web/templates/layout/_topnav.html.eex:107
msgid "POA Core"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:94
#: lib/block_scout_web/templates/layout/_topnav.html.eex:106
msgid "POA Sokol"
msgstr ""
@ -731,7 +726,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_validation/index.html.eex:53
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:33
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:75
#: lib/block_scout_web/views/address_view.ex:211
#: lib/block_scout_web/views/address_view.ex:215
#: lib/block_scout_web/views/tokens/overview_view.ex:37
msgid "Read Contract"
msgstr ""
@ -757,13 +752,13 @@ msgid "Responses"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:71
#: lib/block_scout_web/templates/layout/_topnav.html.eex:78
#: lib/block_scout_web/templates/layout/_topnav.html.eex:83
#: lib/block_scout_web/templates/layout/_topnav.html.eex:90
msgid "Search"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:71
#: lib/block_scout_web/templates/layout/_topnav.html.eex:83
msgid "Search by address, transaction hash, or block number"
msgstr ""
@ -858,7 +853,7 @@ msgid "There are no tokens."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_transaction/index.html.eex:77
#: lib/block_scout_web/templates/address_transaction/index.html.eex:90
msgid "There are no transactions for this address."
msgstr ""
@ -872,11 +867,6 @@ msgstr ""
msgid "There are no transfers for this Token."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/api_docs/index.html.eex:7
msgid "This API is provided for developers transitioning their applications from Etherscan to Explorer. It supports GET and POST requests."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/overview.html.eex:23
msgid "This transaction is pending confirmation."
@ -940,7 +930,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_validation/index.html.eex:18
#: lib/block_scout_web/templates/address_validation/index.html.eex:62
#: lib/block_scout_web/templates/address_validation/index.html.eex:70
#: lib/block_scout_web/views/address_view.ex:207
#: lib/block_scout_web/views/address_view.ex:211
msgid "Tokens"
msgstr ""
@ -1001,7 +991,7 @@ msgstr ""
#: lib/block_scout_web/templates/block_transaction/index.html.eex:35
#: lib/block_scout_web/templates/chain/show.html.eex:71
#: lib/block_scout_web/templates/layout/_topnav.html.eex:35
#: lib/block_scout_web/views/address_view.ex:208
#: lib/block_scout_web/views/address_view.ex:212
msgid "Transactions"
msgstr ""

@ -10,7 +10,9 @@ defmodule BlockScoutWeb.AddressControllerTest do
conn = get(conn, address_path(conn, :index))
assert conn.assigns.addresses |> Enum.map(& &1.hash) == address_hashes
assert conn.assigns.address_tx_count_pairs
|> Enum.map(fn {address, _transaction_count} -> address end)
|> Enum.map(& &1.hash) == address_hashes
end
end

@ -76,7 +76,7 @@ defmodule BlockScoutWeb.AddressTransactionControllerTest do
50
|> insert_list(:transaction, from_address: address)
|> with_block()
|> Enum.map(& &1.hash)
|> Enum.map(&to_string(&1.hash))
%Transaction{block_number: block_number, index: index} =
:transaction
@ -85,47 +85,21 @@ defmodule BlockScoutWeb.AddressTransactionControllerTest do
conn =
get(conn, address_transaction_path(BlockScoutWeb.Endpoint, :index, address.hash), %{
"type" => "JSON",
"block_number" => Integer.to_string(block_number),
"index" => Integer.to_string(index)
})
{:ok, %{"transactions" => transactions}} = conn.resp_body |> Poison.decode()
actual_hashes =
conn.assigns.transactions
|> Enum.map(& &1.hash)
transactions
|> Enum.map(& &1["transaction_hash"])
|> Enum.reverse()
assert second_page_hashes == actual_hashes
end
test "does not return pending transactions if beyond page one", %{conn: conn} do
address = insert(:address)
50
|> insert_list(:transaction, from_address: address)
|> with_block()
|> Enum.map(& &1.hash)
%Transaction{block_number: block_number, index: index} =
:transaction
|> insert(from_address: address)
|> with_block()
pending = insert(:transaction, from_address: address, to_address: address)
conn =
get(conn, address_transaction_path(BlockScoutWeb.Endpoint, :index, address.hash), %{
"block_number" => Integer.to_string(block_number),
"index" => Integer.to_string(index)
})
actual_pending_hashes =
conn.assigns.pending_transactions
|> Enum.map(& &1.hash)
|> Enum.reverse()
refute Enum.member?(actual_pending_hashes, pending.hash)
end
test "next_page_params exist if not on last page", %{conn: conn} do
address = insert(:address)
block = %Block{number: number} = insert(:block)

@ -310,4 +310,114 @@ defmodule BlockScoutWeb.API.RPC.TransactionControllerTest do
assert response["message"] == "OK"
end
end
describe "gettxinfo" do
test "with missing txhash", %{conn: conn} do
params = %{
"module" => "transaction",
"action" => "gettxinfo"
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["message"] =~ "txhash is required"
assert response["status"] == "0"
assert Map.has_key?(response, "result")
refute response["result"]
end
test "with an invalid txhash", %{conn: conn} do
params = %{
"module" => "transaction",
"action" => "gettxinfo",
"txhash" => "badhash"
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["message"] =~ "Invalid txhash format"
assert response["status"] == "0"
assert Map.has_key?(response, "result")
refute response["result"]
end
test "with a txhash that doesn't exist", %{conn: conn} do
params = %{
"module" => "transaction",
"action" => "gettxinfo",
"txhash" => "0x40eb908387324f2b575b4879cd9d7188f69c8fc9d87c901b9e2daaea4b442170"
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["message"] =~ "Transaction not found"
assert response["status"] == "0"
assert Map.has_key?(response, "result")
refute response["result"]
end
test "with a txhash with ok status", %{conn: conn} do
block = insert(:block)
transaction =
:transaction
|> insert()
|> with_block(block, status: :ok)
address = insert(:address)
log =
insert(:log,
address: address,
transaction: transaction,
first_topic: "first topic",
second_topic: "second topic"
)
params = %{
"module" => "transaction",
"action" => "gettxinfo",
"txhash" => "#{transaction.hash}"
}
expected_result = %{
"hash" => "#{transaction.hash}",
"timeStamp" => "#{DateTime.to_unix(transaction.block.timestamp)}",
"blockNumber" => "#{transaction.block_number}",
"confirmations" => "0",
"success" => true,
"from" => "#{transaction.from_address_hash}",
"to" => "#{transaction.to_address_hash}",
"value" => "#{transaction.value.value}",
"input" => "#{transaction.input}",
"gasLimit" => "#{transaction.gas}",
"gasUsed" => "#{transaction.gas_used}",
"logs" => [
%{
"address" => "#{address}",
"data" => "#{log.data}",
"topics" => ["first topic", "second topic", nil, nil]
}
]
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["result"] == expected_result
assert response["status"] == "1"
assert response["message"] == "OK"
end
end
end

@ -22,7 +22,7 @@ defmodule BlockScoutWeb.Tokens.HolderControllerTest do
insert_list(
2,
:token_balance,
:address_current_token_balance,
token_contract_address_hash: token.contract_address_hash
)
@ -43,7 +43,7 @@ defmodule BlockScoutWeb.Tokens.HolderControllerTest do
1..50
|> Enum.map(
&insert(
:token_balance,
:address_current_token_balance,
token_contract_address_hash: token.contract_address_hash,
value: &1 + 1000
)
@ -52,7 +52,7 @@ defmodule BlockScoutWeb.Tokens.HolderControllerTest do
token_balance =
insert(
:token_balance,
:address_current_token_balance,
token_contract_address_hash: token.contract_address_hash,
value: 50000
)
@ -78,7 +78,7 @@ defmodule BlockScoutWeb.Tokens.HolderControllerTest do
Enum.each(
1..51,
&insert(
:token_balance,
:address_current_token_balance,
token_contract_address_hash: token.contract_address_hash,
value: &1 + 1000
)

@ -9,7 +9,7 @@ defmodule BlockScoutWeb.ViewingTokensTest do
insert_list(
2,
:token_balance,
:address_current_token_balance,
token_contract_address_hash: token.contract_address_hash
)

@ -0,0 +1,142 @@
defmodule BlockScoutWeb.Schema.Query.AddressTest do
use BlockScoutWeb.ConnCase
describe "address field" do
test "with valid argument 'hashes', returns all expected fields", %{conn: conn} do
address = insert(:address, fetched_coin_balance: 100)
query = """
query ($hashes: [AddressHash!]!) {
addresses(hashes: $hashes) {
hash
fetched_coin_balance
fetched_coin_balance_block_number
contract_code
}
}
"""
variables = %{"hashes" => to_string(address.hash)}
conn = get(conn, "/graphql", query: query, variables: variables)
assert json_response(conn, 200) == %{
"data" => %{
"addresses" => [
%{
"hash" => to_string(address.hash),
"fetched_coin_balance" => to_string(address.fetched_coin_balance.value),
"fetched_coin_balance_block_number" => address.fetched_coin_balance_block_number,
"contract_code" => nil
}
]
}
}
end
test "with contract address, `contract_code` is serialized as expected", %{conn: conn} do
address = insert(:contract_address, fetched_coin_balance: 100)
query = """
query ($hashes: [AddressHash!]!) {
addresses(hashes: $hashes) {
contract_code
}
}
"""
variables = %{"hashes" => to_string(address.hash)}
conn = get(conn, "/graphql", query: query, variables: variables)
assert json_response(conn, 200) == %{
"data" => %{
"addresses" => [
%{
"contract_code" => to_string(address.contract_code)
}
]
}
}
end
test "errors for non-existent address hashes", %{conn: conn} do
address = build(:address)
query = """
query ($hashes: [AddressHash!]!) {
addresses(hashes: $hashes) {
fetched_coin_balance
}
}
"""
variables = %{"hashes" => [to_string(address.hash)]}
conn = get(conn, "/graphql", query: query, variables: variables)
assert %{"errors" => [error]} = json_response(conn, 200)
assert error["message"] =~ ~s(Addresses not found.)
end
test "errors if argument 'hashes' is missing", %{conn: conn} do
query = """
query {
addresses {
fetched_coin_balance
}
}
"""
variables = %{}
conn = get(conn, "/graphql", query: query, variables: variables)
assert %{"errors" => [error]} = json_response(conn, 200)
assert error["message"] == ~s(In argument "hashes": Expected type "[AddressHash!]!", found null.)
end
test "errors if argument 'hashes' is not a list of address hashes", %{conn: conn} do
query = """
query ($hashes: [AddressHash!]!) {
addresses(hashes: $hashes) {
fetched_coin_balance
}
}
"""
variables = %{"hashes" => ["someInvalidHash"]}
conn = get(conn, "/graphql", query: query, variables: variables)
assert %{"errors" => [error]} = json_response(conn, 200)
assert error["message"] =~ ~s(Argument "hashes" has invalid value)
end
test "correlates complexity to size of 'hashes' argument", %{conn: conn} do
# max of 12 addresses with four fields of complexity 1 can be fetched
# per query:
# 12 * 4 = 48, which is less than a max complexity of 50
hashes = 13 |> build_list(:address) |> Enum.map(&to_string(&1.hash))
query = """
query ($hashes: [AddressHash!]!) {
addresses(hashes: $hashes) {
hash
fetched_coin_balance
fetched_coin_balance_block_number
contract_code
}
}
"""
variables = %{"hashes" => hashes}
conn = get(conn, "/graphql", query: query, variables: variables)
assert %{"errors" => [error1, error2]} = json_response(conn, 200)
assert error1["message"] =~ ~s(Field addresses is too complex)
assert error2["message"] =~ ~s(Operation is too complex)
end
end
end

@ -8,6 +8,7 @@ defmodule BlockScoutWeb.Schema.Query.BlockTest do
query = """
query ($number: Int!) {
block(number: $number) {
hash
consensus
difficulty
gas_limit
@ -31,6 +32,7 @@ defmodule BlockScoutWeb.Schema.Query.BlockTest do
assert json_response(conn, 200) == %{
"data" => %{
"block" => %{
"hash" => to_string(block.hash),
"consensus" => block.consensus,
"difficulty" => to_string(block.difficulty),
"gas_limit" => to_string(block.gas_limit),

@ -24,6 +24,12 @@ defmodule BlockScoutWeb.SmartContractViewTest do
assert SmartContractView.address?(type)
end
test "returns true when the type is equal to the string 'address payable'" do
type = "address payable"
assert SmartContractView.address?(type)
end
test "returns false when the type is not equal the string 'address'" do
type = "name"
@ -57,17 +63,37 @@ defmodule BlockScoutWeb.SmartContractViewTest do
end
end
describe "values/1" do
test "joins the values when it is a list" do
describe "values/2" do
test "joins the values when it is a list of a given type" do
values = [8, 6, 9, 2, 2, 37]
assert SmartContractView.values(values) == "8,6,9,2,2,37"
assert SmartContractView.values(values, "type") == "8,6,9,2,2,37"
end
test "convert the value to string receiving a value and the 'address' type" do
value = <<95, 38, 9, 115, 52, 182, 163, 43, 121, 81, 223, 97, 253, 12, 88, 3, 236, 93, 131, 84>>
assert SmartContractView.values(value, "address") == "0x5f26097334b6a32b7951df61fd0c5803ec5d8354"
end
test "convert the value to string receiving a value and the 'address payable' type" do
value = <<95, 38, 9, 115, 52, 182, 163, 43, 121, 81, 223, 97, 253, 12, 88, 3, 236, 93, 131, 84>>
assert SmartContractView.values(value, "address payable") == "0x5f26097334b6a32b7951df61fd0c5803ec5d8354"
end
test "convert each value to string and join them when receiving 'address[]' as the type" do
value = [
<<95, 38, 9, 115, 52, 182, 163, 43, 121, 81, 223, 97, 253, 12, 88, 3, 236, 93, 131, 84>>,
<<207, 38, 14, 163, 23, 85, 86, 55, 197, 95, 112, 229, 93, 186, 141, 90, 216, 65, 76, 176>>
]
assert SmartContractView.values(value, "address[]") ==
"0x5f26097334b6a32b7951df61fd0c5803ec5d8354, 0xcf260ea317555637c55f70e55dba8d5ad8414cb0"
end
test "returns the value" do
test "returns the value when the type is neither 'address' nor 'address payable'" do
value = "POA"
assert SmartContractView.values(value) == "POA"
assert SmartContractView.values(value, "not address") == "POA"
end
end
end

@ -11,16 +11,23 @@ defmodule EthereumJSONRPC.Block do
@type elixir :: %{String.t() => non_neg_integer | DateTime.t() | String.t() | nil}
@type params :: %{
difficulty: pos_integer(),
extra_data: EthereumJSONRPC.hash(),
gas_limit: non_neg_integer(),
gas_used: non_neg_integer(),
hash: EthereumJSONRPC.hash(),
logs_bloom: EthereumJSONRPC.hash(),
miner_hash: EthereumJSONRPC.hash(),
mix_hash: EthereumJSONRPC.hash(),
nonce: EthereumJSONRPC.hash(),
number: non_neg_integer(),
parent_hash: EthereumJSONRPC.hash(),
receipts_root: EthereumJSONRPC.hash(),
sha3_uncles: EthereumJSONRPC.hash(),
size: non_neg_integer(),
state_root: EthereumJSONRPC.hash(),
timestamp: DateTime.t(),
total_difficulty: non_neg_integer(),
transactions_root: EthereumJSONRPC.hash(),
uncles: [EthereumJSONRPC.hash()]
}
@ -95,16 +102,23 @@ defmodule EthereumJSONRPC.Block do
...> )
%{
difficulty: 340282366920938463463374607431465537093,
extra_data: "0xd5830108048650617269747986312e32322e31826c69",
gas_limit: 6706541,
gas_used: 0,
hash: "0x52c867bc0a91e573dc39300143c3bead7408d09d45bdb686749f02684ece72f3",
logs_bloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
miner_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
mix_hash: "0x0",
nonce: 0,
number: 1,
parent_hash: "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f",
receipts_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
sha3_uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
size: 576,
state_root: "0xc196ad59d867542ef20b29df5f418d07dc7234f4bc3d25260526620b7958a8fb",
timestamp: Timex.parse!("2017-12-15T21:03:30Z", "{ISO:Extended:Z}"),
total_difficulty: 340282366920938463463374607431465668165,
transactions_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
uncles: []
}
@ -136,16 +150,23 @@ defmodule EthereumJSONRPC.Block do
...> )
%{
difficulty: 17561410778,
extra_data: "0x476574682f4c5649562f76312e302e302f6c696e75782f676f312e342e32",
gas_limit: 5000,
gas_used: 0,
hash: "0x4d9423080290a650eaf6db19c87c76dff83d1b4ab64aefe6e5c5aa2d1f4b6623",
logs_bloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
mix_hash: "0xbbb93d610b2b0296a59f18474ac3d6086a9902aa7ca4b9a306692f7c3d496fdf",
miner_hash: "0xbb7b8287f3f0a933474a79eae42cbca977791171",
nonce: 5539500215739777653,
number: 59,
parent_hash: "0xcd5b5c4cecd7f18a13fe974255badffd58e737dc67596d56bc01f063dd282e9e",
receipts_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
sha3_uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
size: 542,
state_root: "0x6fd0a5d82ca77d9f38c3ebbde11b11d304a5fcf3854f291df64395ab38ed43ba",
timestamp: Timex.parse!("2015-07-30T15:32:07Z", "{ISO:Extended:Z}"),
total_difficulty: 1039309006117,
transactions_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
uncles: []
}
@ -154,30 +175,43 @@ defmodule EthereumJSONRPC.Block do
def elixir_to_params(
%{
"difficulty" => difficulty,
"extraData" => extra_data,
"gasLimit" => gas_limit,
"gasUsed" => gas_used,
"hash" => hash,
"logsBloom" => logs_bloom,
"miner" => miner_hash,
"number" => number,
"parentHash" => parent_hash,
"receiptsRoot" => receipts_root,
"sha3Uncles" => sha3_uncles,
"size" => size,
"stateRoot" => state_root,
"timestamp" => timestamp,
"totalDifficulty" => total_difficulty,
"transactionsRoot" => transactions_root,
"uncles" => uncles
} = elixir
) do
%{
difficulty: difficulty,
extra_data: extra_data,
gas_limit: gas_limit,
gas_used: gas_used,
hash: hash,
logs_bloom: logs_bloom,
miner_hash: miner_hash,
mix_hash: Map.get(elixir, "mixHash", "0x0"),
nonce: Map.get(elixir, "nonce", 0),
number: number,
parent_hash: parent_hash,
receipts_root: receipts_root,
sha3_uncles: sha3_uncles,
size: size,
state_root: state_root,
timestamp: timestamp,
total_difficulty: total_difficulty,
transactions_root: transactions_root,
uncles: uncles
}
end

@ -45,16 +45,23 @@ defmodule EthereumJSONRPC.Blocks do
[
%{
difficulty: 131072,
extra_data: "0x",
gas_limit: 6700000,
gas_used: 0,
hash: "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f",
logs_bloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
miner_hash: "0x0000000000000000000000000000000000000000",
mix_hash: "0x0",
nonce: 0,
number: 0,
parent_hash: "0x0000000000000000000000000000000000000000000000000000000000000000",
receipts_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
sha3_uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
size: 533,
state_root: "0xfad4af258fd11939fae0c6c6eec9d340b1caac0b0196fd9a1bc3f489c5bf00b3",
timestamp: Timex.parse!("1970-01-01T00:00:00Z", "{ISO:Extended:Z}"),
total_difficulty: 131072,
transactions_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
uncles: ["0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311"]
}
]

@ -10,15 +10,6 @@ defmodule EthereumJSONRPC.RequestCoordinatorTest do
setup :set_mox_global
setup :verify_on_exit!
defp sleep_time(timeouts) do
wait_per_timeout =
:ethereum_jsonrpc
|> Application.get_env(RequestCoordinator)
|> Keyword.fetch!(:wait_per_timeout)
timeouts * wait_per_timeout
end
setup do
table = Application.get_env(:ethereum_jsonrpc, EthereumJSONRPC.RequestCoordinator)[:rolling_window_opts][:table]

@ -95,18 +95,6 @@ defmodule EthereumJSONRPC.WebSocket.WebSocketClientTest do
end
end
defp cowboy(0) do
dispatch = :cowboy_router.compile([{:_, [{"/websocket", EthereumJSONRPC.WebSocket.Cowboy.WebSocketHandler, []}]}])
{:ok, _} = :cowboy.start_http(EthereumJSONRPC.WebSocket.Cowboy, 100, [], env: [dispatch: dispatch])
:ranch.get_port(EthereumJSONRPC.WebSocket.Cowboy)
end
defp cowboy(port) do
dispatch = :cowboy_router.compile([{:_, [{"/websocket", EthereumJSONRPC.WebSocket.Cowboy.WebSocketHandler, []}]}])
{:ok, _} = :cowboy.start_http(EthereumJSONRPC.WebSocket.Cowboy, 100, [port: port], env: [dispatch: dispatch])
port
end
defp example_state(_) do
%{state: %WebSocketClient{url: "ws://example.com"}}
end

@ -207,10 +207,15 @@ defmodule EthereumJSONRPCTest do
"gasLimit" => "0x0",
"gasUsed" => "0x0",
"hash" => block_hash,
"extraData" => "0x0",
"logsBloom" => "0x0",
"miner" => "0x0",
"number" => block_number,
"parentHash" => "0x0",
"receiptsRoot" => "0x0",
"size" => "0x0",
"sha3Uncles" => "0x0",
"stateRoot" => "0x0",
"timestamp" => "0x0",
"totalDifficulty" => "0x0",
"transactions" => [
@ -231,6 +236,7 @@ defmodule EthereumJSONRPCTest do
"value" => "0x0"
}
],
"transactionsRoot" => "0x0",
"uncles" => []
}
}
@ -364,16 +370,22 @@ defmodule EthereumJSONRPCTest do
id: 0,
result: %{
"difficulty" => "0x0",
"extraData" => "0x0",
"gasLimit" => "0x0",
"gasUsed" => "0x0",
"hash" => "0x0",
"logsBloom" => "0x0",
"miner" => "0x0",
"number" => "0x0",
"parentHash" => "0x0",
"receiptsRoot" => "0x0",
"sha3Uncles" => "0x0",
"size" => "0x0",
"stateRoot" => "0x0",
"timestamp" => "0x0",
"totalDifficulty" => "0x0",
"transactions" => [],
"transactionsRoot" => "0x0",
"uncles" => []
},
jsonrpc: "2.0"

@ -23,6 +23,7 @@ defmodule Explorer.Chain do
alias Explorer.Chain.{
Address,
Address.CoinBalance,
Address.CurrentTokenBalance,
Address.TokenBalance,
Block,
Data,
@ -919,13 +920,20 @@ defmodule Explorer.Chain do
Lists the top 250 `t:Explorer.Chain.Address.t/0`'s' in descending order based on coin balance.
"""
@spec list_top_addresses :: [Address.t()]
@spec list_top_addresses :: [{Address.t(), non_neg_integer()}]
def list_top_addresses do
Address
|> limit(250)
|> order_by(desc: :fetched_coin_balance, asc: :hash)
|> where([address], address.fetched_coin_balance > ^0)
|> Repo.all()
query =
from(a in Address,
left_join: t in Transaction,
on: a.hash == t.from_address_hash,
where: a.fetched_coin_balance > ^0,
group_by: [a.hash, a.fetched_coin_balance],
order_by: [desc: a.fetched_coin_balance, asc: a.hash],
select: {a, fragment("coalesce(1 + max(?), 0)", t.nonce)},
limit: 250
)
Repo.all(query)
end
@doc """
@ -2070,7 +2078,7 @@ defmodule Explorer.Chain do
@spec fetch_token_holders_from_token_hash(Hash.Address.t(), [paging_options]) :: [TokenBalance.t()]
def fetch_token_holders_from_token_hash(contract_address_hash, options) do
contract_address_hash
|> TokenBalance.token_holders_ordered_by_value(options)
|> CurrentTokenBalance.token_holders_ordered_by_value(options)
|> Repo.all()
end

@ -0,0 +1,108 @@
defmodule Explorer.Chain.Address.CurrentTokenBalance do
@moduledoc """
Represents the current token balance from addresses according to the last block.
"""
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query, only: [from: 2, limit: 2, order_by: 3, preload: 2, where: 3]
alias Explorer.{Chain, PagingOptions}
alias Explorer.Chain.{Address, Block, Hash, Token}
@default_paging_options %PagingOptions{page_size: 50}
@typedoc """
* `address` - The `t:Explorer.Chain.Address.t/0` that is the balance's owner.
* `address_hash` - The address hash foreign key.
* `token` - The `t:Explorer.Chain.Token/0` so that the address has the balance.
* `token_contract_address_hash` - The contract address hash foreign key.
* `block_number` - The block's number that the transfer took place.
* `value` - The value that's represents the balance.
"""
@type t :: %__MODULE__{
address: %Ecto.Association.NotLoaded{} | Address.t(),
address_hash: Hash.Address.t(),
token: %Ecto.Association.NotLoaded{} | Token.t(),
token_contract_address_hash: Hash.Address,
block_number: Block.block_number(),
inserted_at: DateTime.t(),
updated_at: DateTime.t(),
value: Decimal.t() | nil
}
schema "address_current_token_balances" do
field(:value, :decimal)
field(:block_number, :integer)
field(:value_fetched_at, :utc_datetime)
belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Address)
belongs_to(
:token,
Token,
foreign_key: :token_contract_address_hash,
references: :contract_address_hash,
type: Hash.Address
)
timestamps()
end
@optional_fields ~w(value value_fetched_at)a
@required_fields ~w(address_hash block_number token_contract_address_hash)a
@allowed_fields @optional_fields ++ @required_fields
@doc false
def changeset(%__MODULE__{} = token_balance, attrs) do
token_balance
|> cast(attrs, @allowed_fields)
|> validate_required(@required_fields)
|> foreign_key_constraint(:address_hash)
|> foreign_key_constraint(:token_contract_address_hash)
end
{:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000")
@burn_address_hash burn_address_hash
@doc """
Builds an `Ecto.Query` to fetch the token holders from the given token contract address hash.
The Token Holders are the addresses that own a positive amount of the Token. So this query is
considering the following conditions:
* The token balance from the last block.
* Balances greater than 0.
* Excluding the burn address (0x0000000000000000000000000000000000000000).
"""
def token_holders_ordered_by_value(token_contract_address_hash, options \\ []) do
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
token_contract_address_hash
|> token_holders_query
|> preload(:address)
|> order_by([tb], desc: :value)
|> page_token_balances(paging_options)
|> limit(^paging_options.page_size)
end
defp token_holders_query(token_contract_address_hash) do
from(
tb in __MODULE__,
where: tb.token_contract_address_hash == ^token_contract_address_hash,
where: tb.address_hash != ^@burn_address_hash,
where: tb.value > 0
)
end
defp page_token_balances(query, %PagingOptions{key: nil}), do: query
defp page_token_balances(query, %PagingOptions{key: {value, address_hash}}) do
where(
query,
[tb],
tb.value < ^value or (tb.value == ^value and tb.address_hash < ^address_hash)
)
end
end

@ -5,14 +5,12 @@ defmodule Explorer.Chain.Address.TokenBalance do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query, only: [from: 2, limit: 2, where: 3, subquery: 1, order_by: 3, preload: 2]
import Ecto.Query, only: [from: 2, subquery: 1]
alias Explorer.{Chain, PagingOptions}
alias Explorer.Chain
alias Explorer.Chain.Address.TokenBalance
alias Explorer.Chain.{Address, Block, Hash, Token}
@default_paging_options %PagingOptions{page_size: 50}
@typedoc """
* `address` - The `t:Explorer.Chain.Address.t/0` that is the balance's owner.
* `address_hash` - The address hash foreign key.
@ -84,43 +82,6 @@ defmodule Explorer.Chain.Address.TokenBalance do
from(tb in subquery(query), where: tb.value > 0, preload: :token)
end
@doc """
Builds an `Ecto.Query` to fetch the token holders from the given token contract address hash.
The Token Holders are the addresses that own a positive amount of the Token. So this query is
considering the following conditions:
* The token balance from the last block.
* Balances greater than 0.
* Excluding the burn address (0x0000000000000000000000000000000000000000).
"""
def token_holders_from_token_hash(token_contract_address_hash) do
query = token_holders_query(token_contract_address_hash)
from(tb in subquery(query), where: tb.value > 0)
end
def token_holders_ordered_by_value(token_contract_address_hash, options) do
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
token_contract_address_hash
|> token_holders_from_token_hash()
|> order_by([tb], desc: tb.value, desc: tb.address_hash)
|> preload(:address)
|> page_token_balances(paging_options)
|> limit(^paging_options.page_size)
end
defp token_holders_query(contract_address_hash) do
from(
tb in TokenBalance,
distinct: :address_hash,
where: tb.token_contract_address_hash == ^contract_address_hash and tb.address_hash != ^@burn_address_hash,
order_by: [desc: :block_number]
)
end
@doc """
Builds an `Ecto.Query` to group all tokens with their number of holders.
"""
@ -144,16 +105,6 @@ defmodule Explorer.Chain.Address.TokenBalance do
)
end
defp page_token_balances(query, %PagingOptions{key: nil}), do: query
defp page_token_balances(query, %PagingOptions{key: {value, address_hash}}) do
where(
query,
[tb],
tb.value < ^value or (tb.value == ^value and tb.address_hash < ^address_hash)
)
end
@doc """
Builds an `Ecto.Query` to fetch the unfetched token balances.

@ -19,6 +19,7 @@ defmodule Explorer.Chain.Import do
Import.Logs,
Import.Tokens,
Import.TokenTransfers,
Import.Address.CurrentTokenBalances,
Import.Address.TokenBalances
]

@ -0,0 +1,124 @@
defmodule Explorer.Chain.Import.Address.CurrentTokenBalances do
@moduledoc """
Bulk imports `t:Explorer.Chain.Address.CurrentTokenBalance.t/0`.
"""
require Ecto.Query
import Ecto.Query, only: [from: 2]
alias Ecto.{Changeset, Multi}
alias Explorer.Chain.Address.CurrentTokenBalance
alias Explorer.Chain.Import
@behaviour Import.Runner
# milliseconds
@timeout 60_000
@type imported :: [CurrentTokenBalance.t()]
@impl Import.Runner
def ecto_schema_module, do: CurrentTokenBalance
@impl Import.Runner
def option_key, do: :address_current_token_balances
@impl Import.Runner
def imported_table_row do
%{
value_type: "[#{ecto_schema_module()}.t()]",
value_description: "List of `t:#{ecto_schema_module()}.t/0`s"
}
end
@impl Import.Runner
def run(multi, changes_list, %{timestamps: timestamps} = options) do
insert_options =
options
|> Map.get(option_key(), %{})
|> Map.take(~w(on_conflict timeout)a)
|> Map.put_new(:timeout, @timeout)
|> Map.put(:timestamps, timestamps)
Multi.run(multi, :address_current_token_balances, fn _ ->
insert(changes_list, insert_options)
end)
end
@impl Import.Runner
def timeout, do: @timeout
@spec insert([map()], %{
optional(:on_conflict) => Import.Runner.on_conflict(),
required(:timeout) => timeout(),
required(:timestamps) => Import.timestamps()
}) ::
{:ok, [CurrentTokenBalance.t()]}
| {:error, [Changeset.t()]}
def insert(changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0)
{:ok, _} =
Import.insert_changes_list(
unique_token_balances(changes_list),
conflict_target: ~w(address_hash token_contract_address_hash)a,
on_conflict: on_conflict,
for: CurrentTokenBalance,
returning: true,
timeout: timeout,
timestamps: timestamps
)
end
# Remove duplicated token balances based on `{address_hash, token_hash}` considering the last block
# to avoid `cardinality_violation` error in Postgres. This error happens when there are duplicated
# rows being inserted.
defp unique_token_balances(changes_list) do
changes_list
|> Enum.sort(&(&1.block_number > &2.block_number))
|> Enum.uniq_by(fn %{address_hash: address_hash, token_contract_address_hash: token_hash} ->
{address_hash, token_hash}
end)
end
defp default_on_conflict do
from(
current_token_balance in CurrentTokenBalance,
update: [
set: [
block_number:
fragment(
"CASE WHEN EXCLUDED.block_number > ? THEN EXCLUDED.block_number ELSE ? END",
current_token_balance.block_number,
current_token_balance.block_number
),
inserted_at:
fragment(
"CASE WHEN EXCLUDED.block_number > ? THEN EXCLUDED.inserted_at ELSE ? END",
current_token_balance.block_number,
current_token_balance.inserted_at
),
updated_at:
fragment(
"CASE WHEN EXCLUDED.block_number > ? THEN EXCLUDED.updated_at ELSE ? END",
current_token_balance.block_number,
current_token_balance.updated_at
),
value:
fragment(
"CASE WHEN EXCLUDED.block_number > ? THEN EXCLUDED.value ELSE ? END",
current_token_balance.block_number,
current_token_balance.value
),
value_fetched_at:
fragment(
"CASE WHEN EXCLUDED.block_number > ? THEN EXCLUDED.value_fetched_at ELSE ? END",
current_token_balance.block_number,
current_token_balance.value_fetched_at
)
]
]
)
end
end

@ -0,0 +1,32 @@
defmodule Explorer.Repo.Migrations.CreateAddressCurrentTokenBalances do
use Ecto.Migration
def change do
create table(:address_current_token_balances) do
add(:address_hash, references(:addresses, column: :hash, type: :bytea), null: false)
add(:block_number, :bigint, null: false)
add(
:token_contract_address_hash,
references(:tokens, column: :contract_address_hash, type: :bytea),
null: false
)
add(:value, :decimal, null: true)
add(:value_fetched_at, :utc_datetime, default: fragment("NULL"), null: true)
timestamps(null: false, type: :utc_datetime)
end
create(unique_index(:address_current_token_balances, ~w(address_hash token_contract_address_hash)a))
create(
index(
:address_current_token_balances,
[:value],
name: :address_current_token_balances_value,
where: "value IS NOT NULL"
)
)
end
end

@ -0,0 +1,149 @@
defmodule Explorer.Chain.Address.CurrentTokenBalanceTest do
use Explorer.DataCase
alias Explorer.{Chain, PagingOptions, Repo}
alias Explorer.Chain.Token
alias Explorer.Chain.Address.CurrentTokenBalance
describe "token_holders_ordered_by_value/2" do
test "returns the last value for each address" do
%Token{contract_address_hash: contract_address_hash} = insert(:token)
address_a = insert(:address)
address_b = insert(:address)
insert(
:address_current_token_balance,
address: address_a,
token_contract_address_hash: contract_address_hash,
value: 5000
)
insert(
:address_current_token_balance,
address: address_b,
block_number: 1001,
token_contract_address_hash: contract_address_hash,
value: 4000
)
token_holders_count =
contract_address_hash
|> CurrentTokenBalance.token_holders_ordered_by_value()
|> Repo.all()
|> Enum.count()
assert token_holders_count == 2
end
test "sort by the highest value" do
%Token{contract_address_hash: contract_address_hash} = insert(:token)
address_a = insert(:address)
address_b = insert(:address)
address_c = insert(:address)
insert(
:address_current_token_balance,
address: address_a,
token_contract_address_hash: contract_address_hash,
value: 5000
)
insert(
:address_current_token_balance,
address: address_b,
token_contract_address_hash: contract_address_hash,
value: 4000
)
insert(
:address_current_token_balance,
address: address_c,
token_contract_address_hash: contract_address_hash,
value: 15000
)
token_holders_values =
contract_address_hash
|> CurrentTokenBalance.token_holders_ordered_by_value()
|> Repo.all()
|> Enum.map(&Decimal.to_integer(&1.value))
assert token_holders_values == [15_000, 5_000, 4_000]
end
test "returns only token balances that have value greater than 0" do
%Token{contract_address_hash: contract_address_hash} = insert(:token)
insert(
:address_current_token_balance,
token_contract_address_hash: contract_address_hash,
value: 0
)
result =
contract_address_hash
|> CurrentTokenBalance.token_holders_ordered_by_value()
|> Repo.all()
assert result == []
end
test "ignores the burn address" do
{:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000")
burn_address = insert(:address, hash: burn_address_hash)
%Token{contract_address_hash: contract_address_hash} = insert(:token)
insert(
:address_current_token_balance,
address: burn_address,
token_contract_address_hash: contract_address_hash,
value: 1000
)
result =
contract_address_hash
|> CurrentTokenBalance.token_holders_ordered_by_value()
|> Repo.all()
assert result == []
end
test "paginates the result by value and different address" do
address_a = build(:address, hash: "0xcb2cf1fd3199584ac5faa16c6aca49472dc6495a")
address_b = build(:address, hash: "0x5f26097334b6a32b7951df61fd0c5803ec5d8354")
%Token{contract_address_hash: contract_address_hash} = insert(:token)
first_page =
insert(
:address_current_token_balance,
address: address_a,
token_contract_address_hash: contract_address_hash,
value: 4000
)
second_page =
insert(
:address_current_token_balance,
address: address_b,
token_contract_address_hash: contract_address_hash,
value: 4000
)
paging_options = %PagingOptions{
key: {first_page.value, first_page.address_hash},
page_size: 2
}
result_paginated =
contract_address_hash
|> CurrentTokenBalance.token_holders_ordered_by_value(paging_options: paging_options)
|> Repo.all()
|> Enum.map(& &1.address_hash)
assert result_paginated == [second_page.address_hash]
end
end
end

@ -0,0 +1,95 @@
defmodule Explorer.Chain.Import.Address.CurrentTokenBalancesTest do
use Explorer.DataCase
alias Explorer.Chain.Import.Address.CurrentTokenBalances
alias Explorer.Chain.{Address.CurrentTokenBalance}
describe "insert/2" do
setup do
address = insert(:address, hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca")
token = insert(:token)
insert_options = %{
timeout: :infinity,
timestamps: %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()}
}
%{address: address, token: token, insert_options: insert_options}
end
test "inserts in the current token balances", %{address: address, token: token, insert_options: insert_options} do
changes = [
%{
address_hash: address.hash,
block_number: 1,
token_contract_address_hash: token.contract_address_hash,
value: Decimal.new(100)
}
]
CurrentTokenBalances.insert(changes, insert_options)
current_token_balances =
CurrentTokenBalance
|> Explorer.Repo.all()
|> Enum.count()
assert current_token_balances == 1
end
test "considers the last block upserting", %{address: address, token: token, insert_options: insert_options} do
insert(
:address_current_token_balance,
address: address,
block_number: 1,
token_contract_address_hash: token.contract_address_hash,
value: 100
)
changes = [
%{
address_hash: address.hash,
block_number: 2,
token_contract_address_hash: token.contract_address_hash,
value: Decimal.new(200)
}
]
CurrentTokenBalances.insert(changes, insert_options)
current_token_balance = Explorer.Repo.get_by(CurrentTokenBalance, address_hash: address.hash)
assert current_token_balance.block_number == 2
assert current_token_balance.value == Decimal.new(200)
end
test "considers the last block when there are duplicated params", %{
address: address,
token: token,
insert_options: insert_options
} do
changes = [
%{
address_hash: address.hash,
block_number: 4,
token_contract_address_hash: token.contract_address_hash,
value: Decimal.new(200)
},
%{
address_hash: address.hash,
block_number: 1,
token_contract_address_hash: token.contract_address_hash,
value: Decimal.new(100)
}
]
CurrentTokenBalances.insert(changes, insert_options)
current_token_balance = Explorer.Repo.get_by(CurrentTokenBalance, address_hash: address.hash)
assert current_token_balance.block_number == 4
assert current_token_balance.value == Decimal.new(200)
end
end
end

@ -6,6 +6,7 @@ defmodule Explorer.Chain.ImportTest do
alias Explorer.Chain.{
Address,
Address.TokenBalance,
Address.CurrentTokenBalance,
Block,
Data,
Log,
@ -395,6 +396,55 @@ defmodule Explorer.Chain.ImportTest do
assert 3 == count
end
test "inserts a current_token_balance" do
params = %{
addresses: %{
params: [
%{hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca"},
%{hash: "0x515c09c5bba1ed566b02a5b0599ec5d5d0aee73d"},
%{hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"}
],
timeout: 5
},
tokens: %{
on_conflict: :nothing,
params: [
%{
contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
type: "ERC-20"
}
],
timeout: 5
},
address_current_token_balances: %{
params: [
%{
address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
block_number: "37",
value: 200
},
%{
address_hash: "0x515c09c5bba1ed566b02a5b0599ec5d5d0aee73d",
token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
block_number: "37",
value: 100
}
],
timeout: 5
}
}
Import.all(params)
count =
CurrentTokenBalance
|> Explorer.Repo.all()
|> Enum.count()
assert count == 2
end
test "with empty map" do
assert {:ok, %{}} == Import.all(%{})
end

@ -1182,7 +1182,10 @@ defmodule Explorer.ChainTest do
|> Enum.map(&insert(:address, fetched_coin_balance: &1))
|> Enum.map(& &1.hash)
assert address_hashes == Enum.map(Chain.list_top_addresses(), & &1.hash)
assert address_hashes ==
Chain.list_top_addresses()
|> Enum.map(fn {address, _transaction_count} -> address end)
|> Enum.map(& &1.hash)
end
test "with top addresses in order with matching value" do
@ -1201,7 +1204,10 @@ defmodule Explorer.ChainTest do
|> insert(fetched_coin_balance: 4, hash: Enum.fetch!(test_hashes, 4))
|> Map.fetch!(:hash)
assert [first_result_hash | tail] == Enum.map(Chain.list_top_addresses(), & &1.hash)
assert [first_result_hash | tail] ==
Chain.list_top_addresses()
|> Enum.map(fn {address, _transaction_count} -> address end)
|> Enum.map(& &1.hash)
end
end
@ -2956,173 +2962,32 @@ defmodule Explorer.ChainTest do
end
describe "fetch_token_holders_from_token_hash/2" do
test "returns the last value for each address" do
test "returns the token holders" do
%Token{contract_address_hash: contract_address_hash} = insert(:token)
address = insert(:address)
address_a = insert(:address)
address_b = insert(:address)
insert(
:token_balance,
address: address,
block_number: 1000,
:address_current_token_balance,
address: address_a,
token_contract_address_hash: contract_address_hash,
value: 5000
)
insert(
:token_balance,
block_number: 1001,
token_contract_address_hash: contract_address_hash,
value: 4000
)
insert(
:token_balance,
address: address,
block_number: 1002,
token_contract_address_hash: contract_address_hash,
value: 2000
)
values =
contract_address_hash
|> Chain.fetch_token_holders_from_token_hash([])
|> Enum.map(&Decimal.to_integer(&1.value))
assert values == [4000, 2000]
end
test "sort by the highest value" do
%Token{contract_address_hash: contract_address_hash} = insert(:token)
insert(
:token_balance,
block_number: 1000,
token_contract_address_hash: contract_address_hash,
value: 2000
)
insert(
:token_balance,
:address_current_token_balance,
address: address_b,
block_number: 1001,
token_contract_address_hash: contract_address_hash,
value: 1000
)
insert(
:token_balance,
block_number: 1002,
token_contract_address_hash: contract_address_hash,
value: 4000
)
insert(
:token_balance,
block_number: 1002,
token_contract_address_hash: contract_address_hash,
value: 3000
)
values =
token_holders_count =
contract_address_hash
|> Chain.fetch_token_holders_from_token_hash([])
|> Enum.map(&Decimal.to_integer(&1.value))
assert values == [4000, 3000, 2000, 1000]
end
test "returns only token balances that have value" do
%Token{contract_address_hash: contract_address_hash} = insert(:token)
insert(
:token_balance,
token_contract_address_hash: contract_address_hash,
value: 0
)
assert Chain.fetch_token_holders_from_token_hash(contract_address_hash, []) == []
end
test "returns an empty list when there are no address with value greater than 0" do
%Token{contract_address_hash: contract_address_hash} = insert(:token)
insert(:token_balance, value: 1000)
assert Chain.fetch_token_holders_from_token_hash(contract_address_hash, []) == []
end
test "ignores the burn address" do
{:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000")
burn_address = insert(:address, hash: burn_address_hash)
%Token{contract_address_hash: contract_address_hash} = insert(:token)
insert(
:token_balance,
address: burn_address,
token_contract_address_hash: contract_address_hash,
value: 1000
)
assert Chain.fetch_token_holders_from_token_hash(contract_address_hash, []) == []
end
test "paginates the result by value and different address" do
address_a = build(:address, hash: "0xcb2cf1fd3199584ac5faa16c6aca49472dc6495a")
address_b = build(:address, hash: "0x5f26097334b6a32b7951df61fd0c5803ec5d8354")
%Token{contract_address_hash: contract_address_hash} = insert(:token)
first_page =
insert(
:token_balance,
address: address_a,
token_contract_address_hash: contract_address_hash,
value: 4000
)
second_page =
insert(
:token_balance,
address: address_b,
token_contract_address_hash: contract_address_hash,
value: 4000
)
paging_options = %PagingOptions{
key: {first_page.value, first_page.address_hash},
page_size: 2
}
holders_paginated =
contract_address_hash
|> Chain.fetch_token_holders_from_token_hash(paging_options: paging_options)
|> Enum.map(& &1.address_hash)
assert holders_paginated == [second_page.address_hash]
end
test "considers the last block only if it has value" do
address = insert(:address, hash: "0x5f26097334b6a32b7951df61fd0c5803ec5d8354")
%Token{contract_address_hash: contract_address_hash} = insert(:token)
insert(
:token_balance,
address: address,
block_number: 1000,
token_contract_address_hash: contract_address_hash,
value: 5000
)
insert(
:token_balance,
address: address,
block_number: 1002,
token_contract_address_hash: contract_address_hash,
value: 0
)
|> Enum.count()
assert Chain.fetch_token_holders_from_token_hash(contract_address_hash, []) == []
assert token_holders_count == 2
end
end

@ -13,6 +13,7 @@ defmodule Explorer.Factory do
alias Explorer.Chain.{
Address,
Address.CurrentTokenBalance,
Address.TokenBalance,
Address.CoinBalance,
Block,
@ -480,6 +481,16 @@ defmodule Explorer.Factory do
}
end
def address_current_token_balance_factory() do
%CurrentTokenBalance{
address: build(:address),
token_contract_address_hash: insert(:token).contract_address_hash,
block_number: block_number(),
value: Enum.random(1..100_000),
value_fetched_at: DateTime.utc_now()
}
end
defmacrop left + right do
quote do
fragment("? + ?", unquote(left), unquote(right))

@ -5,9 +5,10 @@ use Mix.Config
import Bitwise
config :indexer,
block_transformer: Indexer.Block.Transform.Base,
ecto_repos: [Explorer.Repo],
# bytes
memory_limit: 1 <<< 30,
ecto_repos: [Explorer.Repo]
memory_limit: 1 <<< 30
config :logger, :indexer,
# keep synced with `config/config.exs`

@ -9,6 +9,7 @@ defmodule Indexer.Block.Fetcher do
alias Indexer.{AddressExtraction, CoinBalance, MintTransfer, Token, TokenTransfers}
alias Indexer.Address.{CoinBalances, TokenBalances}
alias Indexer.Block.Fetcher.Receipts
alias Indexer.Block.Transform
@type address_hash_to_fetched_balance_block_number :: %{String.t() => Block.block_number()}
@type transaction_hash_to_block_number :: %{String.t() => Block.block_number()}
@ -96,6 +97,7 @@ defmodule Indexer.Block.Fetcher do
transactions: transactions_without_receipts,
block_second_degree_relations: block_second_degree_relations
} = result,
blocks = Transform.transform_blocks(blocks),
{:receipts, {:ok, receipt_params}} <- {:receipts, Receipts.fetch(state, transactions_without_receipts)},
%{logs: logs, receipts: receipts} = receipt_params,
transactions_with_receipts = Receipts.put(transactions_without_receipts, receipts),

@ -107,6 +107,7 @@ defmodule Indexer.Block.Realtime.Fetcher do
|> put_in([:addresses, :params], balances_addresses_params)
|> put_in([:blocks, :params, Access.all(), :consensus], true)
|> put_in([Access.key(:address_coin_balances, %{}), :params], balances_params)
|> put_in([Access.key(:address_current_token_balances, %{}), :params], address_token_balances)
|> put_in([Access.key(:address_token_balances), :params], address_token_balances)
|> put_in([Access.key(:internal_transactions, %{}), :params], internal_transactions_params),
{:ok, imported} = ok <- Chain.import(chain_import_options) do

@ -0,0 +1,31 @@
defmodule Indexer.Block.Transform do
@moduledoc """
Protocol for transforming blocks.
"""
@type block :: map()
@doc """
Transforms a block.
"""
@callback transform(block :: block()) :: block()
@doc """
Runs a list of blocks through the configured block transformer.
"""
def transform_blocks(blocks) when is_list(blocks) do
transformer = Application.get_env(:indexer, :block_transformer)
unless transformer do
raise ArgumentError,
"""
No block transformer defined. Set a blocker transformer."
config :indexer,
block_transformer: Indexer.Block.Transform.Base
"""
end
Enum.map(blocks, &transformer.transform/1)
end
end

@ -0,0 +1,14 @@
defmodule Indexer.Block.Transform.Base do
@moduledoc """
Default block transformer to be used.
"""
alias Indexer.Block.Transform
@behaviour Transform
@impl Transform
def transform(block) when is_map(block) do
block
end
end

@ -0,0 +1,16 @@
defmodule Indexer.Block.Transform.Clique do
@moduledoc """
Handles block transforms for Clique chain.
"""
alias Indexer.Block.{Transform, Util}
@behaviour Transform
@impl Transform
def transform(block) when is_map(block) do
miner_address = Util.signer(block)
%{block | miner_hash: miner_address}
end
end

@ -0,0 +1,75 @@
defmodule Indexer.Block.Util do
@moduledoc """
Helper functions for parsing block information.
"""
@doc """
Calculates the signer's address by recovering the ECDSA public key.
https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm
"""
def signer(block) when is_map(block) do
# Last 65 bytes is the signature. Multiply by two since we haven't transformed to raw bytes
{extra_data, signature} = String.split_at(trim_prefix(block.extra_data), -130)
block = %{block | extra_data: extra_data}
signature_hash = signature_hash(block)
recover_pub_key(signature_hash, decode(signature))
end
# Signature hash calculated from the block header.
# Needed for PoA-based chains
defp signature_hash(block) do
header_data = [
decode(block.parent_hash),
decode(block.sha3_uncles),
decode(block.miner_hash),
decode(block.state_root),
decode(block.transactions_root),
decode(block.receipts_root),
decode(block.logs_bloom),
block.difficulty,
block.number,
block.gas_limit,
block.gas_used,
DateTime.to_unix(block.timestamp),
decode(block.extra_data),
decode(block.mix_hash),
decode(block.nonce)
]
:keccakf1600.hash(:sha3_256, ExRLP.encode(header_data))
end
defp trim_prefix("0x" <> rest), do: rest
defp decode("0x" <> rest) do
decode(rest)
end
defp decode(data) do
Base.decode16!(data, case: :mixed)
end
# Recovers the key from the signature hash and signature
defp recover_pub_key(signature_hash, signature) do
<<
r::bytes-size(32),
s::bytes-size(32),
v::integer-size(8)
>> = signature
# First byte represents compression which can be ignored
# Private key is the last 64 bytes
{:ok, <<_compression::bytes-size(1), private_key::binary>>} =
:libsecp256k1.ecdsa_recover_compact(signature_hash, r <> s, :uncompressed, v)
# Public key comes from the last 20 bytes
<<_::bytes-size(12), public_key::binary>> = :keccakf1600.hash(:sha3_256, private_key)
miner_address = Base.encode16(public_key, case: :lower)
"0x" <> miner_address
end
end

@ -80,7 +80,13 @@ defmodule Indexer.TokenBalance.Fetcher do
end
def import_token_balances(token_balances_params) do
case Chain.import(%{address_token_balances: %{params: token_balances_params}, timeout: :infinity}) do
import_params = %{
address_token_balances: %{params: token_balances_params},
address_current_token_balances: %{params: token_balances_params},
timeout: :infinity
}
case Chain.import(import_params) do
{:ok, _} ->
:ok

@ -29,7 +29,7 @@ defmodule Indexer.TokenBalances do
token_balances
|> Task.async_stream(&fetch_token_balance/1, on_timeout: :kill_task)
|> Stream.map(&format_task_results/1)
|> Enum.filter(&ignore_request_with_timeouts/1)
|> Enum.filter(&ignore_request_with_errors/1)
token_balances
|> MapSet.new()
@ -70,11 +70,12 @@ defmodule Indexer.TokenBalances do
|> TokenBalance.Fetcher.async_fetch()
end
def format_task_results({:exit, :timeout}), do: {:error, :timeout}
def format_task_results({:ok, token_balance}), do: token_balance
defp format_task_results({:exit, :timeout}), do: {:error, :timeout}
defp format_task_results({:ok, token_balance}), do: token_balance
def ignore_request_with_timeouts({:error, :timeout}), do: false
def ignore_request_with_timeouts(_token_balance), do: true
defp ignore_request_with_errors({:error, :timeout}), do: false
defp ignore_request_with_errors(%{value: nil, value_fetched_at: nil, error: _error}), do: false
defp ignore_request_with_errors(_token_balance), do: true
def log_fetching_errors(from, token_balances_params) do
error_messages =

@ -46,10 +46,14 @@ defmodule Indexer.MixProject do
[
# JSONRPC access to Parity for `Explorer.Indexer`
{:ethereum_jsonrpc, in_umbrella: true},
# RLP encoding
{:ex_rlp, "~> 0.3"},
# Code coverage
{:excoveralls, "~> 0.10.0", only: [:test], github: "KronicDeth/excoveralls", branch: "circle-workflows"},
# Importing to database
{:explorer, in_umbrella: true},
# libsecp2561k1 crypto functions
{:libsecp256k1, "~> 0.1.10"},
# Log errors and application output to separate files
{:logger_file_backend, "~> 0.0.10"},
# Mocking `EthereumJSONRPC.Transport`, so we avoid hitting real chains for local testing

@ -448,20 +448,26 @@ defmodule Indexer.Block.Catchup.BoundIntervalSupervisorTest do
jsonrpc: "2.0",
result: %{
"difficulty" => "0x0",
"extraData" => "0x0",
"gasLimit" => "0x0",
"gasUsed" => "0x0",
"hash" =>
Explorer.Factory.block_hash()
|> to_string(),
"logsBloom" => "0x0",
"miner" => "0xb2930b35844a230f00e51431acae96fe543a0347",
"number" => "0x0",
"parentHash" =>
Explorer.Factory.block_hash()
|> to_string(),
"receiptsRoot" => "0x0",
"sha3Uncles" => "0x0",
"size" => "0x0",
"stateRoot" => "0x0",
"timestamp" => "0x0",
"totalDifficulty" => "0x0",
"transactions" => [],
"transactionsRoot" => "0x0",
"uncles" => []
}
}

@ -0,0 +1,42 @@
defmodule Indexer.Block.Transform.BaseTest do
use ExUnit.Case
alias Indexer.Block.Transform.Base
@block %{
difficulty: 1,
extra_data:
"0xd68301080d846765746886676f312e3130856c696e7578000000000000000000773ab2ca8f47904a14739ad80a75b71d9d29b9fff8b7ecdcb73efffa6f74122f17d304b5dc8e6e5f256c9474dd115c8d4dae31b7a3d409e5c3270f8fde41cd8c00",
gas_limit: 7_753_377,
gas_used: 1_810_195,
hash: "0x7004c895e812c55b0c2be8a46d72ca300a683dc27d1d7917ee7742d4d0359c1f",
logs_bloom:
"0x00000000000000020000000000002000000400000000000000000000000000000000000000000000040000080004000020000010000000000000000000000000000000000000000008000008000000000000000000200000000000000000000000000000020000000000000000000800000000000000804000000010080000000800000000000000000000000000000000000000000000800000000000080000000008000400000000404000000000000000000000000200000000000000000000000002000000000000001002000000000000002000000008000000000020000000000000000000000000000000000000000000000000400000800000000000",
miner: "0x0000000000000000000000000000000000000000",
mix_hash: "0x0000000000000000000000000000000000000000000000000000000000000000",
nonce: "0x0000000000000000",
number: 2_848_394,
parent_hash: "0x20350fc367e19d3865be1ea7da72ab81f8f9941c43ac6bb24a34a0a7caa2f3df",
receipts_root: "0x6ade4ac1079ea50cfadcce2b75ffbe4f9b14bf69b4607bbf1739463076ca6246",
sha3_uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
size: 6437,
state_root: "0x23f63347851bcd109059d007d71e19c4f5e73b7f0862bebcd04458333a004d92",
timestamp: DateTime.from_unix!(1_534_796_040),
total_difficulty: 5_353_647,
transactions: [
"0x7e3bb851fc74a436826d2af6b96e4db9484431811ef0d9c9e78370488d33d4e5",
"0x3976fd1e3d2a715c3cfcfde9bd3210798c26c017b8edb841d319227ecb3322fb",
"0xd8db124005bb8b6fda7b71fd56ac782552a66af58fe843ba3c4930423b87d1d2",
"0x10c1a1ca4d9f4b2bd5b89f7bbcbbc2d69e166fe23662b8db4f6beae0f50ac9fd",
"0xaa58a6545677c796a56b8bc874174c8cfd31a6c6e6ca3a87e086d4f66d52858a"
],
transactions_root: "0xde8d25c0b9b54310128a21601331094b43f910f9f96102869c2e2dca94884bf4",
uncles: []
}
describe "transform/1" do
test "passes the block through unchanged" do
assert Base.transform(@block) == @block
end
end
end

@ -0,0 +1,43 @@
defmodule Indexer.Block.Transform.CliqueTest do
use ExUnit.Case
alias Indexer.Block.Transform.Clique
@block %{
difficulty: 1,
extra_data:
"0xd68301080d846765746886676f312e3130856c696e7578000000000000000000773ab2ca8f47904a14739ad80a75b71d9d29b9fff8b7ecdcb73efffa6f74122f17d304b5dc8e6e5f256c9474dd115c8d4dae31b7a3d409e5c3270f8fde41cd8c00",
gas_limit: 7_753_377,
gas_used: 1_810_195,
hash: "0x7004c895e812c55b0c2be8a46d72ca300a683dc27d1d7917ee7742d4d0359c1f",
logs_bloom:
"0x00000000000000020000000000002000000400000000000000000000000000000000000000000000040000080004000020000010000000000000000000000000000000000000000008000008000000000000000000200000000000000000000000000000020000000000000000000800000000000000804000000010080000000800000000000000000000000000000000000000000000800000000000080000000008000400000000404000000000000000000000000200000000000000000000000002000000000000001002000000000000002000000008000000000020000000000000000000000000000000000000000000000000400000800000000000",
miner_hash: "0x0000000000000000000000000000000000000000",
mix_hash: "0x0000000000000000000000000000000000000000000000000000000000000000",
nonce: "0x0000000000000000",
number: 2_848_394,
parent_hash: "0x20350fc367e19d3865be1ea7da72ab81f8f9941c43ac6bb24a34a0a7caa2f3df",
receipts_root: "0x6ade4ac1079ea50cfadcce2b75ffbe4f9b14bf69b4607bbf1739463076ca6246",
sha3_uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
size: 6437,
state_root: "0x23f63347851bcd109059d007d71e19c4f5e73b7f0862bebcd04458333a004d92",
timestamp: DateTime.from_unix!(1_534_796_040),
total_difficulty: 5_353_647,
transactions: [
"0x7e3bb851fc74a436826d2af6b96e4db9484431811ef0d9c9e78370488d33d4e5",
"0x3976fd1e3d2a715c3cfcfde9bd3210798c26c017b8edb841d319227ecb3322fb",
"0xd8db124005bb8b6fda7b71fd56ac782552a66af58fe843ba3c4930423b87d1d2",
"0x10c1a1ca4d9f4b2bd5b89f7bbcbbc2d69e166fe23662b8db4f6beae0f50ac9fd",
"0xaa58a6545677c796a56b8bc874174c8cfd31a6c6e6ca3a87e086d4f66d52858a"
],
transactions_root: "0xde8d25c0b9b54310128a21601331094b43f910f9f96102869c2e2dca94884bf4",
uncles: []
}
describe "transform/1" do
test "updates the miner hash with signer address" do
expected = %{@block | miner_hash: "0xfc18cbc391de84dbd87db83b20935d3e89f5dd91"}
assert Clique.transform(@block) == expected
end
end
end

@ -0,0 +1,56 @@
defmodule Indexer.Block.TransformTest do
use ExUnit.Case
alias Indexer.Block.Transform
@block %{
difficulty: 1,
extra_data:
"0xd68301080d846765746886676f312e3130856c696e7578000000000000000000773ab2ca8f47904a14739ad80a75b71d9d29b9fff8b7ecdcb73efffa6f74122f17d304b5dc8e6e5f256c9474dd115c8d4dae31b7a3d409e5c3270f8fde41cd8c00",
gas_limit: 7_753_377,
gas_used: 1_810_195,
hash: "0x7004c895e812c55b0c2be8a46d72ca300a683dc27d1d7917ee7742d4d0359c1f",
logs_bloom:
"0x00000000000000020000000000002000000400000000000000000000000000000000000000000000040000080004000020000010000000000000000000000000000000000000000008000008000000000000000000200000000000000000000000000000020000000000000000000800000000000000804000000010080000000800000000000000000000000000000000000000000000800000000000080000000008000400000000404000000000000000000000000200000000000000000000000002000000000000001002000000000000002000000008000000000020000000000000000000000000000000000000000000000000400000800000000000",
miner_hash: "0x0000000000000000000000000000000000000000",
mix_hash: "0x0000000000000000000000000000000000000000000000000000000000000000",
nonce: "0x0000000000000000",
number: 2_848_394,
parent_hash: "0x20350fc367e19d3865be1ea7da72ab81f8f9941c43ac6bb24a34a0a7caa2f3df",
receipts_root: "0x6ade4ac1079ea50cfadcce2b75ffbe4f9b14bf69b4607bbf1739463076ca6246",
sha3_uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
size: 6437,
state_root: "0x23f63347851bcd109059d007d71e19c4f5e73b7f0862bebcd04458333a004d92",
timestamp: DateTime.from_unix!(1_534_796_040),
total_difficulty: 5_353_647,
transactions: [
"0x7e3bb851fc74a436826d2af6b96e4db9484431811ef0d9c9e78370488d33d4e5",
"0x3976fd1e3d2a715c3cfcfde9bd3210798c26c017b8edb841d319227ecb3322fb",
"0xd8db124005bb8b6fda7b71fd56ac782552a66af58fe843ba3c4930423b87d1d2",
"0x10c1a1ca4d9f4b2bd5b89f7bbcbbc2d69e166fe23662b8db4f6beae0f50ac9fd",
"0xaa58a6545677c796a56b8bc874174c8cfd31a6c6e6ca3a87e086d4f66d52858a"
],
transactions_root: "0xde8d25c0b9b54310128a21601331094b43f910f9f96102869c2e2dca94884bf4",
uncles: []
}
@blocks [@block, @block]
describe "transform_blocks/1" do
setup do
original = Application.get_env(:indexer, :block_transformer)
on_exit(fn -> Application.put_env(:indexer, :block_transformer, original) end)
end
test "transforms a list of blocks" do
assert Transform.transform_blocks(@blocks)
end
test "raises when no transformer is configured" do
Application.put_env(:indexer, :block_transformer, nil)
assert_raise ArgumentError, fn -> Transform.transform_blocks(@blocks) end
end
end
end

@ -69,6 +69,9 @@ defmodule Indexer.Block.Uncle.FetcherTest do
"number" => number_quantity,
"parentHash" => "0x006edcaa1e6fde822908783bc4ef1ad3675532d542fce53537557391cfe34c3c",
"size" => "0x243",
"receiptsRoot" => "0x0",
"sha3Uncles" => "0x0",
"stateRoot" => "0x0",
"timestamp" => "0x5b437f41",
"totalDifficulty" => "0x342337ffffffffffffffffffffffffed8d29bb",
"transactions" => [
@ -93,6 +96,7 @@ defmodule Indexer.Block.Uncle.FetcherTest do
"value" => "0x0"
}
],
"transactionsRoot" => "0x0",
"uncles" => [uncle_uncle_hash_data]
}
}

@ -0,0 +1,40 @@
defmodule Indexer.Block.UtilTest do
use ExUnit.Case
alias Indexer.Block.Util
test "signer/1" do
data = %{
difficulty: 1,
extra_data:
"0xd68301080d846765746886676f312e3130856c696e7578000000000000000000773ab2ca8f47904a14739ad80a75b71d9d29b9fff8b7ecdcb73efffa6f74122f17d304b5dc8e6e5f256c9474dd115c8d4dae31b7a3d409e5c3270f8fde41cd8c00",
gas_limit: 7_753_377,
gas_used: 1_810_195,
hash: "0x7004c895e812c55b0c2be8a46d72ca300a683dc27d1d7917ee7742d4d0359c1f",
logs_bloom:
"0x00000000000000020000000000002000000400000000000000000000000000000000000000000000040000080004000020000010000000000000000000000000000000000000000008000008000000000000000000200000000000000000000000000000020000000000000000000800000000000000804000000010080000000800000000000000000000000000000000000000000000800000000000080000000008000400000000404000000000000000000000000200000000000000000000000002000000000000001002000000000000002000000008000000000020000000000000000000000000000000000000000000000000400000800000000000",
miner_hash: "0x0000000000000000000000000000000000000000",
mix_hash: "0x0000000000000000000000000000000000000000000000000000000000000000",
nonce: "0x0000000000000000",
number: 2_848_394,
parent_hash: "0x20350fc367e19d3865be1ea7da72ab81f8f9941c43ac6bb24a34a0a7caa2f3df",
receipts_root: "0x6ade4ac1079ea50cfadcce2b75ffbe4f9b14bf69b4607bbf1739463076ca6246",
sha3_uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
size: 6437,
state_root: "0x23f63347851bcd109059d007d71e19c4f5e73b7f0862bebcd04458333a004d92",
timestamp: DateTime.from_unix!(1_534_796_040),
total_difficulty: 5_353_647,
transactions: [
"0x7e3bb851fc74a436826d2af6b96e4db9484431811ef0d9c9e78370488d33d4e5",
"0x3976fd1e3d2a715c3cfcfde9bd3210798c26c017b8edb841d319227ecb3322fb",
"0xd8db124005bb8b6fda7b71fd56ac782552a66af58fe843ba3c4930423b87d1d2",
"0x10c1a1ca4d9f4b2bd5b89f7bbcbbc2d69e166fe23662b8db4f6beae0f50ac9fd",
"0xaa58a6545677c796a56b8bc874174c8cfd31a6c6e6ca3a87e086d4f66d52858a"
],
transactions_root: "0xde8d25c0b9b54310128a21601331094b43f910f9f96102869c2e2dca94884bf4",
uncles: []
}
assert Util.signer(data) == "0xfc18cbc391de84dbd87db83b20935d3e89f5dd91"
end
end

@ -45,28 +45,21 @@ defmodule Indexer.TokenBalancesTest do
} = List.first(result)
end
test "does not ignore calls that were returned with error" do
address = insert(:address)
test "ignores calls that gave errors to try fetch they again later" do
address = insert(:address, hash: "0x7113ffcb9c18a97da1b9cfc43e6cb44ed9165509")
token = insert(:token, contract_address: build(:contract_address))
address_hash_string = Hash.to_string(address.hash)
data = %{
token_contract_address_hash: token.contract_address_hash,
address_hash: address_hash_string,
block_number: 1_000
}
token_balances = [
%{
address_hash: to_string(address.hash),
block_number: 1_000,
token_contract_address_hash: to_string(token.contract_address_hash)
}
]
get_balance_from_blockchain_with_error()
{:ok, result} = TokenBalances.fetch_token_balances_from_blockchain([data])
assert %{
value: nil,
token_contract_address_hash: token_contract_address_hash,
address_hash: address_hash,
block_number: 1_000,
value_fetched_at: nil
} = List.first(result)
assert TokenBalances.fetch_token_balances_from_blockchain(token_balances) == {:ok, []}
end
test "ignores results that raised :timeout" do

@ -32,6 +32,7 @@
"ex_cldr_units": {:hex, :ex_cldr_units, "1.1.1", "b3c7256709bdeb3740a5f64ce2bce659eb9cf4cc1afb4cf94aba033b4a18bc5f", [:mix], [{:ex_cldr, "~> 1.0", [hex: :ex_cldr, optional: false]}, {:ex_cldr_numbers, "~> 1.0", [hex: :ex_cldr_numbers, optional: false]}]},
"ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"ex_machina": {:hex, :ex_machina, "2.2.1", "df84d0b23487aaa8570c35e586d7f9f197a7787e1121344a41d8832a7ea41edf", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
"ex_rlp": {:hex, :ex_rlp, "0.3.1", "190554f7b26f79734fc5a772241eec14a71b2e83576e43f451479feb017013e9", [:mix], [], "hexpm"},
"exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], []},
"excoveralls": {:git, "https://github.com/KronicDeth/excoveralls.git", "0a859b68851eeba9b43eba59fbc8f9098299cfe1", [branch: "circle-workflows"]},
"exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, optional: false]}]},
@ -50,7 +51,7 @@
"jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], []},
"junit_formatter": {:hex, :junit_formatter, "2.2.0", "da6093f0740c58a824f9585ebb7cb1b960efaecf48d1fa969e95d9c47c6b19dd", [:mix], [], "hexpm"},
"keccakf1600": {:hex, :keccakf1600_orig, "2.0.0", "0a7217ddb3ee8220d449bbf7575ec39d4e967099f220a91e3dfca4dbaef91963", [:rebar3], []},
"libsecp256k1": {:hex, :libsecp256k1, "0.1.4", "42b7f76d8e32f85f578ccda0abfdb1afa0c5c231d1fd8aeab9cda352731a2d83", [:rebar3], []},
"libsecp256k1": {:hex, :libsecp256k1, "0.1.10", "d27495e2b9851c7765129b76c53b60f5e275bd6ff68292c50536bf6b8d091a4d", [:make, :mix], [{:mix_erlang_tasks, "0.1.0", [hex: :mix_erlang_tasks, repo: "hexpm", optional: false]}], "hexpm"},
"logger_file_backend": {:hex, :logger_file_backend, "0.0.10", "876f9f84ae110781207c54321ffbb62bebe02946fe3c13f0d7c5f5d8ad4fa910", [:mix], [], "hexpm"},
"makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
"makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
@ -59,6 +60,7 @@
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []},
"mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"},
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []},
"mix_erlang_tasks": {:hex, :mix_erlang_tasks, "0.1.0", "36819fec60b80689eb1380938675af215565a89320a9e29c72c70d97512e4649", [:mix], [], "hexpm"},
"mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"},
"mock": {:hex, :mock, "0.3.2", "e98e998fd76c191c7e1a9557c8617912c53df3d4a6132f561eb762b699ef59fa", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},
"mox": {:hex, :mox, "0.4.0", "7f120840f7d626184a3d65de36189ca6f37d432e5d63acd80045198e4c5f7e6e", [:mix], [], "hexpm"},

Loading…
Cancel
Save