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. 4
      apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex
  37. 6
      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 working_directory: ~/app
steps: steps:
- run: sudo apt-get update; sudo apt-get -y install autoconf build-essential libgmp3-dev libtool
- checkout - checkout
- run: mix local.hex --force - run: mix local.hex --force
@ -70,6 +72,11 @@ jobs:
- run: mix compile - 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` # `deps` needs to be cached with `_build` because `_build` will symlink into `deps`
- save_cache: - save_cache:

@ -1,300 +1,327 @@
import { reducer, initialState } from '../../js/pages/address' import { reducer, initialState } from '../../js/pages/address'
describe('PAGE_LOAD', () => { describe('RECEIVED_NEW_BLOCK', () => {
test('page 1 without filter', () => { test('with new block', () => {
const state = initialState const state = Object.assign({}, initialState, {
validationCount: 30,
validatedBlocks: [{ blockNumber: 1, blockHtml: 'test 1' }]
})
const action = { const action = {
type: 'PAGE_LOAD', type: 'RECEIVED_NEW_BLOCK',
addressHash: '1234', msg: { blockNumber: 2, blockHtml: 'test 2' }
beyondPageOne: false,
pendingTransactionHashes: ['1']
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.addressHash).toBe('1234') expect(output.validationCount).toEqual(31)
expect(output.beyondPageOne).toBe(false) expect(output.validatedBlocks).toEqual([
expect(output.filter).toBe(undefined) { blockNumber: 2, blockHtml: 'test 2' },
expect(output.pendingTransactionHashes).toEqual(['1']) { blockNumber: 1, blockHtml: 'test 1' }
])
}) })
test('page 2 without filter', () => { test('when channel has been disconnected', () => {
const state = initialState const state = Object.assign({}, initialState, {
channelDisconnected: true,
validationCount: 30,
validatedBlocks: [{ blockNumber: 1, blockHtml: 'test 1' }]
})
const action = { const action = {
type: 'PAGE_LOAD', type: 'RECEIVED_NEW_BLOCK',
addressHash: '1234', msg: { blockNumber: 2, blockHtml: 'test 2' }
beyondPageOne: true,
pendingTransactionHashes: ['1']
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.addressHash).toBe('1234') expect(output.validationCount).toEqual(30)
expect(output.beyondPageOne).toBe(true) expect(output.validatedBlocks).toEqual([
expect(output.filter).toBe(undefined) { blockNumber: 1, blockHtml: 'test 1' }
expect(output.pendingTransactionHashes).toEqual(['1']) ])
}) })
test('page 1 with "to" filter', () => { test('beyond page one', () => {
const state = initialState const state = Object.assign({}, initialState, {
beyondPageOne: true,
validationCount: 30,
validatedBlocks: [{ blockNumber: 1, blockHtml: 'test 1' }]
})
const action = { const action = {
type: 'PAGE_LOAD', type: 'RECEIVED_NEW_BLOCK',
addressHash: '1234', msg: { blockNumber: 2, blockHtml: 'test 2' }
beyondPageOne: false,
filter: 'to'
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.addressHash).toBe('1234') expect(output.validationCount).toEqual(31)
expect(output.beyondPageOne).toBe(false) expect(output.validatedBlocks).toEqual([
expect(output.filter).toBe('to') { 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 = { const action = {
type: 'PAGE_LOAD', type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH',
addressHash: '1234', msgs: [{ internalTransactionHtml: 'test 2' }]
beyondPageOne: true,
filter: 'to'
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.addressHash).toBe('1234') expect(output.internalTransactions).toEqual([
expect(output.beyondPageOne).toBe(true) { internalTransactionHtml: 'test 2' },
expect(output.filter).toBe('to') { internalTransactionHtml: 'test 1' }
])
}) })
}) test('with batch of new internal transactions', () => {
const state = Object.assign({}, initialState, {
test('CHANNEL_DISCONNECTED', () => { internalTransactions: [{ internalTransactionHtml: 'test 1' }]
const state = initialState })
const action = { const action = {
type: 'CHANNEL_DISCONNECTED' type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH',
} msgs: [
const output = reducer(state, action) { internalTransactionHtml: 'test 2' },
{ internalTransactionHtml: 'test 3' },
expect(output.channelDisconnected).toBe(true) { internalTransactionHtml: 'test 4' },
}) { internalTransactionHtml: 'test 5' },
{ internalTransactionHtml: 'test 6' },
test('RECEIVED_UPDATED_BALANCE', () => { { internalTransactionHtml: 'test 7' },
const state = initialState { internalTransactionHtml: 'test 8' },
const action = { { internalTransactionHtml: 'test 9' },
type: 'RECEIVED_UPDATED_BALANCE', { internalTransactionHtml: 'test 10' },
msg: { { internalTransactionHtml: 'test 11' },
balance: 'hello world' { internalTransactionHtml: 'test 12' },
{ internalTransactionHtml: 'test 13' }
]
} }
} const output = reducer(state, action)
const output = reducer(state, action)
expect(output.balance).toBe('hello world')
})
describe('RECEIVED_NEW_PENDING_TRANSACTION', () => { expect(output.internalTransactions).toEqual([
test('single transaction', () => { { internalTransactionHtml: 'test 1' }
const state = initialState ])
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 = { const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION', type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH',
msg: { msgs: [
transactionHash: '0x00', { internalTransactionHtml: 'test 2' }
transactionHtml: 'test' ]
}
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual(['test']) expect(output.internalTransactionsBatch).toEqual([
expect(output.transactionCount).toEqual(null) { internalTransactionHtml: 'test 2' },
{ internalTransactionHtml: 'test 1' }
])
}) })
test('single transaction after single transaction', () => { test('when channel has been disconnected', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
newPendingTransactions: ['test 1'] channelDisconnected: true,
internalTransactions: [{ internalTransactionHtml: 'test 1' }]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION', type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH',
msg: { msgs: [{ internalTransactionHtml: 'test 2' }]
transactionHash: '0x02',
transactionHtml: 'test 2'
}
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual(['test 1', 'test 2']) expect(output.internalTransactions).toEqual([
expect(output.pendingTransactionHashes.length).toEqual(1) { internalTransactionHtml: 'test 1' }
])
}) })
test('after disconnection', () => { test('beyond page one', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
channelDisconnected: true beyondPageOne: true,
internalTransactions: [{ internalTransactionHtml: 'test 1' }]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION', type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH',
msg: { msgs: [{ internalTransactionHtml: 'test 2' }]
transactionHash: '0x00',
transactionHtml: 'test'
}
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([]) expect(output.internalTransactions).toEqual([
expect(output.pendingTransactionHashes).toEqual([]) { internalTransactionHtml: 'test 1' }
])
}) })
test('on page 2', () => { test('with filtered out internal transaction', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
beyondPageOne: true filter: 'to'
}) })
const action = { const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION', type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH',
msg: { msgs: [{ internalTransactionHtml: 'test 2' }]
transactionHash: '0x00',
transactionHtml: 'test'
}
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([]) expect(output.internalTransactions).toEqual([])
expect(output.pendingTransactionHashes).toEqual([])
}) })
}) })
describe('RECEIVED_NEW_TRANSACTION', () => { describe('RECEIVED_NEW_PENDING_TRANSACTION', () => {
test('single transaction', () => { test('with new pending transaction', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
addressHash: '0x111' pendingTransactions: [{ transactionHash: 1, transactionHtml: 'test 1' }]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_TRANSACTION', type: 'RECEIVED_NEW_PENDING_TRANSACTION',
msg: { msg: { transactionHash: 2, transactionHtml: 'test 2' }
transactionHtml: 'test'
}
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newTransactions).toEqual([{ transactionHtml: 'test' }]) expect(output.pendingTransactions).toEqual([
expect(output.transactionCount).toEqual(null) { 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, { const state = Object.assign({}, initialState, {
newTransactions: [{ transactionHtml: 'test 1' }] channelDisconnected: true,
pendingTransactions: [{ transactionHash: 1, transactionHtml: 'test 1' }]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_TRANSACTION', type: 'RECEIVED_NEW_PENDING_TRANSACTION',
msg: { msg: { transactionHash: 2, transactionHtml: 'test 2' }
transactionHtml: 'test 2'
}
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newTransactions).toEqual([ expect(output.pendingTransactions).toEqual([
{ transactionHtml: 'test 1' }, { transactionHash: 1, transactionHtml: 'test 1' }
{ transactionHtml: 'test 2' }
]) ])
}) })
test('after disconnection', () => { test('beyond page one', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
channelDisconnected: true beyondPageOne: true,
pendingTransactions: [{ transactionHash: 1, transactionHtml: 'test 1' }]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_TRANSACTION', type: 'RECEIVED_NEW_PENDING_TRANSACTION',
msg: { msg: { transactionHash: 2, transactionHtml: 'test 2' }
transactionHtml: 'test'
}
} }
const output = reducer(state, action) 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, { const state = Object.assign({}, initialState, {
beyondPageOne: true, filter: 'to'
transactionCount: 1,
addressHash: '0x111'
}) })
const action = { const action = {
type: 'RECEIVED_NEW_TRANSACTION', type: 'RECEIVED_NEW_PENDING_TRANSACTION',
msg: { msg: { transactionHash: 2, transactionHtml: 'test 2' }
transactionHtml: 'test'
}
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newTransactions).toEqual([]) expect(output.pendingTransactions).toEqual([])
expect(output.transactionCount).toEqual(1)
}) })
test('transaction from current address with "from" filter', () => { })
describe('RECEIVED_NEW_TRANSACTION', () => {
test('with new transaction', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
addressHash: '1234', pendingTransactions: [{ transactionHash: 2, transactionHtml: 'test' }],
filter: 'from' transactions: [{ transactionHash: 1, transactionHtml: 'test 1' }]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_TRANSACTION', type: 'RECEIVED_NEW_TRANSACTION',
msg: { msg: { transactionHash: 2, transactionHtml: 'test 2' }
fromAddressHash: '1234',
transactionHtml: 'test'
}
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newTransactions).toEqual([ expect(output.pendingTransactions).toEqual([
{ fromAddressHash: '1234', transactionHtml: 'test' } { 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, { const state = Object.assign({}, initialState, {
addressHash: '1234', channelDisconnected: true,
filter: 'to' pendingTransactions: [{ transactionHash: 2, transactionHtml: 'test' }],
transactions: [{ transactionHash: 1, transactionHtml: 'test 1' }]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_TRANSACTION', type: 'RECEIVED_NEW_TRANSACTION',
msg: { msg: { transactionHash: 2, transactionHtml: 'test 2' }
fromAddressHash: '1234',
transactionHtml: 'test'
}
} }
const output = reducer(state, action) 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, { const state = Object.assign({}, initialState, {
addressHash: '1234', beyondPageOne: true,
filter: 'to' transactions: [{ transactionHash: 1, transactionHtml: 'test 1' }]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_TRANSACTION', type: 'RECEIVED_NEW_TRANSACTION',
msg: { msg: { transactionHash: 2, transactionHtml: 'test 2' }
toAddressHash: '1234',
transactionHtml: 'test'
}
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newTransactions).toEqual([ expect(output.pendingTransactions).toEqual([])
{ toAddressHash: '1234', transactionHtml: 'test' } 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, { const state = Object.assign({}, initialState, {
addressHash: '1234', filter: 'to'
filter: 'from'
}) })
const action = { const action = {
type: 'RECEIVED_NEW_TRANSACTION', type: 'RECEIVED_NEW_TRANSACTION',
msg: { msg: { transactionHash: 2, transactionHtml: 'test 2' }
toAddressHash: '1234',
transactionHtml: 'test'
}
} }
const output = reducer(state, action) 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 = { const action = {
type: 'RECEIVED_NEW_TRANSACTION', type: 'RECEIVED_NEXT_TRANSACTIONS_PAGE',
msg: { msg: {
transactionHash: '0x00', nextPageUrl: '2',
transactionHtml: 'test' transactions: [{ transactionHash: 2, transactionHtml: 'test 2' }]
} }
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newTransactions).toEqual([ expect(output.loadingNextPage).toEqual(false)
{ transactionHash: '0x00', transactionHtml: 'test' } 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', () => { test('CHANNEL_DISCONNECTED', () => {
const state = initialState 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' 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', () => { test('RECEIVED_NEW_BLOCK', () => {
const state = { ...initialState, blockNumber: 1 } const state = { ...initialState, blockNumber: 1 }
const action = { const action = {
@ -23,374 +12,3 @@ test('RECEIVED_NEW_BLOCK', () => {
expect(output.confirmations).toBe(4) 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 './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/clipboard_buttons'
import './lib/currency' import './lib/currency'
import './lib/from_now' import './lib/from_now'
@ -37,8 +44,3 @@ import './lib/token_balance_dropdown_search'
import './lib/token_transfers_toggle' import './lib/token_transfers_toggle'
import './lib/tooltip' import './lib/tooltip'
import './lib/try_api' 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('s', 60)
moment.relativeTimeThreshold('ss', 1) moment.relativeTimeThreshold('ss', 1)
export function updateAllAges () { export function updateAllAges ($container = $(document)) {
$('[data-from-now]').each((i, el) => tryUpdateAge(el)) $container.find('[data-from-now]').each((i, el) => tryUpdateAge(el))
return $container
} }
function tryUpdateAge (el) { function tryUpdateAge (el) {
if (!el.dataset.fromNow) return if (!el.dataset.fromNow) return

@ -4,47 +4,48 @@ import URI from 'urijs'
import humps from 'humps' import humps from 'humps'
import numeral from 'numeral' import numeral from 'numeral'
import socket from '../socket' import socket from '../socket'
import { batchChannel, initRedux, slideDownPrepend, slideUpRemove } from '../utils' import { createStore, connectElements, batchChannel, listMorph, onScrollBottom } from '../utils'
import { updateAllAges } from '../lib/from_now'
import { updateAllCalculatedUsdValues } from '../lib/currency.js' import { updateAllCalculatedUsdValues } from '../lib/currency.js'
import { loadTokenBalanceDropdown } from '../lib/token_balance_dropdown' import { loadTokenBalanceDropdown } from '../lib/token_balance_dropdown'
const BATCH_THRESHOLD = 10 const BATCH_THRESHOLD = 10
const TRANSACTION_VALIDATED_MOVE_DELAY = 1000
export const initialState = { export const initialState = {
addressHash: null,
balance: null,
batchCountAccumulator: 0,
beyondPageOne: null,
channelDisconnected: false, channelDisconnected: false,
addressHash: null,
filter: null, filter: null,
newBlock: null,
newInternalTransactions: [], balance: null,
newPendingTransactions: [],
newTransactions: [],
pendingTransactionHashes: [],
transactionCount: 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) { export function reducer (state = initialState, action) {
switch (action.type) { switch (action.type) {
case 'PAGE_LOAD': { case 'PAGE_LOAD':
return Object.assign({}, state, { case 'ELEMENTS_LOAD': {
addressHash: action.addressHash, return Object.assign({}, state, _.omit(action, 'type'))
beyondPageOne: action.beyondPageOne,
filter: action.filter,
pendingTransactionHashes: action.pendingTransactionHashes,
transactionCount: numeral(action.transactionCount).value(),
validationCount: action.validationCount ? numeral(action.validationCount).value() : null
})
} }
case 'CHANNEL_DISCONNECTED': { case 'CHANNEL_DISCONNECTED': {
if (state.beyondPageOne) return state if (state.beyondPageOne) return state
return Object.assign({}, state, { return Object.assign({}, state, {
channelDisconnected: true, channelDisconnected: true,
batchCountAccumulator: 0 internalTransactionsBatch: []
}) })
} }
case 'RECEIVED_NEW_BLOCK': { case 'RECEIVED_NEW_BLOCK': {
@ -54,7 +55,10 @@ export function reducer (state = initialState, action) {
if (state.beyondPageOne) return Object.assign({}, state, { validationCount }) if (state.beyondPageOne) return Object.assign({}, state, { validationCount })
return Object.assign({}, state, { return Object.assign({}, state, {
newBlock: action.msg.blockHtml, validatedBlocks: [
action.msg,
...state.validatedBlocks
],
validationCount validationCount
}) })
} }
@ -68,16 +72,19 @@ export function reducer (state = initialState, action) {
(state.filter === 'from' && fromAddressHash === state.addressHash) (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, { return Object.assign({}, state, {
newInternalTransactions: [ internalTransactions: [
...state.newInternalTransactions, ...incomingInternalTransactions.reverse(),
..._.map(incomingInternalTransactions, 'internalTransactionHtml') ...state.internalTransactions
] ]
}) })
} else { } else {
return Object.assign({}, state, { 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, { return Object.assign({}, state, {
newPendingTransactions: [ pendingTransactions: [
...state.newPendingTransactions, action.msg,
action.msg.transactionHtml ...state.pendingTransactions
],
pendingTransactionHashes: [
...state.pendingTransactionHashes,
action.msg.transactionHash
] ]
}) })
} }
case 'REMOVE_PENDING_TRANSACTION': {
return Object.assign({}, state, {
pendingTransactions: state.pendingTransactions.filter((transaction) => action.msg.transactionHash !== transaction.transactionHash)
})
}
case 'RECEIVED_NEW_TRANSACTION': { case 'RECEIVED_NEW_TRANSACTION': {
if (state.channelDisconnected) return state if (state.channelDisconnected) return state
@ -111,15 +119,12 @@ export function reducer (state = initialState, action) {
return Object.assign({}, state, { transactionCount }) return Object.assign({}, state, { transactionCount })
} }
const updatedPendingTransactionHashes =
_.without(state.pendingTransactionHashes, action.msg.transactionHash)
return Object.assign({}, state, { return Object.assign({}, state, {
newTransactions: [ pendingTransactions: state.pendingTransactions.map((transaction) => action.msg.transactionHash === transaction.transactionHash ? Object.assign({}, action.msg, { validated: true }) : transaction),
...state.newTransactions, transactions: [
action.msg action.msg,
...state.transactions
], ],
pendingTransactionHashes: updatedPendingTransactionHashes,
transactionCount: transactionCount transactionCount: transactionCount
}) })
} }
@ -128,109 +133,245 @@ export function reducer (state = initialState, action) {
balance: action.msg.balance 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: default:
return state return state
} }
} }
const $addressDetailsPage = $('[data-page="address-details"]') const elements = {
if ($addressDetailsPage.length) { '[data-selector="channel-disconnected-message"]': {
initRedux(reducer, { render ($el, state) {
main (store) { if (state.channelDisconnected) $el.show()
const addressHash = $addressDetailsPage[0].dataset.pageAddressHash }
const addressChannel = socket.channel(`addresses:${addressHash}`, {}) },
const { filter, blockNumber } = humps.camelizeKeys(URI(window.location).query(true)) '[data-selector="balance-card"]': {
store.dispatch({ load ($el) {
type: 'PAGE_LOAD', return { balance: $el.html() }
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) }))
}, },
render (state, oldState) { render ($el, state, oldState) {
const $balance = $('[data-selector="balance-card"]') if (oldState.balance === state.balance) return
const $channelBatching = $('[data-selector="channel-batching-message"]') $el.empty().append(state.balance)
const $channelBatchingCount = $('[data-selector="channel-batching-count"]') loadTokenBalanceDropdown()
const $channelDisconnected = $('[data-selector="channel-disconnected-message"]') updateAllCalculatedUsdValues()
const $emptyInternalTransactionsList = $('[data-selector="empty-internal-transactions-list"]') }
const $emptyTransactionsList = $('[data-selector="empty-transactions-list"]') },
const $internalTransactionsList = $('[data-selector="internal-transactions-list"]') '[data-selector="transaction-count"]': {
const $pendingTransactionsCount = $('[data-selector="pending-transactions-count"]') load ($el) {
const $pendingTransactionsList = $('[data-selector="pending-transactions-list"]') return { transactionCount: numeral($el.text()).value() }
const $transactionCount = $('[data-selector="transaction-count"]') },
const $transactionsList = $('[data-selector="transactions-list"]') render ($el, state, oldState) {
const $validationCount = $('[data-selector="validation-count"]') if (oldState.transactionCount === state.transactionCount) return
const $validationsList = $('[data-selector="validations-list"]') $el.empty().append(numeral(state.transactionCount).format())
}
if ($emptyInternalTransactionsList.length && state.newInternalTransactions.length) window.location.reload() },
if ($emptyTransactionsList.length && state.newTransactions.length) window.location.reload() '[data-selector="validation-count"]': {
if (state.channelDisconnected) $channelDisconnected.show() load ($el) {
if (oldState.balance !== state.balance) { return { validationCount: numeral($el.text()).value }
$balance.empty().append(state.balance) },
loadTokenBalanceDropdown() render ($el, state, oldState) {
updateAllCalculatedUsdValues() if (oldState.validationCount === state.validationCount) return
} $el.empty().append(numeral(state.validationCount).format())
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) { '[data-selector="loading-next-page"]': {
$channelBatching.show() render ($el, state) {
$channelBatchingCount[0].innerHTML = numeral(state.batchCountAccumulator).format() if (state.loadingNextPage) {
$el.show()
} else { } 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('')) render ($el, state, oldState) {
updateAllAges() 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() render ($el, state, oldState) {
newlyValidatedTransactions.forEach(({ transactionHash, transactionHtml }) => { if (oldState.transactions === state.transactions) return
let $transaction = $(`[data-selector="pending-transactions-list"] [data-transaction-hash="${transactionHash}"]`) function updateTransactions () {
$transaction.html($(transactionHtml).html()) const container = $el[0]
if ($transaction.is(':visible')) { const newElements = _.map(state.transactions, ({ transactionHtml }) => $(transactionHtml)[0])
setTimeout(() => { listMorph(container, newElements, { key: 'dataset.transactionHash' })
$transaction.addClass('shrink-out') }
setTimeout(() => { if ($('[data-selector="pending-transactions-list"]').is(':visible')) {
slideUpRemove($transaction) setTimeout(updateTransactions, TRANSACTION_VALIDATED_MOVE_DELAY + 400)
slideDownPrepend($transactionsList, transactionHtml) } else {
}, 400) updateTransactions()
}, 1000) }
} else { }
$transaction.remove() },
slideDownPrepend($transactionsList, transactionHtml) '[data-selector="internal-transactions-list"]': {
} load ($el) {
}) return {
updateAllAges() internalTransactions: $el.children().map((index, el) => ({
internalTransactionHtml: el.outerHTML
})).toArray()
} }
if (oldState.newBlock !== state.newBlock) { },
slideDownPrepend($validationsList, state.newBlock) render ($el, state, oldState) {
updateAllAges() 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 'jquery'
import _ from 'lodash'
import URI from 'urijs'
import humps from 'humps' import humps from 'humps'
import numeral from 'numeral' import numeral from 'numeral'
import socket from '../socket' import socket from '../socket'
import { updateAllAges } from '../lib/from_now' import { initRedux } from '../utils'
import { batchChannel, initRedux, slideDownPrepend, slideUpRemove } from '../utils'
const BATCH_THRESHOLD = 10
export const initialState = { export const initialState = {
batchCountAccumulator: 0,
newPendingTransactionHashesBatch: [],
beyondPageOne: null,
blockNumber: null, blockNumber: null,
channelDisconnected: false, confirmations: null
confirmations: null,
newPendingTransactions: [],
newTransactions: [],
newTransactionHashes: [],
transactionCount: null,
pendingTransactionCount: null
} }
export function reducer (state = initialState, action) { export function reducer (state = initialState, action) {
switch (action.type) { switch (action.type) {
case 'PAGE_LOAD': { case 'PAGE_LOAD': {
return Object.assign({}, state, { return Object.assign({}, state, {
beyondPageOne: action.beyondPageOne, blockNumber: parseInt(action.blockNumber, 10)
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
}) })
} }
case 'RECEIVED_NEW_BLOCK': { case 'RECEIVED_NEW_BLOCK': {
@ -46,62 +23,6 @@ export function reducer (state = initialState, action) {
}) })
} else return state } 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: default:
return state 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 'jquery'
import _ from 'lodash' 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) { export function batchChannel (func) {
let msgs = [] let msgs = []
@ -40,6 +42,37 @@ export function initRedux (reducer, { main, render, debug } = {}) {
if (main) main(store) 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) { export function skippedBlockListBuilder (skippedBlockNumbers, newestBlock, oldestBlock) {
for (let i = newestBlock - 1; i > oldestBlock; i--) skippedBlockNumbers.push(i) for (let i = newestBlock - 1; i > oldestBlock; i--) skippedBlockNumbers.push(i)
return skippedBlockNumbers 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) { export function slideDownBefore ($container, content) {
smarterSlideDown($(content), { smarterSlideDown($(content), {
insert ($el) { insert ($el) {
@ -105,3 +145,71 @@ function smarterSlideUp ($el, { complete = _.noop } = {}) {
$el.slideUp({ complete, easing: 'linear' }) $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", "version": "2.1.1",
"resolved": false, "resolved": false,
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"dev": true, "dev": true
"optional": true
}, },
"aproba": { "aproba": {
"version": "1.2.0", "version": "1.2.0",
@ -3814,15 +3813,13 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": false, "resolved": false,
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true, "dev": true
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"resolved": false, "resolved": false,
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@ -3839,22 +3836,19 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": false, "resolved": false,
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"dev": true, "dev": true
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": false, "resolved": false,
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true, "dev": true
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"resolved": false, "resolved": false,
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
"dev": true, "dev": true
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@ -3985,8 +3979,7 @@
"version": "2.0.3", "version": "2.0.3",
"resolved": false, "resolved": false,
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"dev": true, "dev": true
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@ -4000,7 +3993,6 @@
"resolved": false, "resolved": false,
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@ -4017,7 +4009,6 @@
"resolved": false, "resolved": false,
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@ -4026,15 +4017,13 @@
"version": "0.0.8", "version": "0.0.8",
"resolved": false, "resolved": false,
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"dev": true, "dev": true
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.2.4", "version": "2.2.4",
"resolved": false, "resolved": false,
"integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.1", "safe-buffer": "^5.1.1",
"yallist": "^3.0.0" "yallist": "^3.0.0"
@ -4055,7 +4044,6 @@
"resolved": false, "resolved": false,
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@ -4144,8 +4132,7 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": false, "resolved": false,
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"dev": true, "dev": true
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@ -4159,7 +4146,6 @@
"resolved": false, "resolved": false,
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@ -4255,8 +4241,7 @@
"version": "5.1.1", "version": "5.1.1",
"resolved": false, "resolved": false,
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
"dev": true, "dev": true
"optional": true
}, },
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
@ -4298,7 +4283,6 @@
"resolved": false, "resolved": false,
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@ -4320,7 +4304,6 @@
"resolved": false, "resolved": false,
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"ansi-regex": "^2.0.0" "ansi-regex": "^2.0.0"
} }
@ -4369,15 +4352,13 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": false, "resolved": false,
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true, "dev": true
"optional": true
}, },
"yallist": { "yallist": {
"version": "3.0.2", "version": "3.0.2",
"resolved": false, "resolved": false,
"integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=",
"dev": true, "dev": true
"optional": true
} }
} }
}, },
@ -6428,6 +6409,11 @@
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==",
"dev": true "dev": true
}, },
"nanoassert": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-1.1.0.tgz",
"integrity": "sha1-TzFS4JVA/eKMdvRLGbvNHVpCR40="
},
"nanomatch": { "nanomatch": {
"version": "1.2.13", "version": "1.2.13",
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", "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": { "natural-compare": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",

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

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

@ -7,9 +7,10 @@ defmodule BlockScoutWeb.AddressController do
def index(conn, _params) do def index(conn, _params) do
render(conn, "index.html", render(conn, "index.html",
addresses: Chain.list_top_addresses(), address_tx_count_pairs: Chain.list_top_addresses(),
address_estimated_count: Chain.address_estimated_count(), 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 end

@ -8,34 +8,87 @@ defmodule BlockScoutWeb.AddressTransactionController do
import BlockScoutWeb.AddressController, only: [transaction_count: 1, validation_count: 1] 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] 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, Market}
alias Explorer.Chain.Hash
alias Explorer.ExchangeRates.Token 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 def index(conn, %{"address_id" => address_hash_string} = params) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.hash_to_address(address_hash) do {:ok, address} <- Chain.hash_to_address(address_hash) do
pending_options = pending_options =
[ @transaction_necessity_by_association
necessity_by_association: %{ |> Keyword.merge(paging_options(%{}))
[created_contract_address: :names] => :optional,
[from_address: :names] => :optional,
[to_address: :names] => :optional,
:token_transfers => :optional
}
]
|> Keyword.merge(paging_options(params))
|> Keyword.merge(current_filter(params)) |> Keyword.merge(current_filter(params))
full_options = put_in(pending_options, [:necessity_by_association, :block], :required) full_options = put_in(pending_options, [:necessity_by_association, :block], :required)
transactions_plus_one = Chain.address_to_transactions(address, full_options) {transactions, next_page} = get_transactions_and_next_page(address, full_options)
{transactions, next_page} = split_list_by_page(transactions_plus_one)
pending_transactions = pending_transactions = Chain.address_to_pending_transactions(address, pending_options)
case Map.has_key?(params, "block_number") do
true -> []
false -> Chain.address_to_pending_transactions(address, pending_options)
end
render( render(
conn, conn,
@ -57,4 +110,9 @@ defmodule BlockScoutWeb.AddressTransactionController do
not_found(conn) not_found(conn)
end end
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 end

@ -3,6 +3,26 @@ defmodule BlockScoutWeb.API.RPC.TransactionController do
alias Explorer.Chain 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 def gettxreceiptstatus(conn, params) do
with {:txhash_param, {:ok, txhash_param}} <- fetch_txhash(params), with {:txhash_param, {:ok, txhash_param}} <- fetch_txhash(params),
{:format, {:ok, transaction_hash}} <- to_transaction_hash(txhash_param) do {: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")} {:txhash_param, Map.fetch(params, "txhash")}
end 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 defp to_transaction_hash(transaction_hash_string) do
{:format, Chain.string_to_transaction_hash(transaction_hash_string)} {:format, Chain.string_to_transaction_hash(transaction_hash_string)}
end end
@ -54,4 +81,11 @@ defmodule BlockScoutWeb.API.RPC.TransactionController do
_ -> "" _ -> ""
end end
end end
defp max_block_number do
case Chain.max_block_number() do
{:ok, number} -> number
{:error, :not_found} -> 0
end
end
end end

@ -321,6 +321,30 @@ defmodule BlockScoutWeb.Etherscan do
"result" => nil "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 %{ @transaction_gettxreceiptstatus_example_value %{
"status" => "1", "status" => "1",
"message" => "OK", "message" => "OK",
@ -428,6 +452,28 @@ defmodule BlockScoutWeb.Etherscan do
example: ~s("18") 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 %{ @address_balance %{
name: "AddressBalance", name: "AddressBalance",
fields: %{ 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 %{ @transaction_status_model %{
name: "TransactionStatus", name: "TransactionStatus",
fields: %{ 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 %{ @transaction_gettxreceiptstatus_action %{
name: "gettxreceiptstatus", name: "gettxreceiptstatus",
description: "Get transaction receipt status.", description: "Get transaction receipt status.",
@ -1692,6 +1804,7 @@ defmodule BlockScoutWeb.Etherscan do
@transaction_module %{ @transaction_module %{
name: "transaction", name: "transaction",
actions: [ actions: [
@transaction_gettxinfo_action,
@transaction_gettxreceiptstatus_action, @transaction_gettxreceiptstatus_action,
@transaction_getstatus_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 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, forward("/graphiql", Absinthe.Plug.GraphiQL,
schema: BlockScoutWeb.Schema, schema: BlockScoutWeb.Schema,
interface: :playground, interface: :playground,
socket: BlockScoutWeb.UserSocket socket: BlockScoutWeb.UserSocket,
analyze_complexity: true,
max_complexity: 50
) )
scope "/", BlockScoutWeb do scope "/", BlockScoutWeb do

@ -3,11 +3,18 @@ defmodule BlockScoutWeb.Schema do
use Absinthe.Schema use Absinthe.Schema
alias BlockScoutWeb.Resolvers.{Block, Transaction} alias BlockScoutWeb.Resolvers.{Address, Block, Transaction}
import_types(BlockScoutWeb.Schema.Types) import_types(BlockScoutWeb.Schema.Types)
query do 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." @desc "Gets a block by number."
field :block, :block do field :block, :block do
arg(:number, non_null(:integer)) arg(:number, non_null(:integer))

@ -3,7 +3,7 @@ defmodule BlockScoutWeb.Schema.Scalars do
use Absinthe.Schema.Notation use Absinthe.Schema.Notation
alias Explorer.Chain.{Hash, Wei} alias Explorer.Chain.{Data, Hash, Wei}
alias Explorer.Chain.Hash.{Address, Full, Nonce} alias Explorer.Chain.Hash.{Address, Full, Nonce}
@desc """ @desc """
@ -24,6 +24,23 @@ defmodule BlockScoutWeb.Schema.Scalars do
serialize(&to_string/1) serialize(&to_string/1)
end 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 """ @desc """
A 32-byte [KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash. 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(Absinthe.Type.Custom)
import_types(BlockScoutWeb.Schema.Scalars) 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 """ @desc """
A package of data that contains zero or more transactions, the hash of the previous block ("parent"), and optionally 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 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". structure that they form is called a "blockchain".
""" """
object :block do object :block do
field(:hash, :full_hash)
field(:consensus, :boolean) field(:consensus, :boolean)
field(:difficulty, :decimal) field(:difficulty, :decimal)
field(:gas_limit, :decimal) field(:gas_limit, :decimal)

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

@ -9,9 +9,10 @@
</p> </p>
<span data-selector="top-addresses-list"> <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", <%= render "_tile.html",
address: address, index: index, exchange_rate: @exchange_rate, address: address, index: index, exchange_rate: @exchange_rate,
total_supply: @total_supply, tx_count: tx_count,
validation_count: validation_count(address) %> validation_count: validation_count(address) %>
<% end %> <% end %>
</span> </span>

@ -8,12 +8,12 @@
</div> </div>
<div class="card-body"> <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"> <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> <a href="#" class="alert-link"><span data-selector="channel-batching-count"></span> <%= gettext "More internal transactions have come in" %></a>
</div> </div>
</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"> <div data-selector="reload-button" class="alert alert-danger">
<a href="#" class="alert-link"><%= gettext "Connection Lost, click to load newer internal transactions" %></a> <a href="#" class="alert-link"><%= gettext "Connection Lost, click to load newer internal transactions" %></a>
</div> </div>

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

@ -99,7 +99,7 @@
</div> </div>
<div class="card-body"> <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"> <div data-selector="reload-button" class="alert alert-danger">
<a href="#" class="alert-link"><%= gettext "Connection Lost, click to load newer validations" %></a> <a href="#" class="alert-link"><%= gettext "Connection Lost, click to load newer validations" %></a>
</div> </div>

@ -4,7 +4,7 @@
<h1 class="card-title mb-0">API Documentation</h2> <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> <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>
</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="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"> <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") %> <%= gettext("Internal Transaction") %>

@ -56,13 +56,25 @@
<%= gettext("Accounts") %> <%= gettext("Accounts") %>
<% end %> <% end %>
</li> </li>
<li class="nav-item"> <li class="nav-item dropdown">
<%= link to: api_docs_path(@conn, :index), class: "nav-link topnav-nav-link" do %> <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"> <span class="nav-link-icon">
<%= render BlockScoutWeb.IconsView, "_api_icon.html" %> <%= render BlockScoutWeb.IconsView, "_api_icon.html" %>
</span> </span>
<%= gettext("API") %> <%= gettext("APIs") %>
<% end %> </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> </li>
</ul> </ul>
<div class="search-form d-lg-flex d-inline-block"> <div class="search-form d-lg-flex d-inline-block">

@ -5,6 +5,6 @@
[<%= for item <- @outputs do %> [<%= for item <- @outputs do %>
<span class="function-response-item"><%= if named_argument?(item) do %><%= item["name"] %> <span class="function-response-item"><%= if named_argument?(item) do %><%= item["name"] %>
<% end %> <% 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> </pre>
</div> </div>

@ -45,13 +45,13 @@
<div data-wei-ether-converter> <div data-wei-ether-converter>
<span data-conversion-unit><%= output["value"] %></span> <span data-conversion-unit><%= output["value"] %></span>
<span class="py-2 px-2"> <span class="py-2 px-2">
<input class="wei-ether" type="checkbox" autocomplete="off"> <input class="wei-ether" type="checkbox" autocomplete="off">
<span class="d-inline-block" data-conversion-text-wei><%= gettext("WEI")%></span> <span class="d-inline-block" data-conversion-text-wei><%= gettext("WEI")%></span>
<span class="d-none" data-conversion-text-eth><%= gettext("ETH")%></span> <span class="d-none" data-conversion-text-eth><%= gettext("ETH")%></span>
</span> </span>
</div> </div>
<% else %> <% else %>
<%= output["value"] %> <%= values(output["value"], output["type"]) %>
<% end %> <% end %>
<% end %> <% end %>
<% end %> <% end %>

@ -9,17 +9,17 @@
<%= gettext("Validated Transactions") %> <%= gettext("Validated Transactions") %>
</p> </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"> <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> <a href="#" class="alert-link"><span data-selector="channel-batching-count"></span> <%= gettext "More transactions have come in" %></a>
</div> </div>
</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"> <div data-selector="reload-button" class="alert alert-danger">
<a href="#" class="alert-link"><%= gettext "Connection Lost, click to load newer transactions" %></a> <a href="#" class="alert-link"><%= gettext "Connection Lost, click to load newer transactions" %></a>
</div> </div>
</div> </div>
<span data-selector="transactions-list"> <span data-selector="transactions-list">
<%= for transaction <- @transactions do %> <%= for transaction <- @transactions do %>
<%= render BlockScoutWeb.TransactionView, "_tile.html", transaction: transaction %> <%= render BlockScoutWeb.TransactionView, "_tile.html", transaction: transaction %>

@ -1,7 +1,7 @@
defmodule BlockScoutWeb.AddressView do defmodule BlockScoutWeb.AddressView do
use BlockScoutWeb, :view use BlockScoutWeb, :view
import BlockScoutWeb.AddressController, only: [transaction_count: 1, validation_count: 1] import BlockScoutWeb.AddressController, only: [validation_count: 1]
alias BlockScoutWeb.LayoutView alias BlockScoutWeb.LayoutView
alias Explorer.Chain alias Explorer.Chain
@ -94,16 +94,20 @@ defmodule BlockScoutWeb.AddressView do
format_wei_value(balance, :ether) format_wei_value(balance, :ether)
end end
def balance_percentage(%Address{fetched_coin_balance: balance}) do def balance_percentage(%Address{fetched_coin_balance: balance}, total_supply) do
balance balance
|> Wei.to(:ether) |> Wei.to(:ether)
|> Decimal.div(Decimal.new(Chain.total_supply())) |> Decimal.div(Decimal.new(total_supply))
|> Decimal.mult(100) |> Decimal.mult(100)
|> Decimal.round(4) |> Decimal.round(4)
|> Decimal.to_string(:normal) |> Decimal.to_string(:normal)
|> Kernel.<>("% #{gettext("Market Cap")}") |> Kernel.<>("% #{gettext("Market Cap")}")
end 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: nil}), do: ""
def balance_block_number(%Address{fetched_coin_balance_block_number: fetched_coin_balance_block_number}) 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 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 def render("gettxreceiptstatus.json", %{status: status}) do
prepared_status = prepare_tx_receipt_status(status) prepared_status = prepare_tx_receipt_status(status)
RPCView.render("show.json", data: %{"status" => prepared_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("_", " ") "errDescription" => error |> Atom.to_string() |> String.replace("_", " ")
} }
end 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 end

@ -3,13 +3,24 @@ defmodule BlockScoutWeb.SmartContractView do
def queryable?(inputs), do: Enum.any?(inputs) 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" => ""}), do: false
def named_argument?(%{"name" => nil}), do: false def named_argument?(%{"name" => nil}), do: false
def named_argument?(%{"name" => _}), do: true def named_argument?(%{"name" => _}), do: true
def named_argument?(_), do: false def named_argument?(_), do: false
def values(values) when is_list(values), do: Enum.join(values, ",") def values(addresses, type) when type == "address[]" do
def values(value), do: value 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 end

@ -83,11 +83,6 @@ msgstr ""
msgid "A string with the name of the module to be invoked." msgid "A string with the name of the module to be invoked."
msgstr "" msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:64
msgid "API"
msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/api_docs/_metatags.html.eex:4 #: lib/block_scout_web/templates/api_docs/_metatags.html.eex:4
msgid "API endpoints for the %{subnetwork}" 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/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:119
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:141 #: 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" msgid "Code"
msgstr "" 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:14
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:43 #: 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/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 #: lib/block_scout_web/views/transaction_view.ex:176
msgid "Internal Transactions" msgid "Internal Transactions"
msgstr "" msgstr ""
@ -639,7 +634,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:72 #: 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/address_validation/index.html.eex:117
#: lib/block_scout_web/templates/block/index.html.eex:20 #: lib/block_scout_web/templates/block/index.html.eex:20
#: lib/block_scout_web/templates/block_transaction/index.html.eex:50 #: lib/block_scout_web/templates/block_transaction/index.html.eex:50
@ -661,12 +656,12 @@ msgid "Owner Address"
msgstr "" msgstr ""
#, elixir-format #, 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" msgid "POA Core"
msgstr "" msgstr ""
#, elixir-format #, 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" msgid "POA Sokol"
msgstr "" msgstr ""
@ -731,7 +726,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_validation/index.html.eex:53 #: 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:33
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:75 #: 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 #: lib/block_scout_web/views/tokens/overview_view.ex:37
msgid "Read Contract" msgid "Read Contract"
msgstr "" msgstr ""
@ -757,13 +752,13 @@ msgid "Responses"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:71 #: lib/block_scout_web/templates/layout/_topnav.html.eex:83
#: lib/block_scout_web/templates/layout/_topnav.html.eex:78 #: lib/block_scout_web/templates/layout/_topnav.html.eex:90
msgid "Search" msgid "Search"
msgstr "" msgstr ""
#, elixir-format #, 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" msgid "Search by address, transaction hash, or block number"
msgstr "" msgstr ""
@ -858,7 +853,7 @@ msgid "There are no tokens."
msgstr "" msgstr ""
#, elixir-format #, 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." msgid "There are no transactions for this address."
msgstr "" msgstr ""
@ -872,11 +867,6 @@ msgstr ""
msgid "There are no transfers for this Token." msgid "There are no transfers for this Token."
msgstr "" 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 #, elixir-format
#: lib/block_scout_web/templates/transaction/overview.html.eex:23 #: lib/block_scout_web/templates/transaction/overview.html.eex:23
msgid "This transaction is pending confirmation." 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:18
#: lib/block_scout_web/templates/address_validation/index.html.eex:62 #: 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/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" msgid "Tokens"
msgstr "" msgstr ""
@ -1001,7 +991,7 @@ msgstr ""
#: lib/block_scout_web/templates/block_transaction/index.html.eex:35 #: 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/chain/show.html.eex:71
#: lib/block_scout_web/templates/layout/_topnav.html.eex:35 #: 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" msgid "Transactions"
msgstr "" msgstr ""

@ -83,11 +83,6 @@ msgstr ""
msgid "A string with the name of the module to be invoked." msgid "A string with the name of the module to be invoked."
msgstr "" msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:64
msgid "API"
msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/api_docs/_metatags.html.eex:4 #: lib/block_scout_web/templates/api_docs/_metatags.html.eex:4
msgid "API endpoints for the %{subnetwork}" 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/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:119
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:141 #: 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" msgid "Code"
msgstr "" 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:14
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:43 #: 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/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 #: lib/block_scout_web/views/transaction_view.ex:176
msgid "Internal Transactions" msgid "Internal Transactions"
msgstr "" msgstr ""
@ -639,7 +634,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:72 #: 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/address_validation/index.html.eex:117
#: lib/block_scout_web/templates/block/index.html.eex:20 #: lib/block_scout_web/templates/block/index.html.eex:20
#: lib/block_scout_web/templates/block_transaction/index.html.eex:50 #: lib/block_scout_web/templates/block_transaction/index.html.eex:50
@ -661,12 +656,12 @@ msgid "Owner Address"
msgstr "" msgstr ""
#, elixir-format #, 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" msgid "POA Core"
msgstr "" msgstr ""
#, elixir-format #, 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" msgid "POA Sokol"
msgstr "" msgstr ""
@ -731,7 +726,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_validation/index.html.eex:53 #: 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:33
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:75 #: 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 #: lib/block_scout_web/views/tokens/overview_view.ex:37
msgid "Read Contract" msgid "Read Contract"
msgstr "" msgstr ""
@ -757,13 +752,13 @@ msgid "Responses"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/layout/_topnav.html.eex:71 #: lib/block_scout_web/templates/layout/_topnav.html.eex:83
#: lib/block_scout_web/templates/layout/_topnav.html.eex:78 #: lib/block_scout_web/templates/layout/_topnav.html.eex:90
msgid "Search" msgid "Search"
msgstr "" msgstr ""
#, elixir-format #, 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" msgid "Search by address, transaction hash, or block number"
msgstr "" msgstr ""
@ -858,7 +853,7 @@ msgid "There are no tokens."
msgstr "" msgstr ""
#, elixir-format #, 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." msgid "There are no transactions for this address."
msgstr "" msgstr ""
@ -872,11 +867,6 @@ msgstr ""
msgid "There are no transfers for this Token." msgid "There are no transfers for this Token."
msgstr "" 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 #, elixir-format
#: lib/block_scout_web/templates/transaction/overview.html.eex:23 #: lib/block_scout_web/templates/transaction/overview.html.eex:23
msgid "This transaction is pending confirmation." 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:18
#: lib/block_scout_web/templates/address_validation/index.html.eex:62 #: 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/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" msgid "Tokens"
msgstr "" msgstr ""
@ -1001,7 +991,7 @@ msgstr ""
#: lib/block_scout_web/templates/block_transaction/index.html.eex:35 #: 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/chain/show.html.eex:71
#: lib/block_scout_web/templates/layout/_topnav.html.eex:35 #: 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" msgid "Transactions"
msgstr "" msgstr ""

@ -10,7 +10,9 @@ defmodule BlockScoutWeb.AddressControllerTest do
conn = get(conn, address_path(conn, :index)) 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
end end

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

@ -310,4 +310,114 @@ defmodule BlockScoutWeb.API.RPC.TransactionControllerTest do
assert response["message"] == "OK" assert response["message"] == "OK"
end end
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 end

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

@ -9,7 +9,7 @@ defmodule BlockScoutWeb.ViewingTokensTest do
insert_list( insert_list(
2, 2,
:token_balance, :address_current_token_balance,
token_contract_address_hash: token.contract_address_hash 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 = """
query ($number: Int!) { query ($number: Int!) {
block(number: $number) { block(number: $number) {
hash
consensus consensus
difficulty difficulty
gas_limit gas_limit
@ -31,6 +32,7 @@ defmodule BlockScoutWeb.Schema.Query.BlockTest do
assert json_response(conn, 200) == %{ assert json_response(conn, 200) == %{
"data" => %{ "data" => %{
"block" => %{ "block" => %{
"hash" => to_string(block.hash),
"consensus" => block.consensus, "consensus" => block.consensus,
"difficulty" => to_string(block.difficulty), "difficulty" => to_string(block.difficulty),
"gas_limit" => to_string(block.gas_limit), "gas_limit" => to_string(block.gas_limit),

@ -24,6 +24,12 @@ defmodule BlockScoutWeb.SmartContractViewTest do
assert SmartContractView.address?(type) assert SmartContractView.address?(type)
end 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 test "returns false when the type is not equal the string 'address'" do
type = "name" type = "name"
@ -57,17 +63,37 @@ defmodule BlockScoutWeb.SmartContractViewTest do
end end
end end
describe "values/1" do describe "values/2" do
test "joins the values when it is a list" do test "joins the values when it is a list of a given type" do
values = [8, 6, 9, 2, 2, 37] 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 end
test "returns the value" do test "returns the value when the type is neither 'address' nor 'address payable'" do
value = "POA" value = "POA"
assert SmartContractView.values(value) == "POA" assert SmartContractView.values(value, "not address") == "POA"
end end
end end
end end

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

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

@ -10,15 +10,6 @@ defmodule EthereumJSONRPC.RequestCoordinatorTest do
setup :set_mox_global setup :set_mox_global
setup :verify_on_exit! 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 setup do
table = Application.get_env(:ethereum_jsonrpc, EthereumJSONRPC.RequestCoordinator)[:rolling_window_opts][:table] table = Application.get_env(:ethereum_jsonrpc, EthereumJSONRPC.RequestCoordinator)[:rolling_window_opts][:table]

@ -95,18 +95,6 @@ defmodule EthereumJSONRPC.WebSocket.WebSocketClientTest do
end end
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 defp example_state(_) do
%{state: %WebSocketClient{url: "ws://example.com"}} %{state: %WebSocketClient{url: "ws://example.com"}}
end end

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

@ -23,6 +23,7 @@ defmodule Explorer.Chain do
alias Explorer.Chain.{ alias Explorer.Chain.{
Address, Address,
Address.CoinBalance, Address.CoinBalance,
Address.CurrentTokenBalance,
Address.TokenBalance, Address.TokenBalance,
Block, Block,
Data, 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. 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 def list_top_addresses do
Address query =
|> limit(250) from(a in Address,
|> order_by(desc: :fetched_coin_balance, asc: :hash) left_join: t in Transaction,
|> where([address], address.fetched_coin_balance > ^0) on: a.hash == t.from_address_hash,
|> Repo.all() 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 end
@doc """ @doc """
@ -2070,7 +2078,7 @@ defmodule Explorer.Chain do
@spec fetch_token_holders_from_token_hash(Hash.Address.t(), [paging_options]) :: [TokenBalance.t()] @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 def fetch_token_holders_from_token_hash(contract_address_hash, options) do
contract_address_hash contract_address_hash
|> TokenBalance.token_holders_ordered_by_value(options) |> CurrentTokenBalance.token_holders_ordered_by_value(options)
|> Repo.all() |> Repo.all()
end 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 use Ecto.Schema
import Ecto.Changeset 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.TokenBalance
alias Explorer.Chain.{Address, Block, Hash, Token} alias Explorer.Chain.{Address, Block, Hash, Token}
@default_paging_options %PagingOptions{page_size: 50}
@typedoc """ @typedoc """
* `address` - The `t:Explorer.Chain.Address.t/0` that is the balance's owner. * `address` - The `t:Explorer.Chain.Address.t/0` that is the balance's owner.
* `address_hash` - The address hash foreign key. * `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) from(tb in subquery(query), where: tb.value > 0, preload: :token)
end 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 """ @doc """
Builds an `Ecto.Query` to group all tokens with their number of holders. Builds an `Ecto.Query` to group all tokens with their number of holders.
""" """
@ -144,16 +105,6 @@ defmodule Explorer.Chain.Address.TokenBalance do
) )
end 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 """ @doc """
Builds an `Ecto.Query` to fetch the unfetched token balances. Builds an `Ecto.Query` to fetch the unfetched token balances.

@ -19,6 +19,7 @@ defmodule Explorer.Chain.Import do
Import.Logs, Import.Logs,
Import.Tokens, Import.Tokens,
Import.TokenTransfers, Import.TokenTransfers,
Import.Address.CurrentTokenBalances,
Import.Address.TokenBalances 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.{ alias Explorer.Chain.{
Address, Address,
Address.TokenBalance, Address.TokenBalance,
Address.CurrentTokenBalance,
Block, Block,
Data, Data,
Log, Log,
@ -395,6 +396,55 @@ defmodule Explorer.Chain.ImportTest do
assert 3 == count assert 3 == count
end 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 test "with empty map" do
assert {:ok, %{}} == Import.all(%{}) assert {:ok, %{}} == Import.all(%{})
end end

@ -1182,7 +1182,10 @@ defmodule Explorer.ChainTest do
|> Enum.map(&insert(:address, fetched_coin_balance: &1)) |> Enum.map(&insert(:address, fetched_coin_balance: &1))
|> Enum.map(& &1.hash) |> 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 end
test "with top addresses in order with matching value" do 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)) |> insert(fetched_coin_balance: 4, hash: Enum.fetch!(test_hashes, 4))
|> Map.fetch!(:hash) |> 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
end end
@ -2956,173 +2962,32 @@ defmodule Explorer.ChainTest do
end end
describe "fetch_token_holders_from_token_hash/2" do 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) %Token{contract_address_hash: contract_address_hash} = insert(:token)
address = insert(:address) address_a = insert(:address)
address_b = insert(:address)
insert( insert(
:token_balance, :address_current_token_balance,
address: address, address: address_a,
block_number: 1000,
token_contract_address_hash: contract_address_hash, token_contract_address_hash: contract_address_hash,
value: 5000 value: 5000
) )
insert( insert(
:token_balance, :address_current_token_balance,
block_number: 1001, address: address_b,
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,
block_number: 1001, block_number: 1001,
token_contract_address_hash: contract_address_hash, token_contract_address_hash: contract_address_hash,
value: 1000
)
insert(
:token_balance,
block_number: 1002,
token_contract_address_hash: contract_address_hash,
value: 4000 value: 4000
) )
insert( token_holders_count =
:token_balance,
block_number: 1002,
token_contract_address_hash: contract_address_hash,
value: 3000
)
values =
contract_address_hash contract_address_hash
|> Chain.fetch_token_holders_from_token_hash([]) |> Chain.fetch_token_holders_from_token_hash([])
|> Enum.map(&Decimal.to_integer(&1.value)) |> Enum.count()
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
)
assert Chain.fetch_token_holders_from_token_hash(contract_address_hash, []) == [] assert token_holders_count == 2
end end
end end

@ -13,6 +13,7 @@ defmodule Explorer.Factory do
alias Explorer.Chain.{ alias Explorer.Chain.{
Address, Address,
Address.CurrentTokenBalance,
Address.TokenBalance, Address.TokenBalance,
Address.CoinBalance, Address.CoinBalance,
Block, Block,
@ -480,6 +481,16 @@ defmodule Explorer.Factory do
} }
end 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 defmacrop left + right do
quote do quote do
fragment("? + ?", unquote(left), unquote(right)) fragment("? + ?", unquote(left), unquote(right))

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

@ -9,6 +9,7 @@ defmodule Indexer.Block.Fetcher do
alias Indexer.{AddressExtraction, CoinBalance, MintTransfer, Token, TokenTransfers} alias Indexer.{AddressExtraction, CoinBalance, MintTransfer, Token, TokenTransfers}
alias Indexer.Address.{CoinBalances, TokenBalances} alias Indexer.Address.{CoinBalances, TokenBalances}
alias Indexer.Block.Fetcher.Receipts alias Indexer.Block.Fetcher.Receipts
alias Indexer.Block.Transform
@type address_hash_to_fetched_balance_block_number :: %{String.t() => Block.block_number()} @type address_hash_to_fetched_balance_block_number :: %{String.t() => Block.block_number()}
@type transaction_hash_to_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, transactions: transactions_without_receipts,
block_second_degree_relations: block_second_degree_relations block_second_degree_relations: block_second_degree_relations
} = result, } = result,
blocks = Transform.transform_blocks(blocks),
{:receipts, {:ok, receipt_params}} <- {:receipts, Receipts.fetch(state, transactions_without_receipts)}, {:receipts, {:ok, receipt_params}} <- {:receipts, Receipts.fetch(state, transactions_without_receipts)},
%{logs: logs, receipts: receipts} = receipt_params, %{logs: logs, receipts: receipts} = receipt_params,
transactions_with_receipts = Receipts.put(transactions_without_receipts, receipts), 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([:addresses, :params], balances_addresses_params)
|> put_in([:blocks, :params, Access.all(), :consensus], true) |> put_in([:blocks, :params, Access.all(), :consensus], true)
|> put_in([Access.key(:address_coin_balances, %{}), :params], balances_params) |> 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(:address_token_balances), :params], address_token_balances)
|> put_in([Access.key(:internal_transactions, %{}), :params], internal_transactions_params), |> put_in([Access.key(:internal_transactions, %{}), :params], internal_transactions_params),
{:ok, imported} = ok <- Chain.import(chain_import_options) do {: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 end
def import_token_balances(token_balances_params) do 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, _} ->
:ok :ok

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

@ -46,10 +46,14 @@ defmodule Indexer.MixProject do
[ [
# JSONRPC access to Parity for `Explorer.Indexer` # JSONRPC access to Parity for `Explorer.Indexer`
{:ethereum_jsonrpc, in_umbrella: true}, {:ethereum_jsonrpc, in_umbrella: true},
# RLP encoding
{:ex_rlp, "~> 0.3"},
# Code coverage # Code coverage
{:excoveralls, "~> 0.10.0", only: [:test], github: "KronicDeth/excoveralls", branch: "circle-workflows"}, {:excoveralls, "~> 0.10.0", only: [:test], github: "KronicDeth/excoveralls", branch: "circle-workflows"},
# Importing to database # Importing to database
{:explorer, in_umbrella: true}, {:explorer, in_umbrella: true},
# libsecp2561k1 crypto functions
{:libsecp256k1, "~> 0.1.10"},
# Log errors and application output to separate files # Log errors and application output to separate files
{:logger_file_backend, "~> 0.0.10"}, {:logger_file_backend, "~> 0.0.10"},
# Mocking `EthereumJSONRPC.Transport`, so we avoid hitting real chains for local testing # 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", jsonrpc: "2.0",
result: %{ result: %{
"difficulty" => "0x0", "difficulty" => "0x0",
"extraData" => "0x0",
"gasLimit" => "0x0", "gasLimit" => "0x0",
"gasUsed" => "0x0", "gasUsed" => "0x0",
"hash" => "hash" =>
Explorer.Factory.block_hash() Explorer.Factory.block_hash()
|> to_string(), |> to_string(),
"logsBloom" => "0x0",
"miner" => "0xb2930b35844a230f00e51431acae96fe543a0347", "miner" => "0xb2930b35844a230f00e51431acae96fe543a0347",
"number" => "0x0", "number" => "0x0",
"parentHash" => "parentHash" =>
Explorer.Factory.block_hash() Explorer.Factory.block_hash()
|> to_string(), |> to_string(),
"receiptsRoot" => "0x0",
"sha3Uncles" => "0x0",
"size" => "0x0", "size" => "0x0",
"stateRoot" => "0x0",
"timestamp" => "0x0", "timestamp" => "0x0",
"totalDifficulty" => "0x0", "totalDifficulty" => "0x0",
"transactions" => [], "transactions" => [],
"transactionsRoot" => "0x0",
"uncles" => [] "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, "number" => number_quantity,
"parentHash" => "0x006edcaa1e6fde822908783bc4ef1ad3675532d542fce53537557391cfe34c3c", "parentHash" => "0x006edcaa1e6fde822908783bc4ef1ad3675532d542fce53537557391cfe34c3c",
"size" => "0x243", "size" => "0x243",
"receiptsRoot" => "0x0",
"sha3Uncles" => "0x0",
"stateRoot" => "0x0",
"timestamp" => "0x5b437f41", "timestamp" => "0x5b437f41",
"totalDifficulty" => "0x342337ffffffffffffffffffffffffed8d29bb", "totalDifficulty" => "0x342337ffffffffffffffffffffffffed8d29bb",
"transactions" => [ "transactions" => [
@ -93,6 +96,7 @@ defmodule Indexer.Block.Uncle.FetcherTest do
"value" => "0x0" "value" => "0x0"
} }
], ],
"transactionsRoot" => "0x0",
"uncles" => [uncle_uncle_hash_data] "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) } = List.first(result)
end end
test "does not ignore calls that were returned with error" do test "ignores calls that gave errors to try fetch they again later" do
address = insert(:address) address = insert(:address, hash: "0x7113ffcb9c18a97da1b9cfc43e6cb44ed9165509")
token = insert(:token, contract_address: build(:contract_address)) token = insert(:token, contract_address: build(:contract_address))
address_hash_string = Hash.to_string(address.hash)
data = %{ token_balances = [
token_contract_address_hash: token.contract_address_hash, %{
address_hash: address_hash_string, address_hash: to_string(address.hash),
block_number: 1_000 block_number: 1_000,
} token_contract_address_hash: to_string(token.contract_address_hash)
}
]
get_balance_from_blockchain_with_error() get_balance_from_blockchain_with_error()
{:ok, result} = TokenBalances.fetch_token_balances_from_blockchain([data]) assert TokenBalances.fetch_token_balances_from_blockchain(token_balances) == {:ok, []}
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)
end end
test "ignores results that raised :timeout" do 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_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_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_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], []}, "exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], []},
"excoveralls": {:git, "https://github.com/KronicDeth/excoveralls.git", "0a859b68851eeba9b43eba59fbc8f9098299cfe1", [branch: "circle-workflows"]}, "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]}]}, "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], []}, "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], []},
"junit_formatter": {:hex, :junit_formatter, "2.2.0", "da6093f0740c58a824f9585ebb7cb1b960efaecf48d1fa969e95d9c47c6b19dd", [:mix], [], "hexpm"}, "junit_formatter": {:hex, :junit_formatter, "2.2.0", "da6093f0740c58a824f9585ebb7cb1b960efaecf48d1fa969e95d9c47c6b19dd", [:mix], [], "hexpm"},
"keccakf1600": {:hex, :keccakf1600_orig, "2.0.0", "0a7217ddb3ee8220d449bbf7575ec39d4e967099f220a91e3dfca4dbaef91963", [:rebar3], []}, "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"}, "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": {: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"}, "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], []}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []},
"mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"}, "mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"},
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, "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"}, "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"}, "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"}, "mox": {:hex, :mox, "0.4.0", "7f120840f7d626184a3d65de36189ca6f37d432e5d63acd80045198e4c5f7e6e", [:mix], [], "hexpm"},

Loading…
Cancel
Save