Merge branch 'master' of https://github.com/blockscout/blockscout into va-tx-actions

pull/6582/head
POA 2 years ago
commit fd4a4372ae
  1. 2
      .github/workflows/config.yml
  2. 2
      .github/workflows/publish-docker-image-every-push.yml
  3. 2
      .github/workflows/publish-docker-image-release.yml
  4. 4
      .tool-versions
  5. 87
      CHANGELOG.md
  6. 3
      apps/block_scout_web/.sobelow-conf
  7. 4
      apps/block_scout_web/assets/css/components/_errors.scss
  8. 1
      apps/block_scout_web/assets/js/app.js
  9. 1
      apps/block_scout_web/assets/js/balance-chart-loader.js
  10. 1
      apps/block_scout_web/assets/js/chart-loader.js
  11. 2
      apps/block_scout_web/assets/js/lib/ad.js
  12. 19
      apps/block_scout_web/assets/js/lib/add_chain_to_mm.js
  13. 6
      apps/block_scout_web/assets/js/lib/analytics.js
  14. 10
      apps/block_scout_web/assets/js/lib/async_listing_load.js
  15. 41
      apps/block_scout_web/assets/js/lib/autocomplete.js
  16. 2
      apps/block_scout_web/assets/js/lib/banner.js
  17. 4
      apps/block_scout_web/assets/js/lib/clipboard_buttons.js
  18. 12
      apps/block_scout_web/assets/js/lib/coin_balance_history_chart.js
  19. 2
      apps/block_scout_web/assets/js/lib/csv_download.js
  20. 1
      apps/block_scout_web/assets/js/lib/currency.js
  21. 11
      apps/block_scout_web/assets/js/lib/history_chart.js
  22. 2
      apps/block_scout_web/assets/js/lib/indexing.js
  23. 6
      apps/block_scout_web/assets/js/lib/infinite_scroll_helpers.js
  24. 3
      apps/block_scout_web/assets/js/lib/list_morph.js
  25. 16
      apps/block_scout_web/assets/js/lib/modals.js
  26. 6
      apps/block_scout_web/assets/js/lib/public_tags_request_form.js
  27. 4
      apps/block_scout_web/assets/js/lib/random_access_pagination.js
  28. 1
      apps/block_scout_web/assets/js/lib/redux_helpers.js
  29. 33
      apps/block_scout_web/assets/js/lib/smart_contract/common_helpers.js
  30. 2
      apps/block_scout_web/assets/js/lib/smart_contract/connect.js
  31. 26
      apps/block_scout_web/assets/js/lib/smart_contract/functions.js
  32. 2
      apps/block_scout_web/assets/js/lib/smart_contract/wei_ether_converter.js
  33. 1
      apps/block_scout_web/assets/js/lib/try_api.js
  34. 1
      apps/block_scout_web/assets/js/lib/try_eth_api.js
  35. 4
      apps/block_scout_web/assets/js/pages/account/delete_item_handler.js
  36. 6
      apps/block_scout_web/assets/js/pages/address.js
  37. 2
      apps/block_scout_web/assets/js/pages/address/coin_balances.js
  38. 2
      apps/block_scout_web/assets/js/pages/address/internal_transactions.js
  39. 1
      apps/block_scout_web/assets/js/pages/address/logs.js
  40. 3
      apps/block_scout_web/assets/js/pages/address/token_transfers.js
  41. 4
      apps/block_scout_web/assets/js/pages/address/transactions.js
  42. 2
      apps/block_scout_web/assets/js/pages/address/validations.js
  43. 11
      apps/block_scout_web/assets/js/pages/blocks.js
  44. 10
      apps/block_scout_web/assets/js/pages/chain.js
  45. 2
      apps/block_scout_web/assets/js/pages/dark-mode-switcher.js
  46. 6
      apps/block_scout_web/assets/js/pages/layout.js
  47. 4
      apps/block_scout_web/assets/js/pages/pending_transactions.js
  48. 4
      apps/block_scout_web/assets/js/pages/search-results/search.js
  49. 40
      apps/block_scout_web/assets/js/pages/sol2uml.js
  50. 3
      apps/block_scout_web/assets/js/pages/token/search.js
  51. 3
      apps/block_scout_web/assets/js/pages/token/token_transfers.js
  52. 1
      apps/block_scout_web/assets/js/pages/token_counters.js
  53. 49
      apps/block_scout_web/assets/js/pages/transaction.js
  54. 2
      apps/block_scout_web/assets/js/pages/transactions.js
  55. 34
      apps/block_scout_web/assets/js/pages/verification_form.js
  56. 5
      apps/block_scout_web/assets/js/pages/verified_contracts.js
  57. 2
      apps/block_scout_web/assets/js/socket.js
  58. 1
      apps/block_scout_web/assets/js/view_specific/address_contract/code_highlighting.js
  59. 1508
      apps/block_scout_web/assets/package-lock.json
  60. 24
      apps/block_scout_web/assets/package.json
  61. 11
      apps/block_scout_web/lib/block_scout_web/api_router.ex
  62. 23
      apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex
  63. 122
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex
  64. 11
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/search_controller.ex
  65. 174
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex
  66. 33
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex
  67. 6
      apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex
  68. 1
      apps/block_scout_web/lib/block_scout_web/paging_helper.ex
  69. 31
      apps/block_scout_web/lib/block_scout_web/smart_contracts_api_v2_router.ex
  70. 3
      apps/block_scout_web/lib/block_scout_web/views/abi_encoded_value_view.ex
  71. 4
      apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex
  72. 41
      apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex
  73. 1
      apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex
  74. 21
      apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex
  75. 231
      apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex
  76. 4
      apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex
  77. 30
      apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex
  78. 8
      apps/block_scout_web/lib/block_scout_web/views/currency_helpers.ex
  79. 4
      apps/block_scout_web/mix.exs
  80. 330
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs
  81. 100
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs
  82. 1573
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs
  83. 49
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs
  84. 3
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs
  85. 10
      apps/block_scout_web/test/block_scout_web/features/viewing_app_test.exs
  86. 2
      apps/block_scout_web/test/block_scout_web/views/transaction_view_test.exs
  87. 2
      apps/ethereum_jsonrpc/mix.exs
  88. 106
      apps/explorer/lib/explorer/chain.ex
  89. 21
      apps/explorer/lib/explorer/chain/address/current_token_balance.ex
  90. 61
      apps/explorer/lib/explorer/chain/cache/min_missing_block.ex
  91. 53
      apps/explorer/lib/explorer/chain/import/runner/blocks.ex
  92. 52
      apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex
  93. 5
      apps/explorer/lib/explorer/chain/import/runner/transactions.ex
  94. 4
      apps/explorer/lib/explorer/chain/pending_block_operation.ex
  95. 7
      apps/explorer/lib/explorer/chain/token_transfer.ex
  96. 13
      apps/explorer/lib/explorer/chain/transaction.ex
  97. 62
      apps/explorer/lib/explorer/smart_contract/reader.ex
  98. 78
      apps/explorer/lib/explorer/utility/missing_block_range.ex
  99. 6
      apps/explorer/mix.exs
  100. 13
      apps/explorer/priv/repo/migrations/20221219151744_create_missing_block_ranges.exs
  101. Some files were not shown because too many files have changed in this diff Show More

@ -156,7 +156,7 @@ jobs:
id: dialyzer-cache
with:
path: priv/plts
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-dialyzer-mixlockhash_16-${{ hashFiles('mix.lock') }}
key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-dialyzer-mixlockhash_18-${{ hashFiles('mix.lock') }}
restore-keys: |
${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-dialyzer-"

@ -7,7 +7,7 @@ on:
env:
OTP_VERSION: '24.3.4.1'
ELIXIR_VERSION: '1.13.4'
NEXT_RELEASE_VERSION: 5.0.0
NEXT_RELEASE_VERSION: 5.0.1
jobs:
push_to_registry:

@ -18,7 +18,7 @@ jobs:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
env:
RELEASE_VERSION: 4.1.8
RELEASE_VERSION: 5.0.0
steps:
- name: Check out the repo
uses: actions/checkout@v3

@ -1,3 +1,3 @@
elixir 1.14.2-otp-25
erlang 25.1.1
elixir 1.14.3-otp-25
erlang 25.2.1
nodejs 16.16.0

@ -1,26 +1,47 @@
# ChangeLog
## Current
### Features
- [#6712](https://github.com/blockscout/blockscout/pull/6712) - API v2 update
- [#6582](https://github.com/blockscout/blockscout/pull/6582) - Transaction actions indexer
- [#6542](https://github.com/blockscout/blockscout/pull/6542) - Init mixpanel and amplitude analytics
### Fixes
- [#6736](https://github.com/blockscout/blockscout/pull/6736) - Fix `/tokens` in old UI
- [#6705](https://github.com/blockscout/blockscout/pull/6705) - Fix `/smart-contracts` bugs in API v2
### Chore
- [#6695](https://github.com/blockscout/blockscout/pull/6695) - Process errors and warnings with enables check-js feature in VS code
<details>
<summary>Dependencies version bumps</summary>
</details>
## 5.0.0-beta
### Features
- [#6092](https://github.com/blockscout/blockscout/pull/6092) - Blockscout Account functionality
- [#6324](https://github.com/blockscout/blockscout/pull/6324) - Add verified contracts list page
- [#6316](https://github.com/blockscout/blockscout/pull/6316) - Public tags functionality
- [#6444](https://github.com/blockscout/blockscout/pull/6444) - Add support for yul verification via rust microservice
- [#6073](https://github.com/blockscout/blockscout/pull/6073) - Add vyper support for rust verifier microservice integration
- [#6401](https://github.com/blockscout/blockscout/pull/6401) - Add Sol2Uml contract visualization
- [#6583](https://github.com/blockscout/blockscout/pull/6583), [#6687](https://github.com/blockscout/blockscout/pull/6687) - Missing ranges collector
- [#6574](https://github.com/blockscout/blockscout/pull/6574), [#6601](https://github.com/blockscout/blockscout/pull/6601) - Allow and manage insecure HTTP connection to the archive node
- [#6433](https://github.com/blockscout/blockscout/pull/6433) - Update error pagess
- [#6433](https://github.com/blockscout/blockscout/pull/6433), [#6698](https://github.com/blockscout/blockscout/pull/6698) - Update error pagess
- [#6544](https://github.com/blockscout/blockscout/pull/6544) - API improvements
- [#5561](https://github.com/blockscout/blockscout/pull/5561), [#6523](https://github.com/blockscout/blockscout/pull/6523), [#6549](https://github.com/blockscout/blockscout/pull/6549) - Improve working with contracts implementations
- [#6401](https://github.com/blockscout/blockscout/pull/6401) - Add Sol2Uml contract visualization
- [#6481](https://github.com/blockscout/blockscout/pull/6481) - Smart contract verification improvements
- [#6444](https://github.com/blockscout/blockscout/pull/6444) - Add support for yul verification via rust microservice
- [#6440](https://github.com/blockscout/blockscout/pull/6440) - Add support for base64 encoded NFT metadata
- [#6407](https://github.com/blockscout/blockscout/pull/6407) - Indexed ratio for int txs fetching stage
- [#6324](https://github.com/blockscout/blockscout/pull/6324) - Add verified contracts list page
- [#6379](https://github.com/blockscout/blockscout/pull/6379), [#6429](https://github.com/blockscout/blockscout/pull/6429) - API v2 for frontend
- [#6379](https://github.com/blockscout/blockscout/pull/6379), [#6429](https://github.com/blockscout/blockscout/pull/6429), [#6642](https://github.com/blockscout/blockscout/pull/6642), [#6677](https://github.com/blockscout/blockscout/pull/6677) - API v2 for frontend
- [#6351](https://github.com/blockscout/blockscout/pull/6351) - Enable forum link env var
- [#6316](https://github.com/blockscout/blockscout/pull/6316) - Copy public tags functionality to master
- [#6196](https://github.com/blockscout/blockscout/pull/6196) - INDEXER_CATCHUP_BLOCKS_BATCH_SIZE and INDEXER_CATCHUP_BLOCKS_CONCURRENCY env varaibles
- [#6196](https://github.com/blockscout/blockscout/pull/6196) - INDEXER_CATCHUP_BLOCKS_BATCH_SIZE and INDEXER_CATCHUP_BLOCKS_CONCURRENCY env variables
- [#6187](https://github.com/blockscout/blockscout/pull/6187) - Filter by created time of verified contracts in listcontracts API endpoint
- [#6092](https://github.com/blockscout/blockscout/pull/6092) - Blockscout Account functionality
- [#6073](https://github.com/blockscout/blockscout/pull/6073) - Add vyper support for rust verifier microservice integration
- [#6111](https://github.com/blockscout/blockscout/pull/6111) - Add Prometheus metrics to indexer
- [#6168](https://github.com/blockscout/blockscout/pull/6168) - Token instance fetcher checks instance owner and updates current token balance
- [#6209](https://github.com/blockscout/blockscout/pull/6209) - Add metrics for block import stages, runners, steps
@ -33,9 +54,13 @@
- [#6510](https://github.com/blockscout/blockscout/pull/6510) - Set consensus: false for blocks on int transaction foreign_key_violation
- [#6565](https://github.com/blockscout/blockscout/pull/6565) - Set restart: :permanent for permanent fetchers
- [#6568](https://github.com/blockscout/blockscout/pull/6568) - Drop unfetched_token_balances index
- [#6647](https://github.com/blockscout/blockscout/pull/6647) - Pending block operations update
- [#6542](https://github.com/blockscout/blockscout/pull/6542) - Init mixpanel and amplitude analytics
- [#6713](https://github.com/blockscout/blockscout/pull/6713) - Remove internal transactions deletion
### Fixes
- [#6676](https://github.com/blockscout/blockscout/pull/6676) - Fix `/smart-contracts` bugs in API v2
- [#6603](https://github.com/blockscout/blockscout/pull/6603) - Add to MM button explorer URL fix
- [#6512](https://github.com/blockscout/blockscout/pull/6512) - Allow gasUsed in failed internal txs; Leave error field for staticcall
- [#6532](https://github.com/blockscout/blockscout/pull/6532) - Fix index creation migration
@ -51,7 +76,7 @@
- [#6243](https://github.com/blockscout/blockscout/pull/6243) - Fix freezes on `/blocks` page
- [#6162](https://github.com/blockscout/blockscout/pull/6162) - Extend token symbol type varchar(255) -> text
- [#6158](https://github.com/blockscout/blockscout/pull/6158) - Add missing clause for merge_twin_vyper_contract_with_changeset function
- [#6090](https://github.com/blockscout/blockscout/pull/6090) - Fix metadata fetching for ERC-1155 tokens instances
- [#6090](https://github.com/blockscout/blockscout/pull/6090) - Fix metadata fetching for ERC-1155 tokens instances
- [#6091](https://github.com/blockscout/blockscout/pull/6091) - Improve fetching media type for NFT
- [#6094](https://github.com/blockscout/blockscout/pull/6094) - Fix inconsistent behaviour of `getsourcecode` method
- [#6105](https://github.com/blockscout/blockscout/pull/6105) - Fix some token transfers broadcasting
@ -65,6 +90,8 @@
- [#6492](https://github.com/blockscout/blockscout/pull/6492) - Remove token instance owner fetching
- [#6536](https://github.com/blockscout/blockscout/pull/6536) - Fix internal transactions query
- [#6550](https://github.com/blockscout/blockscout/pull/6550) - Query token transfers before updating
- [#6599](https://github.com/blockscout/blockscout/pull/6599) - unhandled division by zero
- [#6590](https://github.com/blockscout/blockscout/pull/6590) - ignore some receipt fields for metis
### Chore
@ -87,9 +114,41 @@
- [#6125](https://github.com/blockscout/blockscout/pull/6125) - Rename obsolete "parity" EthereumJSONRPC.Variant to "nethermind"
- [#6124](https://github.com/blockscout/blockscout/pull/6124) - Docker compose: add config for Erigon
- [#6061](https://github.com/blockscout/blockscout/pull/6061) - Discord badge and updated permalink
<details>
<summary>Dependencies version bumps</summary>
- [#6585](https://github.com/blockscout/blockscout/pull/6585) - Bump jquery from 3.6.1 to 3.6.2 in /apps/block_scout_web/assets
- [#6610](https://github.com/blockscout/blockscout/pull/6610) - Bump tesla from 1.4.4 to 1.5.0
- [#6611](https://github.com/blockscout/blockscout/pull/6611) - Bump chart.js from 4.0.1 to 4.1.0 in /apps/block_scout_web/assets
- [#6618](https://github.com/blockscout/blockscout/pull/6618) - Bump chart.js from 4.1.0 to 4.1.1 in /apps/block_scout_web/assets
- [#6619](https://github.com/blockscout/blockscout/pull/6619) - Bump eslint from 8.29.0 to 8.30.0 in /apps/block_scout_web/assets
- [#6620](https://github.com/blockscout/blockscout/pull/6620) - Bump sass from 1.56.2 to 1.57.0 in /apps/block_scout_web/assets
- [#6626](https://github.com/blockscout/blockscout/pull/6626) - Bump @amplitude/analytics-browser from 1.6.1 to 1.6.6 in /apps/block_scout_web/assets
- [#6627](https://github.com/blockscout/blockscout/pull/6627) - Bump sass from 1.57.0 to 1.57.1 in /apps/block_scout_web/assets
- [#6628](https://github.com/blockscout/blockscout/pull/6628) - Bump sweetalert2 from 11.6.15 to 11.6.16 in /apps/block_scout_web/assets
- [#6631](https://github.com/blockscout/blockscout/pull/6631) - Bump jquery from 3.6.2 to 3.6.3 in /apps/block_scout_web/assets
- [#6633](https://github.com/blockscout/blockscout/pull/6633) - Bump ecto_sql from 3.9.1 to 3.9.2
- [#6636](https://github.com/blockscout/blockscout/pull/6636) - Bump ecto from 3.9.3 to 3.9.4
- [#6639](https://github.com/blockscout/blockscout/pull/6639) - Bump @amplitude/analytics-browser from 1.6.6 to 1.6.7 in /apps/block_scout_web/assets
- [#6640](https://github.com/blockscout/blockscout/pull/6640) - Bump @babel/core from 7.20.5 to 7.20.7 in /apps/block_scout_web/assets
- [#6653](https://github.com/blockscout/blockscout/pull/6653) - Bump luxon from 3.1.1 to 3.2.0 in /apps/block_scout_web/assets
- [#6654](https://github.com/blockscout/blockscout/pull/6654) - Bump flow from 1.2.0 to 1.2.1
- [#6669](https://github.com/blockscout/blockscout/pull/6669) - Bump @babel/core from 7.20.7 to 7.20.12 in /apps/block_scout_web/assets
- [#6663](https://github.com/blockscout/blockscout/pull/6663) - Bump eslint from 8.30.0 to 8.31.0 in /apps/block_scout_web/assets
- [#6662](https://github.com/blockscout/blockscout/pull/6662) - Bump viewerjs from 1.11.1 to 1.11.2 in /apps/block_scout_web/assets
- [#6668](https://github.com/blockscout/blockscout/pull/6668) - Bump babel-loader from 9.1.0 to 9.1.2 in /apps/block_scout_web/assets
- [#6670](https://github.com/blockscout/blockscout/pull/6670) - Bump json5 from 1.0.1 to 1.0.2 in /apps/block_scout_web/assets
- [#6673](https://github.com/blockscout/blockscout/pull/6673) - Bump chart.js from 4.1.1 to 4.1.2 in /apps/block_scout_web/assets
- [#6674](https://github.com/blockscout/blockscout/pull/6674) - Bump luxon from 3.2.0 to 3.2.1 in /apps/block_scout_web/assets
- [#6675](https://github.com/blockscout/blockscout/pull/6675) - Bump web3modal from 1.9.10 to 1.9.11 in /apps/block_scout_web/assets
- [#6679](https://github.com/blockscout/blockscout/pull/6679) - Bump gettext from 0.20.0 to 0.21.0
- [#6680](https://github.com/blockscout/blockscout/pull/6680) - Bump flow from 1.2.1 to 1.2.2
- [#6689](https://github.com/blockscout/blockscout/pull/6689) - Bump postcss from 8.4.20 to 8.4.21 in /apps/block_scout_web/assets
- [#6690](https://github.com/blockscout/blockscout/pull/6690) - Bump bamboo from 2.2.0 to 2.3.0
- [#6691](https://github.com/blockscout/blockscout/pull/6691) - Bump flow from 1.2.2 to 1.2.3
- [#6696](https://github.com/blockscout/blockscout/pull/6696) - Bump briefly from 1dd66ee to 13a9790
- [#6697](https://github.com/blockscout/blockscout/pull/6697) - Bump mime from 1.6.0 to 2.0.3
- [#6053](https://github.com/blockscout/blockscout/pull/6053) - Bump jest-environment-jsdom from 29.0.1 to 29.0.2 in /apps/block_scout_web/assets
- [#6055](https://github.com/blockscout/blockscout/pull/6055) - Bump @babel/core from 7.18.13 to 7.19.0 in /apps/block_scout_web/assets
- [#6054](https://github.com/blockscout/blockscout/pull/6054) - Bump jest from 29.0.1 to 29.0.2 in /apps/block_scout_web/assets
@ -226,6 +285,7 @@
- [#6562](https://github.com/blockscout/blockscout/pull/6562) - Bump qs from 6.5.2 to 6.5.3 in /apps/block_scout_web/assets
- [#6577](https://github.com/blockscout/blockscout/pull/6577) - Bump postcss from 8.4.19 to 8.4.20 in /apps/block_scout_web/assets
- [#6578](https://github.com/blockscout/blockscout/pull/6578) - Bump sass from 1.56.1 to 1.56.2 in /apps/block_scout_web/assets
</details>
## 4.1.8-beta
@ -309,7 +369,7 @@
- [#5867](https://github.com/blockscout/blockscout/pull/5867) - Bump @babel/preset-env from 7.16.11 to 7.18.10 in /apps/block_scout_web/assets
- [#5876](https://github.com/blockscout/blockscout/pull/5876) - Bump bignumber.js from 9.0.2 to 9.1.0 in /apps/block_scout_web/assets
- [#5871](https://github.com/blockscout/blockscout/pull/5871) - Bump redux from 4.1.2 to 4.2.0 in /apps/block_scout_web/assets
- [#5868](https://github.com/blockscout/blockscout/pull/5868) - Bump ex_rlp from 0.5.3 to 0.5.4
- [#5868](https://github.com/blockscout/blockscout/pull/5868) - Bump ex_rlp from 0.5.3 to 0.5.4
- [#5874](https://github.com/blockscout/blockscout/pull/5874) - Bump core-js from 3.20.3 to 3.24.1 in /apps/block_scout_web/assets
- [#5882](https://github.com/blockscout/blockscout/pull/5882) - Bump math from 0.3.1 to 0.7.0
- [#5878](https://github.com/blockscout/blockscout/pull/5878) - Bump css-minimizer-webpack-plugin from 3.4.1 to 4.0.0 in /apps/block_scout_web/assets
@ -402,6 +462,7 @@
- [#5778](https://github.com/blockscout/blockscout/pull/5778) - Allow hyphen in database name
### Chore
- [#5787](https://github.com/blockscout/blockscout/pull/5787) - Add job for merging master to specific branch after release
- [#5788](https://github.com/blockscout/blockscout/pull/5788) - Update Docker image on every push to master branch
- [#5736](https://github.com/blockscout/blockscout/pull/5736) - Remove obsolete network selector
@ -2144,4 +2205,4 @@ Reverting of synchronous block counter, implemented in #1848
- [https://github.com/blockscout/blockscout/pull/1532](https://github.com/blockscout/blockscout/pull/1532) - Upgrade elixir to 1.8.1
- [https://github.com/blockscout/blockscout/pull/1553](https://github.com/blockscout/blockscout/pull/1553) - Dockerfile: remove 1.7.1 version pin FROM bitwalker/alpine-elixir-phoenix
- [https://github.com/blockscout/blockscout/pull/1465](https://github.com/blockscout/blockscout/pull/1465) - Resolve lodash security alert
- [https://github.com/blockscout/blockscout/pull/1465](https://github.com/blockscout/blockscout/pull/1465) - Resolve lodash security alert

@ -7,6 +7,7 @@
format: "compact",
ignore: ["Config.Headers", "Config.CSWH", "XSS.SendResp", "XSS.Raw"],
ignore_files: [
"apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex"
"apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex",
"apps/block_scout_web/lib/block_scout_web/smart_contracts_api_v2_router.ex"
]
]

@ -19,7 +19,7 @@ main:has(section.error-container) {
.block-not-found {
display: flex;
flex-direction: column;
padding-bottom: 50px;
padding: 6rem 0 9rem 0;
@media (min-width: $error-tablet-breakpoint) {
flex-direction: row;
align-items: center;
@ -95,7 +95,7 @@ main:has(section.error-container) {
justify-content: center;
flex-direction: column;
margin: auto;
padding-bottom: 3rem;
padding: 6rem 0 9rem 0;
max-width: 827px;
gap: 12px;

@ -37,4 +37,5 @@ import './lib/card_tabs'
import './lib/ad'
import swal from 'sweetalert2'
// @ts-ignore
window.Swal = swal

@ -6,6 +6,7 @@ import { createCoinBalanceHistoryChart } from './lib/coin_balance_history_chart'
(function () {
const coinBalanceHistoryChartElement = $('[data-chart="coinBalanceHistoryChart"]')[0]
if (coinBalanceHistoryChartElement) {
// @ts-ignore
window.coinBalanceHistoryChart = createCoinBalanceHistoryChart(coinBalanceHistoryChartElement)
}
formatAllUsdValues()

@ -4,6 +4,7 @@ import { createMarketHistoryChart } from './lib/history_chart'
(function () {
const dashboardChartElement = document.querySelectorAll('[data-chart="historyChart"]')[0]
if (dashboardChartElement) {
// @ts-ignore
window.dashboardChart = createMarketHistoryChart(dashboardChartElement)
}
formatAllUsdValues()

@ -1,5 +1,5 @@
import $ from 'jquery'
import customAds from './custom_ad'
import customAds from './custom_ad.json'
function countImpressions (impressionUrl) {
if (impressionUrl) {

@ -2,16 +2,25 @@ import 'bootstrap'
export async function addChainToMM ({ btn }) {
try {
// @ts-ignore
const chainIDFromWallet = await window.ethereum.request({ method: 'eth_chainId' })
const chainIDFromInstance = getChainIdHex()
const coinName = document.getElementById('js-coin-name').value
const subNetwork = document.getElementById('js-subnetwork').value
const jsonRPC = document.getElementById('js-json-rpc').value
const coinNameObj = document.getElementById('js-coin-name')
// @ts-ignore
const coinName = coinNameObj && coinNameObj.value
const subNetworkObj = document.getElementById('js-subnetwork')
// @ts-ignore
const subNetwork = subNetworkObj && subNetworkObj.value
const jsonRPCObj = document.getElementById('js-json-rpc')
// @ts-ignore
const jsonRPC = jsonRPCObj && jsonRPCObj.value
// @ts-ignore
const path = process.env.NETWORK_PATH || '/'
const blockscoutURL = location.protocol + '//' + location.host + path
if (chainIDFromWallet !== chainIDFromInstance) {
// @ts-ignore
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [{
@ -44,7 +53,9 @@ export async function addChainToMM ({ btn }) {
}
function getChainIdHex () {
const chainIDFromDOM = document.getElementById('js-chain-id').value
const chainIDObj = document.getElementById('js-chain-id')
// @ts-ignore
const chainIDFromDOM = chainIDObj && chainIDObj.value
const chainIDFromInstance = parseInt(chainIDFromDOM)
return chainIDFromInstance && `0x${chainIDFromInstance.toString(16)}`
}

@ -1,14 +1,18 @@
import mixpanel from 'mixpanel-browser'
import { init as amplitudeInit, track as amplitudeTrack } from '@amplitude/analytics-browser'
// @ts-ignore
const mixpanelToken = process.env.MIXPANEL_TOKEN
// @ts-ignore
const amplitudeApiKey = process.env.AMPLITUDE_API_KEY
let initialized = false
export let mixpanelInitialized = false
export let amplitudeInitialized = false
export function init () {
// @ts-ignore
const mixpanelUrl = process.env.MIXPANEL_URL
// @ts-ignore
const amplitudeUrl = process.env.AMPLITUDE_URL
if (mixpanelToken) {
@ -22,7 +26,7 @@ export function init () {
if (amplitudeApiKey) {
if (amplitudeUrl) {
amplitudeInit(amplitudeApiKey, { serverUrl: amplitudeUrl })
amplitudeInit(amplitudeApiKey, undefined, { serverUrl: amplitudeUrl })
} else {
amplitudeInit(amplitudeApiKey)
}

@ -111,26 +111,28 @@ export function asyncReducer (state = asyncInitialState, action) {
})
}
case 'NAVIGATE_TO_OLDER': {
history.replaceState({}, null, state.nextPagePath)
history.replaceState({}, '', state.nextPagePath)
if (state.pagesStack.length === 0) {
if (window.location.pathname.includes('/search-results')) {
const urlParams = new URLSearchParams(window.location.search)
const queryParam = urlParams.get('q')
// @ts-ignore
state.pagesStack.push(window.location.href.split('?')[0] + `?q=${queryParam}`)
} else {
// @ts-ignore
state.pagesStack.push(window.location.href.split('?')[0])
}
}
if (state.pagesStack[state.pagesStack.length - 1] !== state.nextPagePath) {
if (state.pagesStack[state.pagesStack.length - 1] !== state.nextPagePath && state.nextPagePath) {
state.pagesStack.push(state.nextPagePath)
}
return Object.assign({}, state, { beyondPageOne: true })
}
case 'NAVIGATE_TO_NEWER': {
history.replaceState({}, null, state.prevPagePath)
history.replaceState({}, '', state.prevPagePath)
state.pagesStack.pop()
@ -183,7 +185,7 @@ export const elements = {
if (state.itemKey) {
const container = $el[0]
const newElements = map(state.items, (item) => $(item)[0])
listMorph(container, newElements, { key: state.itemKey })
listMorph(container, newElements, { key: state.itemKey, horizontal: null })
return
}

@ -1,4 +1,5 @@
import $ from 'jquery'
// @ts-ignore
import AutoComplete from '@tarekraafat/autocomplete.js/dist/autoComplete'
import { getTextAdData, fetchTextAdData } from './ad'
import { DateTime } from 'luxon'
@ -13,7 +14,7 @@ const dataSrc = async (query, id) => {
const searchInput = document
.getElementById(id)
searchInput.setAttribute('placeholder', 'Loading...')
searchInput && searchInput.setAttribute('placeholder', 'Loading...')
// Fetch External Data Source
const source = await fetch(
@ -22,7 +23,7 @@ const dataSrc = async (query, id) => {
const data = await source.json()
// Post Loading placeholder text
searchInput.setAttribute('placeholder', placeHolder)
searchInput && searchInput.setAttribute('placeholder', placeHolder)
// Returns Fetched data
return data
} catch (error) {
@ -127,7 +128,8 @@ const config = (id) => {
events: {
input: {
focus: () => {
if (autoCompleteJS.input.value.length) autoCompleteJS.start()
// @ts-ignore
if (autoCompleteJS && autoCompleteJS.input.value.length) autoCompleteJS.start()
}
}
}
@ -141,13 +143,13 @@ const selection = (event) => {
const selectionValue = event.detail.selection.value
if (selectionValue.type === 'contract' || selectionValue.type === 'address' || selectionValue.type === 'label') {
window.location = `/address/${selectionValue.address_hash}`
window.location.href = `/address/${selectionValue.address_hash}`
} else if (selectionValue.type === 'token') {
window.location = `/tokens/${selectionValue.address_hash}`
window.location.href = `/tokens/${selectionValue.address_hash}`
} else if (selectionValue.type === 'transaction') {
window.location = `/tx/${selectionValue.tx_hash}`
window.location.href = `/tx/${selectionValue.tx_hash}`
} else if (selectionValue.type === 'block') {
window.location = `/blocks/${selectionValue.block_hash}`
window.location.href = `/blocks/${selectionValue.block_hash}`
}
}
@ -155,35 +157,40 @@ const openOnFocus = (event, type) => {
const query = event.target.value
if (query) {
if (type === 'desktop') {
autoCompleteJS.start(query)
// @ts-ignore
autoCompleteJS && autoCompleteJS.start(query)
} else if (type === 'mobile') {
autoCompleteJSMobile.start(query)
// @ts-ignore
autoCompleteJSMobile && autoCompleteJSMobile.start(query)
}
} else {
getTextAdData()
.then(({ data: adData, inHouse: _inHouse }) => {
if (adData) {
if (type === 'desktop') {
autoCompleteJS.start('###')
// @ts-ignore
autoCompleteJS && autoCompleteJS.start('###')
} else if (type === 'mobile') {
autoCompleteJSMobile.start('###')
// @ts-ignore
autoCompleteJSMobile && autoCompleteJSMobile.start('###')
}
}
})
}
}
document.querySelector('#main-search-autocomplete') && document.querySelector('#main-search-autocomplete').addEventListener('selection', function (event) {
const mainSearchAutocompleteObj = document.querySelector('#main-search-autocomplete')
const mainSearchAutocompleteMobileObj = document.querySelector('#main-search-autocomplete-mobile')
mainSearchAutocompleteObj && mainSearchAutocompleteObj.addEventListener('selection', function (event) {
selection(event)
})
document.querySelector('#main-search-autocomplete-mobile') && document.querySelector('#main-search-autocomplete-mobile').addEventListener('selection', function (event) {
mainSearchAutocompleteMobileObj && mainSearchAutocompleteMobileObj.addEventListener('selection', function (event) {
selection(event)
})
document.querySelector('#main-search-autocomplete') && document.querySelector('#main-search-autocomplete').addEventListener('focus', function (event) {
mainSearchAutocompleteObj && mainSearchAutocompleteObj.addEventListener('focus', function (event) {
openOnFocus(event, 'desktop')
})
document.querySelector('#main-search-autocomplete-mobile') && document.querySelector('#main-search-autocomplete-mobile').addEventListener('focus', function (event) {
mainSearchAutocompleteMobileObj && mainSearchAutocompleteMobileObj.addEventListener('focus', function (event) {
openOnFocus(event, 'mobile')
})

@ -3,11 +3,13 @@ import $ from 'jquery'
import { showAd } from './ad.js'
if (showAd()) {
// @ts-ignore
window.coinzilla_display = window.coinzilla_display || []
var c_display_preferences = {}
c_display_preferences.zone = '26660bf627543e46851'
c_display_preferences.width = '728'
c_display_preferences.height = '90'
// @ts-ignore
window.coinzilla_display.push(c_display_preferences)
$('.ad-container').show()
} else {

@ -14,7 +14,9 @@ clipboard.on('success', ({ trigger }) => {
.attr('data-original-title', 'Copied!')
.tooltip('show')
copyButton.attr('data-original-title', originalTitle)
if (originalTitle) {
copyButton.attr('data-original-title', originalTitle)
}
setTimeout(() => {
copyButton.tooltip('dispose')

@ -25,7 +25,10 @@ export function createCoinBalanceHistoryChart (el) {
let stepSize = 3
if (data.length > 1) {
const diff = Math.abs(new Date(data[data.length - 1].date) - new Date(data[data.length - 2].date))
const date1 = new Date(data[data.length - 1].date)
const date2 = new Date(data[data.length - 2].date)
// @ts-ignore
const diff = Math.abs(date1 - date2)
const periodInDays = diff / (1000 * 60 * 60 * 24)
stepSize = periodInDays
@ -36,12 +39,14 @@ export function createCoinBalanceHistoryChart (el) {
datasets: [{
label: 'coin balance',
data: coinBalanceHistoryData,
// @ts-ignore
lineTension: 0,
cubicInterpolationMode: 'monotone',
fill: true
}]
},
plugins: {
// @ts-ignore
legend: {
display: false
}
@ -53,20 +58,25 @@ export function createCoinBalanceHistoryChart (el) {
options: {
scales: {
x: {
// @ts-ignore
type: 'time',
time: {
unit: 'day',
tooltipFormat: 'DD',
// @ts-ignore
stepSize
}
},
y: {
// @ts-ignore
type: 'linear',
ticks: {
// @ts-ignore
beginAtZero: true
},
title: {
display: true,
// @ts-ignore
labelString: window.localized.Ether
}
}

@ -28,6 +28,7 @@ const _instance2 = new Pikaday({
})
$button.on('click', () => {
// @ts-ignore
// eslint-disable-next-line
const recaptchaResponse = grecaptcha.getResponse()
if (recaptchaResponse) {
@ -52,6 +53,7 @@ $button.on('click', () => {
$button.prop('disabled', false)
clearInterval(interval)
Cookies.remove('csv-downloaded')
// @ts-ignore
// eslint-disable-next-line
grecaptcha.reset()
}

@ -19,6 +19,7 @@ function formatCurrencyValue (value, symbol) {
symbol = symbol || '$'
if (isNaN(value)) return 'N/A'
if (value === 0 || value === '0') return `${symbol}0.00`
// @ts-ignore
if (value < 0.000001) return `${window.localized['Less than']} ${symbol}0.000001`
if (value < 1) return `${symbol}${numeral(value).format('0.000000')}`
if (value < 100000) return `${symbol}${numeral(value).format('0,0.00')}`

@ -6,6 +6,7 @@ import humps from 'humps'
import numeral from 'numeral'
import { DateTime } from 'luxon'
import { formatUsdValue } from '../lib/currency'
// @ts-ignore
import sassVariables from '../../css/export-vars-to-js.module.scss'
Chart.defaults.font.family = 'Nunito, "Helvetica Neue", Arial, sans-serif,"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"'
@ -172,8 +173,7 @@ function getTxHistoryData (transactionHistory) {
// it should be empty value for tx history the current day
const prevDayStr = data[0].x
const prevDay = DateTime.fromISO(prevDayStr)
let curDay = prevDay.plus({ days: 1 })
curDay = curDay.toISODate()
const curDay = prevDay.plus({ days: 1 }).toISODate()
data.unshift({ x: curDay, y: null })
setDataToLocalStorage('txHistoryData', data)
@ -206,6 +206,7 @@ class MarketHistoryChart {
let marketCapActivated = true
this.price = {
// @ts-ignore
label: window.localized.Price,
yAxisID: 'price',
data: [],
@ -223,6 +224,7 @@ class MarketHistoryChart {
}
this.marketCap = {
// @ts-ignore
label: window.localized['Market Cap'],
yAxisID: 'marketCap',
data: [],
@ -242,6 +244,7 @@ class MarketHistoryChart {
}
this.numTransactions = {
// @ts-ignore
label: window.localized['Tx/day'],
yAxisID: 'numTransactions',
data: [],
@ -272,6 +275,7 @@ class MarketHistoryChart {
}
config.options.plugins.title.text = chartTitle
// @ts-ignore
config.data.datasets = [this.price, this.marketCap, this.numTransactions]
const isChartLoadedKey = 'isChartLoaded'
@ -279,9 +283,10 @@ class MarketHistoryChart {
if (isChartLoaded) {
config.options.animation = false
} else {
window.sessionStorage.setItem(isChartLoadedKey, true)
window.sessionStorage.setItem(isChartLoadedKey, 'true')
}
// @ts-ignore
this.chart = new Chart(el, config)
}

@ -18,8 +18,10 @@ function tryUpdateIndexedStatus (el, indexedRatioBlocks = el.dataset.indexedRati
let indexedText
if (blocksPercentComplete === '100%') {
const intTxsPercentComplete = numeral(el.dataset.indexedRatio).format('0%')
// @ts-ignore
indexedText = `${intTxsPercentComplete} ${window.localized['Blocks With Internal Transactions Indexed']}`
} else {
// @ts-ignore
indexedText = `${blocksPercentComplete} ${window.localized['Blocks Indexed']}`
}

@ -96,8 +96,10 @@ export function connectInfiniteScroll (store) {
function onScrollBottom (callback) {
const $window = $(window)
function infiniteScrollChecker () {
const scrollHeight = $(document).height()
const scrollPosition = $window.height() + $window.scrollTop()
const scrollHeight = $(document).height() || 1
const windowHeight = $window.height() || 0
const windowScrollTop = $window.scrollTop() || 0
const scrollPosition = windowHeight + windowScrollTop
if ((scrollHeight - scrollPosition) / scrollHeight === 0) {
callback()
}

@ -27,6 +27,7 @@ import { updateAllAges } from './from_now'
// 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
// @ts-ignore
export default function (container, newElements, { key, horizontal } = {}) {
if (!container) return
const oldElements = $(container).children().not('.shrink-out').get()
@ -49,6 +50,7 @@ export default function (container, newElements, { key, horizontal } = {}) {
if (overlap[i]) {
return ({
id: overlap[i].id,
// @ts-ignore
el: el.outerHTML === overlap[i].el && overlap[i].el.outerHTML ? el : morph(el, overlap[i].el)
})
} else {
@ -64,6 +66,7 @@ export default function (container, newElements, { key, horizontal } = {}) {
if (el.parentElement) return
if (!canAnimate) return container.insertBefore(el, get(finalList, `[${i - 1}]`))
if (!get(finalList, `[${i - 1}]`)) return slideDownAppend($(container), el)
// @ts-ignore
slideDownBefore($(get(finalList, `[${i - 1}]`)), el)
})
}

@ -127,7 +127,7 @@ export function openQuestionModal (title, text, acceptCallback = null, exceptCal
const $modal = $('#questionStatusModal')
const $closeButton = $modal.find('.close-modal')
$closeButton.attr('disabled', false)
$closeButton.attr('disabled', 'false')
$modal.find('.modal-status-title').text(title)
$modal.find('.modal-status-text').text(text)
@ -149,18 +149,19 @@ export function openQuestionModal (title, text, acceptCallback = null, exceptCal
if (acceptCallback) {
$accept.on('click', event => {
$closeButton.attr('disabled', true)
$closeButton.attr('disabled', 'true')
$accept
.unbind('click')
.attr('disabled', true)
.attr('disabled', 'true')
.find('.btn-line-text').html(spinner)
$except
.unbind('click')
.removeAttr('data-dismiss')
.attr('disabled', true)
.attr('disabled', 'true')
modalLocked = true
// @ts-ignore
acceptCallback($modal, event)
})
} else {
@ -169,18 +170,19 @@ export function openQuestionModal (title, text, acceptCallback = null, exceptCal
if (exceptCallback) {
$except.on('click', event => {
$closeButton.attr('disabled', true)
$closeButton.attr('disabled', 'true')
$except
.unbind('click')
.attr('disabled', true)
.attr('disabled', 'true')
.find('.btn-line-text').html(spinner)
$accept
.unbind('click')
.attr('disabled', true)
.attr('disabled', 'true')
.removeAttr('data-dismiss')
modalLocked = true
// @ts-ignore
exceptCallback($modal, event)
})
} else {

@ -2,6 +2,7 @@ import $ from 'jquery'
const $removeButton = $('.remove-form-field')[0]
const $container = $('#' + $removeButton.dataset.container)
// @ts-ignore
const index = parseInt($container[0].dataset.index)
if (index <= 1) {
@ -12,9 +13,12 @@ $('.add-form-field').on('click', (event) => {
event.preventDefault()
console.log(event)
const $container = $('#' + event.currentTarget.dataset.container)
// @ts-ignore
const index = parseInt($container[0].dataset.index)
if (index < 10) {
// @ts-ignore
$container.append($.parseHTML(event.currentTarget.dataset.prototype))
// @ts-ignore
$container[0].dataset.index = index + 1
}
if (index >= 9) {
@ -29,8 +33,10 @@ $('[data-multiple-input-field-container]').on('click', '.remove-form-field', (ev
event.preventDefault()
console.log(event)
const $container = $('#' + event.currentTarget.dataset.container)
// @ts-ignore
const index = parseInt($container[0].dataset.index)
if (index > 1) {
// @ts-ignore
$container[0].dataset.index = index - 1
event.currentTarget.parentElement.remove()
}

@ -73,6 +73,7 @@ export function asyncReducer (state = asyncInitialState, action) {
if (action.nextPageParams !== null) {
const pageNumber = parseInt(action.nextPageParams.pageNumber)
if (typeof action.path !== 'undefined') {
// @ts-ignore
history.replaceState({}, null, URI(action.path).query(humps.decamelizeKeys(action.nextPageParams)))
}
delete action.nextPageParams.pageNumber
@ -144,7 +145,7 @@ export const elements = {
if (state.itemKey) {
const container = $el[0]
const newElements = map(state.items, (item) => $(item)[0])
listMorph(container, newElements, { key: state.itemKey })
listMorph(container, newElements, { key: state.itemKey, horizontal: null })
return
}
@ -334,6 +335,7 @@ function pagesNumbersGenerate (pagesLimit, $container, currentPageNumber, loadin
resultHTML += renderPaginationElements(pagesLimit - groupedPagesNumber, pagesLimit, currentPageNumber, loading)
} else {
resultHTML += renderPaginationElement(1, currentPageNumber === 1, loading)
// @ts-ignore
const step = parseInt(groupedPagesNumber / 2)
if (currentPageNumber - step - 1 === 2) {
resultHTML += renderPaginationElement(2, currentPageNumber === 2, loading)

@ -8,6 +8,7 @@ import { createStore as reduxCreateStore } from 'redux'
* Create a redux store given the reducer. It also enables the Redux dev tools.
*/
export function createStore (reducer) {
// @ts-ignore
return reduxCreateStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__())
}

@ -96,7 +96,9 @@ export const formatTitleAndError = (error) => {
let errorMap = ''
try {
errorMap = message && message.indexOf('{') >= 0 ? JSON.parse(message && message.slice(message.indexOf('{'))) : ''
// @ts-ignore
message = errorMap.error || ''
// @ts-ignore
txHash = errorMap.transactionHash || ''
} catch (exception) {
message = ''
@ -141,6 +143,7 @@ export const getCurrentAccountFromWCPromise = (provider) => {
export const getCurrentAccountFromMMPromise = () => {
return new Promise((resolve, reject) => {
// @ts-ignore
window.ethereum.request({ method: 'eth_accounts' })
.then(accounts => {
const account = accounts[0] ? accounts[0].toLowerCase() : null
@ -153,35 +156,43 @@ export const getCurrentAccountFromMMPromise = () => {
}
function hideConnectedToContainer () {
document.querySelector(connectedToSelector) && document.querySelector(connectedToSelector).classList.add('hidden')
const obj = document.querySelector(connectedToSelector)
obj && obj.classList.add('hidden')
}
function showConnectedToContainer () {
document.querySelector(connectedToSelector) && document.querySelector(connectedToSelector).classList.remove('hidden')
const obj = document.querySelector(connectedToSelector)
obj && obj.classList.remove('hidden')
}
function hideConnectContainer () {
document.querySelector(connectSelector) && document.querySelector(connectSelector).classList.add('hidden')
const obj = document.querySelector(connectSelector)
obj && obj.classList.add('hidden')
}
function showConnectContainer () {
document.querySelector(connectSelector) && document.querySelector(connectSelector).classList.remove('hidden')
const obj = document.querySelector(connectSelector)
obj && obj.classList.remove('hidden')
}
function hideConnectToContainer () {
document.querySelector(connectToSelector) && document.querySelector(connectToSelector).classList.add('hidden')
const obj = document.querySelector(connectToSelector)
obj && obj.classList.add('hidden')
}
function showConnectToContainer () {
document.querySelector(connectToSelector) && document.querySelector(connectToSelector).classList.remove('hidden')
const obj = document.querySelector(connectToSelector)
obj && obj.classList.remove('hidden')
}
export function showHideDisconnectButton () {
// Show disconnect button only in case of Wallet Connect
const obj = document.querySelector(disconnectSelector)
// @ts-ignore
if (window.web3 && window.web3.currentProvider && window.web3.currentProvider.wc) {
document.querySelector(disconnectSelector) && document.querySelector(disconnectSelector).classList.remove('hidden')
obj && obj.classList.remove('hidden')
} else {
document.querySelector(disconnectSelector) && document.querySelector(disconnectSelector).classList.add('hidden')
obj && obj.classList.add('hidden')
}
}
@ -206,12 +217,14 @@ export function hideConnectButton () {
}
function setConnectToAddress (account) {
if (document.querySelector('[connected-to-address]')) {
document.querySelector('[connected-to-address]').innerHTML = `<a href='/address/${account}'>${trimmedAddressHash(account)}</a>`
const obj = document.querySelector('[connected-to-address]')
if (obj) {
obj.innerHTML = `<a href='/address/${account}'>${trimmedAddressHash(account)}</a>`
}
}
function trimmedAddressHash (account) {
// @ts-ignore
if ($(window).width() < 544) {
return `${account.slice(0, 7)}${account.slice(-6)}`
} else {

@ -4,9 +4,11 @@ import WalletConnectProvider from '@walletconnect/web3-provider'
import { compareChainIDs, formatError, showConnectElements, showConnectedToElements } from './common_helpers'
import { openWarningModal } from '../modals'
// @ts-ignore
const instanceChainIdStr = document.getElementById('js-chain-id').value
const instanceChainId = parseInt(instanceChainIdStr, 10)
const walletConnectOptions = { rpc: {}, chainId: instanceChainId }
// @ts-ignore
const jsonRPC = document.getElementById('js-json-rpc').value
walletConnectOptions.rpc[instanceChainId] = jsonRPC

@ -1,10 +1,10 @@
import $ from 'jquery'
import { connectSelector, disconnectSelector, getContractABI, getMethodInputs, prepareMethodArgs } from './common_helpers'
import { connectSelector, disconnectSelector, getCurrentAccountPromise, getContractABI, getMethodInputs, prepareMethodArgs } from './common_helpers'
import { queryMethod, callMethod } from './interact'
import { walletEnabled, connectToWallet, disconnectWallet, web3ModalInit } from './connect.js'
import '../../pages/address'
const loadFunctions = (element, isCustomABI) => {
const loadFunctions = (element, isCustomABI, from) => {
const $element = $(element)
const url = $element.data('url')
const hash = $element.data('hash')
@ -13,12 +13,14 @@ const loadFunctions = (element, isCustomABI) => {
$.get(
url,
{ hash, type, action, is_custom_abi: isCustomABI },
{ hash, type, action, is_custom_abi: isCustomABI, from },
response => $element.html(response)
)
.done(function () {
document.querySelector(connectSelector) && document.querySelector(connectSelector).addEventListener('click', connectToWallet)
document.querySelector(disconnectSelector) && document.querySelector(disconnectSelector).addEventListener('click', disconnectWallet)
const connectSelectorObj = document.querySelector(connectSelector)
connectSelectorObj && connectSelectorObj.addEventListener('click', connectToWallet)
const disconnectSelectorObj = document.querySelector(disconnectSelector)
disconnectSelectorObj && disconnectSelectorObj.addEventListener('click', disconnectWallet)
web3ModalInit(connectToWallet)
const selector = isCustomABI ? '[data-function-custom]' : '[data-function]'
@ -31,11 +33,13 @@ const loadFunctions = (element, isCustomABI) => {
const $customPower = $(event.currentTarget).find('[name=custom_power]')
let power
if ($customPower.length > 0) {
// @ts-ignore
power = parseInt($customPower.val(), 10)
} else {
power = parseInt($(event.currentTarget).data('power'), 10)
}
const $input = $(event.currentTarget).parent().parent().parent().find('[name=function_input]')
// @ts-ignore
const currentInputVal = parseInt($input.val(), 10) || 1
const newInputVal = (currentInputVal * Math.pow(10, power)).toString()
$input.val(newInputVal.toString())
@ -96,11 +100,19 @@ const readWriteFunction = (element) => {
const container = $('[data-smart-contract-functions]')
if (container.length) {
loadFunctions(container, false)
getWalletAndLoadFunctions()
}
const customABIContainer = $('[data-smart-contract-functions-custom]')
if (customABIContainer.length) {
loadFunctions(customABIContainer, true)
getWalletAndLoadFunctions()
}
function getWalletAndLoadFunctions () {
getCurrentAccountPromise(window.web3 && window.web3.currentProvider).then((currentAccount) => {
loadFunctions(container, false, currentAccount)
}, () => {
loadFunctions(container, false, null)
})
}

@ -9,12 +9,14 @@ const weiToEtherConverter = (element, event) => {
const $conversionTextEth = $element.find('[data-conversion-text-eth]')
const $conversionUnit = $element.find('[data-conversion-unit]')
const originalValueStr = $conversionUnit.data('original-value')
// @ts-ignore
const unitVal = new BigNumber(numeral(originalValueStr).value())
const weiVal = unitVal.dividedBy(weiUnit)
if (event.target.checked) {
$conversionTextWei.removeClass('d-inline-block').addClass('d-none')
$conversionTextEth.removeClass('d-none').addClass('d-inline-block')
// @ts-ignore
$conversionUnit.html(weiVal.toFixed() > 0 ? String(weiVal.toFixed()) : numeral(weiVal).format('0[.000000000000000000]'))
} else {
$conversionTextWei.removeClass('d-none').addClass('d-inline-block')

@ -93,6 +93,7 @@ $('button[data-selector*="btn-try-api-clear"]').click(event => {
// Remove invalid class from required fields if not empty
$('input[data-selector*="try-api-ui"][data-required="true"]').on('keyup', (event) => {
// @ts-ignore
if (event.target.value !== '') {
event.target.classList.remove('is-invalid')
} else {

@ -40,6 +40,7 @@ function parseInput (input) {
return value
case 'json':
try {
// @ts-ignore
return JSON.parse(value)
} catch (e) {
return {}

@ -4,6 +4,7 @@ $('[data-delete-item]').on('click', (event) => {
event.preventDefault()
if (confirm('Are you sure you want to delete item?')) {
// @ts-ignore
$(event.currentTarget.parentElement).find('form').trigger('submit')
}
})
@ -11,9 +12,12 @@ $('[data-delete-item]').on('click', (event) => {
$('[data-delete-request]').on('click', (event) => {
event.preventDefault()
// @ts-ignore
const result = prompt('Public tags: "' + event.currentTarget.dataset.tags.replace(';', '" and "') + '" will be removed.\nWhy do you want to remove tags?')
if (result) {
// @ts-ignore
$(event.currentTarget.parentElement).find('[name="remove_reason"]').val(result)
// @ts-ignore
$(event.currentTarget.parentElement).find('form').trigger('submit')
}
})

@ -56,12 +56,14 @@ export function reducer (state = initialState, action) {
case 'RECEIVED_NEW_BLOCK': {
if (state.channelDisconnected) return state
// @ts-ignore
const validationCount = state.validationCount + 1
return Object.assign({}, state, { validationCount })
}
case 'RECEIVED_NEW_TRANSACTION': {
if (state.channelDisconnected) return state
// @ts-ignore
const transactionCount = (action.msg.fromAddressHash === state.addressHash) ? state.transactionCount + 1 : state.transactionCount
return Object.assign({}, state, { transactionCount })
@ -69,6 +71,7 @@ export function reducer (state = initialState, action) {
case 'RECEIVED_NEW_TOKEN_TRANSFER': {
if (state.channelDisconnected) return state
// @ts-ignore
const tokenTransferCount = (action.msg.fromAddressHash === state.addressHash) ? state.tokenTransferCount + 1 : state.tokenTransferCount
return Object.assign({}, state, { tokenTransferCount })
@ -107,6 +110,7 @@ function loadTokenBalance (blockNumber) {
const elements = {
'[data-selector="channel-disconnected-message"]': {
render ($el, state) {
// @ts-ignore
if (state.channelDisconnected && !window.loading) $el.show()
}
},
@ -251,11 +255,13 @@ if ($addressDetailsPage.length) {
}
window.onbeforeunload = () => {
// @ts-ignore
window.loading = true
}
const store = createStore(reducer)
const addressHash = $addressDetailsPage[0].dataset.pageAddressHash
// @ts-ignore
const { filter, blockNumber } = humps.camelizeKeys(URI(window.location).query(true))
store.dispatch({
type: 'PAGE_LOAD',

@ -38,6 +38,7 @@ export function reducer (state, action) {
const elements = {
'[data-selector="channel-disconnected-message"]': {
render ($el, state) {
// @ts-ignore
if (state.channelDisconnected && !window.loading) $el.show()
}
}
@ -45,6 +46,7 @@ const elements = {
if ($('[data-page="coin-balance-history"]').length) {
window.onbeforeunload = () => {
// @ts-ignore
window.loading = true
}

@ -66,6 +66,7 @@ export function reducer (state, action) {
const elements = {
'[data-selector="channel-disconnected-message"]': {
render ($el, state) {
// @ts-ignore
if (state.channelDisconnected && !window.loading) $el.show()
}
},
@ -96,6 +97,7 @@ const elements = {
if ($('[data-page="address-internal-transactions"]').length) {
window.onbeforeunload = () => {
// @ts-ignore
window.loading = true
}

@ -3,6 +3,7 @@ import omit from 'lodash.omit'
import { connectElements } from '../../lib/redux_helpers.js'
import { createAsyncLoadStore, loadPage } from '../../lib/async_listing_load'
import '../address'
// @ts-ignore
import { utils } from 'web3'
export const initialState = {

@ -49,6 +49,7 @@ export function reducer (state, action) {
const elements = {
'[data-selector="channel-disconnected-message"]': {
render ($el, state) {
// @ts-ignore
if (state.channelDisconnected && !window.loading) $el.show()
}
},
@ -71,11 +72,13 @@ const elements = {
if ($('[data-page="address-token-transfers"]').length) {
window.onbeforeunload = () => {
// @ts-ignore
window.loading = true
}
const store = createAsyncLoadStore(reducer, initialState, 'dataset.identifierHash')
const addressHash = $('[data-page="address-details"]')[0].dataset.pageAddressHash
// @ts-ignore
const { filter, blockNumber } = humps.camelizeKeys(URI(window.location).query(true))
connectElements({ store, elements })

@ -89,6 +89,7 @@ export function reducer (state, action) {
const elements = {
'[data-selector="channel-disconnected-message"]': {
render ($el, state) {
// @ts-ignore
if (state.channelDisconnected && !window.loading) $el.show()
}
},
@ -119,11 +120,13 @@ const elements = {
if ($('[data-page="address-transactions"]').length) {
window.onbeforeunload = () => {
// @ts-ignore
window.loading = true
}
const store = createAsyncLoadStore(reducer, initialState, 'dataset.identifierHash')
const addressHash = $('[data-page="address-details"]')[0].dataset.pageAddressHash
// @ts-ignore
const { filter, blockNumber } = humps.camelizeKeys(URI(window.location).query(true))
connectElements({ store, elements })
@ -174,6 +177,7 @@ if ($('[data-page="address-transactions"]').length) {
function loadTransactions (store) {
const path = $('[class="card-body"]')[0].dataset.asyncListing
store.dispatch({ type: 'START_TRANSACTIONS_FETCH' })
// @ts-ignore
$.getJSON(path, { type: 'JSON' })
.done(response => store.dispatch({ type: 'TRANSACTIONS_FETCHED', msg: humps.camelizeKeys(response) }))
.fail(() => store.dispatch({ type: 'TRANSACTIONS_FETCH_ERROR' }))

@ -39,6 +39,7 @@ export function reducer (state = initialState, action) {
const elements = {
'[data-selector="channel-disconnected-message"]': {
render ($el, state) {
// @ts-ignore
if (state.channelDisconnected && !window.loading) $el.show()
}
}
@ -46,6 +47,7 @@ const elements = {
if ($('[data-page="blocks-validated"]').length) {
window.onbeforeunload = () => {
// @ts-ignore
window.loading = true
}

@ -47,6 +47,7 @@ function baseReducer (state = initialState, action) {
const elements = {
'[data-selector="channel-disconnected-message"]': {
render ($el, state) {
// @ts-ignore
if (state.channelDisconnected && !window.loading) $el.show()
}
}
@ -69,8 +70,8 @@ function withMissingBlocks (reducer) {
}, {})
const blockNumbers = keys(blockNumbersToItems).map(x => parseInt(x, 10))
const minBlock = min(blockNumbers)
const maxBlock = max(blockNumbers)
const minBlock = min(blockNumbers) || 0
const maxBlock = max(blockNumbers) || 0
if (maxBlock - minBlock > 100) return result
return Object.assign({}, result, {
@ -85,6 +86,7 @@ const $uncleListPage = $('[data-page="uncle-list"]')
const $reorgListPage = $('[data-page="reorg-list"]')
if ($blockListPage.length || $uncleListPage.length || $reorgListPage.length) {
window.onbeforeunload = () => {
// @ts-ignore
window.loading = true
}
@ -121,7 +123,10 @@ export function placeHolderBlock (blockNumber) {
</span>
<div>
<span class="tile-title pr-0 pl-0">${blockNumber}</span>
<div class="tile-transactions">${window.localized['Block Processing']}</div>
<div class="tile-transactions">${
// @ts-ignore
window.localized['Block Processing']
}</div>
</div>
</div>
</div>

@ -47,6 +47,7 @@ function baseReducer (state = initialState, action) {
})
}
case 'RECEIVED_NEW_BLOCK': {
// @ts-ignore
if (!state.blocks.length || state.blocks[0].blockNumber < action.msg.blockNumber) {
let pastBlocks
if (state.blocks.length < BLOCKS_PER_PAGE) {
@ -65,6 +66,7 @@ function baseReducer (state = initialState, action) {
})
} else {
return Object.assign({}, state, {
// @ts-ignore
blocks: state.blocks.map((block) => block.blockNumber === action.msg.blockNumber ? action.msg : block),
blockCount: action.msg.blockNumber + 1
})
@ -171,6 +173,7 @@ let chart
const elements = {
'[data-chart="historyChart"]': {
load () {
// @ts-ignore
chart = window.dashboardChart
},
render (_$el, state, oldState) {
@ -284,7 +287,7 @@ const elements = {
if (oldState.transactions === state.transactions) return
const container = $el[0]
const newElements = map(state.transactions, ({ transactionHtml }) => $(transactionHtml)[0])
listMorph(container, newElements, { key: 'dataset.identifierHash' })
listMorph(container, newElements, { key: 'dataset.identifierHash', horizontal: null })
}
},
'[data-selector="channel-batching-count"]': {
@ -387,7 +390,10 @@ export function placeHolderBlock (blockNumber) {
</span>
<div>
<span class="tile-title pr-0 pl-0">${blockNumber}</span>
<div class="tile-transactions">${window.localized['Block Processing']}</div>
<div class="tile-transactions">${
// @ts-ignore
window.localized['Block Processing']
}</div>
</div>
</div>
</div>

@ -8,5 +8,5 @@ $('.dark-mode-changer').on('click', function () {
Cookies.set('chakra-ui-color-mode', 'dark')
}
// reload each theme switch
document.location.reload(true)
document.location.reload()
})

@ -97,6 +97,7 @@ $('.send-public-tag-request-button').click((_event) => {
goal: $('#public_tags_request_is_owner_true').prop('checked') ? 'Add tags' : 'Incorrect public tag',
public_tag: $('#public_tags_request_tags').val(),
smart_contracts: $('*[id=public_tags_request_addresses]').map((_i, el) => {
// @ts-ignore
return el.value
}).get(),
reason: $('#public_tags_request_additional_comment').val()
@ -109,6 +110,7 @@ $(document).ready(() => {
let timer
const waitTime = 500
const observer = new MutationObserver((mutations) => {
// @ts-ignore
if (mutations[0].target.hidden) {
return
}
@ -125,9 +127,11 @@ $(document).ready(() => {
analytics.trackEvent(eventName, eventProperties)
eventName = 'Search list displays at the nav bar'
// @ts-ignore
eventProperties = {
resultsNumber: $results.length,
results: $results.map((_i, el) => {
// @ts-ignore
return el.children[1].innerText
})
}
@ -156,6 +160,7 @@ $(document).ready(() => {
$(document).click(function (event) {
const clickover = $(event.target)
const _opened = $('.navbar-collapse').hasClass('show')
// @ts-ignore
if (_opened === true && $('.navbar').find(clickover).length < 1) {
$('.navbar-toggler').click()
}
@ -194,6 +199,7 @@ $('.main-search-autocomplete').on('keyup', function (event) {
}
})
if (!selected) {
// @ts-ignore
search(event.target.value)
}
}

@ -32,7 +32,9 @@ export function reducer (state = initialState, action) {
if (state.channelDisconnected) return state
return Object.assign({}, state, {
items: state.items.map((item) => item.includes(action.msg.transactionHash) ? action.msg.transactionHtml : item),
// @ts-ignore
pendingTransactionsBatch: state.pendingTransactionsBatch.filter(transactionHtml => !transactionHtml.includes(action.msg.transactionHash)),
// @ts-ignore
pendingTransactionCount: state.pendingTransactionCount - 1
})
}
@ -73,6 +75,7 @@ export function reducer (state = initialState, action) {
const elements = {
'[data-selector="channel-disconnected-message"]': {
render ($el, state) {
// @ts-ignore
if (state.channelDisconnected && !window.loading) $el.show()
}
},
@ -101,6 +104,7 @@ const elements = {
const $transactionPendingListPage = $('[data-page="transaction-pending-list"]')
if ($transactionPendingListPage.length) {
window.onbeforeunload = () => {
// @ts-ignore
window.loading = true
}

@ -47,11 +47,13 @@ if ($('[data-page="search-results"]').length) {
analytics.trackEvent(eventName, eventProperties)
}, waitTime)
// @ts-ignore
$('.js-search-results-query-display').text(value)
const loc = window.location.pathname
if (value.length >= 3 || value === '') {
// @ts-ignore
if ((value && value.length >= 3) || value === '') {
store.dispatch({ type: 'START_SEARCH' })
store.dispatch({ type: 'START_REQUEST' })
$.ajax({

@ -28,25 +28,27 @@ const elements = {
$('#spinner').hide()
$('#gallery img').attr('src', 'data:image/svg+xml;base64,' + state.contract_svg)
const gallery = document.getElementById('gallery')
const viewer = new Viewer(gallery, {
inline: false,
toolbar: {
zoomIn: 2,
zoomOut: 4,
oneToOne: 4,
reset: 4,
play: {
show: 4,
size: 'large'
},
rotateLeft: 4,
rotateRight: 4,
flipHorizontal: 4,
flipVertical: 4
}
})
viewer.update()
$el.show()
if (gallery) {
const viewer = new Viewer(gallery, {
inline: false,
toolbar: {
zoomIn: 2,
zoomOut: 4,
oneToOne: 4,
reset: 4,
play: {
show: 4,
size: 'large'
},
rotateLeft: 4,
rotateRight: 4,
flipHorizontal: 4,
flipVertical: 4
}
})
viewer.update()
$el.show()
}
} else if (state.visualize_error) {
$('#spinner').hide()
$el.empty().text('Cannot visualize contract: ' + state.visualize_error)

@ -41,7 +41,8 @@ if ($('[data-page="tokens"]').length) {
const loc = window.location.pathname
if (value.length >= 3 || value === '') {
// @ts-ignore
if ((value && value.length >= 3) || value === '') {
store.dispatch({ type: 'START_SEARCH' })
store.dispatch({ type: 'START_REQUEST' })
$.ajax({

@ -45,6 +45,7 @@ export function reducer (state, action) {
const elements = {
'[data-selector="channel-disconnected-message"]': {
render ($el, state) {
// @ts-ignore
if (state.channelDisconnected && !window.loading) $el.show()
}
}
@ -52,11 +53,13 @@ const elements = {
if ($('[data-page="token-transfer-list"]')) {
window.onbeforeunload = () => {
// @ts-ignore
window.loading = true
}
const store = createAsyncLoadStore(reducer, initialState, 'dataset.identifierHash')
const addressHash = $('[data-page="token-details"]')[0].dataset.pageAddressHash
// @ts-ignore
const { blockNumber } = humps.camelizeKeys(URI(window.location).query(true))
connectElements({ store, elements })

@ -86,6 +86,7 @@ function updateCounters () {
if ($('[data-page="token-holders-list"]').length) {
window.onbeforeunload = () => {
// @ts-ignore
window.loading = true
}

@ -21,10 +21,13 @@ export function reducer (state = initialState, action) {
return Object.assign({}, state, omit(action, 'type'))
}
case 'RECEIVED_NEW_BLOCK': {
if ((action.msg.blockNumber - state.blockNumber) > state.confirmations) {
return Object.assign({}, state, {
confirmations: action.msg.blockNumber - state.blockNumber
})
if (state.blockNumber) {
// @ts-ignore
if ((action.msg.blockNumber - state.blockNumber) > state.confirmations) {
return Object.assign({}, state, {
confirmations: action.msg.blockNumber - state.blockNumber
})
} else return state
} else return state
}
default:
@ -60,7 +63,8 @@ if ($transactionDetailsPage.length) {
pathParts.includes('raw-trace') ||
pathParts.includes('state')
if (shouldScroll) {
document.getElementById('transaction-tabs').scrollIntoView()
const txTabsObj = document.getElementById('transaction-tabs')
txTabsObj && txTabsObj.scrollIntoView()
}
const blocksChannel = socket.channel('blocks:new_block', {})
@ -77,6 +81,7 @@ if ($transactionDetailsPage.length) {
$('.js-cancel-transaction').on('click', (event) => {
const btn = $(event.target)
// @ts-ignore
if (!window.ethereum) {
btn
.attr('data-original-title', `Please unlock ${btn.data('from')} account in Metamask`)
@ -89,6 +94,7 @@ if ($transactionDetailsPage.length) {
}, 3000)
return
}
// @ts-ignore
const { chainId: walletChainIdHex } = window.ethereum
compareChainIDs(btn.data('chainId'), walletChainIdHex)
.then(() => {
@ -98,6 +104,7 @@ if ($transactionDetailsPage.length) {
value: 0,
nonce: btn.data('nonce').toString()
}
// @ts-ignore
window.ethereum.request({
method: 'eth_sendTransaction',
params: [txParams]
@ -142,26 +149,26 @@ $(function () {
$collapseButton.on('click', event => {
const $button = event.target
const $parent = $button.parentElement
const $collapseButton = $parent.querySelector('[button-collapse-input]')
const $expandButton = $parent.querySelector('[button-expand-input]')
const $hiddenText = $parent.querySelector('[data-hidden-text]')
const $placeHolder = $parent.querySelector('[data-placeholder-dots]')
$collapseButton.classList.add('d-none')
$expandButton.classList.remove('d-none')
$hiddenText.classList.add('d-none')
$placeHolder.classList.remove('d-none')
const $collapseButton = $parent && $parent.querySelector('[button-collapse-input]')
const $expandButton = $parent && $parent.querySelector('[button-expand-input]')
const $hiddenText = $parent && $parent.querySelector('[data-hidden-text]')
const $placeHolder = $parent && $parent.querySelector('[data-placeholder-dots]')
$collapseButton && $collapseButton.classList.add('d-none')
$expandButton && $expandButton.classList.remove('d-none')
$hiddenText && $hiddenText.classList.add('d-none')
$placeHolder && $placeHolder.classList.remove('d-none')
})
$expandButton.on('click', event => {
const $button = event.target
const $parent = $button.parentElement
const $collapseButton = $parent.querySelector('[button-collapse-input]')
const $expandButton = $parent.querySelector('[button-expand-input]')
const $hiddenText = $parent.querySelector('[data-hidden-text]')
const $placeHolder = $parent.querySelector('[data-placeholder-dots]')
$expandButton.classList.add('d-none')
$collapseButton.classList.remove('d-none')
$hiddenText.classList.remove('d-none')
$placeHolder.classList.add('d-none')
const $collapseButton = $parent && $parent.querySelector('[button-collapse-input]')
const $expandButton = $parent && $parent.querySelector('[button-expand-input]')
const $hiddenText = $parent && $parent.querySelector('[data-hidden-text]')
const $placeHolder = $parent && $parent.querySelector('[data-placeholder-dots]')
$expandButton && $expandButton.classList.add('d-none')
$collapseButton && $collapseButton.classList.remove('d-none')
$hiddenText && $hiddenText.classList.remove('d-none')
$placeHolder && $placeHolder.classList.add('d-none')
})
})

@ -58,6 +58,7 @@ export function reducer (state = initialState, action) {
const elements = {
'[data-selector="channel-disconnected-message"]': {
render ($el, state) {
// @ts-ignore
if (state.channelDisconnected && !window.loading) $el.show()
}
},
@ -83,6 +84,7 @@ const elements = {
const $transactionListPage = $('[data-page="transaction-list"]')
if ($transactionListPage.length) {
window.onbeforeunload = () => {
// @ts-ignore
window.loading = true
}

@ -40,6 +40,7 @@ export function reducer (state = initialState, action) {
const elements = {
'[data-selector="channel-disconnected-message"]': {
render ($el, state) {
// @ts-ignore
if (state.channelDisconnected && !window.loading) $el.show()
}
},
@ -66,24 +67,27 @@ const $contractVerificationChooseTypePage = $('[data-page="contract-verification
function filterNightlyBuilds (filter, selectFirstNonNightly_) {
const select = document.getElementById('smart_contract_compiler_version')
const options = select.getElementsByTagName('option')
const options = select && select.getElementsByTagName('option')
let selectFirstNonNightly = selectFirstNonNightly_
for (const option of options) {
const txtValue = option.textContent || option.innerText
if (filter) {
if (txtValue.toLowerCase().indexOf('nightly') > -1) {
option.style.display = 'none'
if (options) {
for (const option of options) {
const txtValue = option.textContent || option.innerText
if (filter) {
if (txtValue.toLowerCase().indexOf('nightly') > -1) {
option.style.display = 'none'
} else {
if (selectFirstNonNightly) {
// @ts-ignore
option.selected = 'selected'
selectFirstNonNightly = false
}
option.style.display = ''
}
} else {
if (selectFirstNonNightly) {
option.selected = 'selected'
selectFirstNonNightly = false
if (txtValue.toLowerCase().indexOf('nightly') > -1) {
option.style.display = ''
}
option.style.display = ''
}
} else {
if (txtValue.toLowerCase().indexOf('nightly') > -1) {
option.style.display = ''
}
}
}
@ -93,6 +97,7 @@ let dropzone
if ($contractVerificationPage.length) {
window.onbeforeunload = () => {
// @ts-ignore
window.loading = true
}
@ -188,6 +193,7 @@ if ($contractVerificationPage.length) {
// submit form without page updating in order to avoid websocket reconnecting
event.preventDefault()
const $form = $('form')[0]
// @ts-ignore
$.post($form.action, convertFormToJSON($form))
})

@ -59,7 +59,10 @@ if ($('[data-page="verified-contracts-list"]').length) {
const $element = $('[data-async-listing]')
$element.on('click', '[data-next-page-button], [data-prev-page-button]', (event) => {
document.getElementById('verified-contracts-list').scrollIntoView()
const obj = document.getElementById('verified-contracts-list')
if (obj) {
obj.scrollIntoView()
}
})
const store = createAsyncLoadStore(reducer, initialState, 'dataset.identifierHash')

@ -1,6 +1,7 @@
import { Socket } from 'phoenix'
import { locale } from './locale'
// @ts-ignore
let websocketRootUrl = process.env.SOCKET_ROOT
if (!websocketRootUrl) {
websocketRootUrl = ''
@ -26,6 +27,7 @@ export default socket
* Returns a Channel instance.
*/
export function subscribeChannel (topic) {
// @ts-ignore
const channel = socket.channels.find(channel => channel.topic === topic)
if (channel) {

@ -1,3 +1,4 @@
// @ts-nocheck
import '../../lib/ace/src-min/ace'
import '../../lib/ace/src-min/mode-csharp'
import '../../lib/ace/src-min/theme-chrome'

File diff suppressed because it is too large Load Diff

@ -20,16 +20,16 @@
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.2.1",
"@amplitude/analytics-browser": "^1.6.7",
"@amplitude/analytics-browser": "^1.6.8",
"@tarekraafat/autocomplete.js": "^10.2.7",
"@walletconnect/web3-provider": "^1.8.0",
"assert": "^2.0.0",
"bignumber.js": "^9.1.1",
"bootstrap": "^4.6.0",
"chart.js": "^4.1.1",
"chart.js": "^4.2.0",
"chartjs-adapter-luxon": "^1.3.0",
"clipboard": "^2.0.11",
"core-js": "^3.26.1",
"core-js": "^3.27.2",
"crypto-browserify": "^3.12.0",
"dropzone": "^5.9.3",
"eth-net-props": "^1.0.41",
@ -56,7 +56,7 @@
"lodash.omit": "^4.5.0",
"lodash.rangeright": "^4.2.0",
"lodash.reduce": "^4.6.0",
"luxon": "^3.1.1",
"luxon": "^3.2.1",
"malihu-custom-scrollbar-plugin": "3.1.5",
"mixpanel-browser": "^2.45.0",
"moment": "^2.29.4",
@ -73,33 +73,33 @@
"redux": "^4.2.0",
"stream-browserify": "^3.0.0",
"stream-http": "^3.1.1",
"sweetalert2": "^11.6.16",
"sweetalert2": "^11.7.0",
"urijs": "^1.19.11",
"url": "^0.11.0",
"util": "^0.12.5",
"viewerjs": "^1.11.1",
"viewerjs": "^1.11.2",
"web3": "^1.8.1",
"web3modal": "^1.9.10",
"web3modal": "^1.9.11",
"xss": "^1.0.14"
},
"devDependencies": {
"@babel/core": "^7.20.5",
"@babel/core": "^7.20.12",
"@babel/preset-env": "^7.20.2",
"autoprefixer": "^10.4.13",
"babel-loader": "^9.1.0",
"babel-loader": "^9.1.2",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^5.2.7",
"css-minimizer-webpack-plugin": "^4.2.2",
"eslint": "^8.30.0",
"eslint": "^8.32.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.1.1",
"file-loader": "^6.2.0",
"jest": "^29.3.1",
"jest-environment-jsdom": "^29.3.1",
"mini-css-extract-plugin": "^2.7.2",
"postcss": "^8.4.20",
"postcss": "^8.4.21",
"postcss-loader": "^7.0.2",
"sass": "^1.57.1",
"sass-loader": "^13.2.0",

@ -13,8 +13,11 @@ defmodule BlockScoutWeb.ApiRouter do
Router for API
"""
use BlockScoutWeb, :router
alias BlockScoutWeb.SmartContractsApiV2Router
alias BlockScoutWeb.Plug.{CheckAccountAPI, CheckApiV2}
forward("/v2/smart-contracts", SmartContractsApiV2Router)
pipeline :api do
plug(:accepts, ["json"])
end
@ -95,7 +98,10 @@ defmodule BlockScoutWeb.ApiRouter do
alias BlockScoutWeb.API.V2
get("/search", V2.SearchController, :search)
scope "/search" do
get("/", V2.SearchController, :search)
get("/check-redirect", V2.SearchController, :check_redirect)
end
scope "/config" do
get("/json-rpc-url", V2.ConfigController, :json_rpc_url)
@ -117,9 +123,11 @@ defmodule BlockScoutWeb.ApiRouter do
end
scope "/addresses" do
get("/", V2.AddressController, :addresses_list)
get("/:address_hash", V2.AddressController, :address)
get("/:address_hash/counters", V2.AddressController, :counters)
get("/:address_hash/token-balances", V2.AddressController, :token_balances)
get("/:address_hash/tokens", V2.AddressController, :tokens)
get("/:address_hash/transactions", V2.AddressController, :transactions)
get("/:address_hash/token-transfers", V2.AddressController, :token_transfers)
get("/:address_hash/internal-transactions", V2.AddressController, :internal_transactions)
@ -130,6 +138,7 @@ defmodule BlockScoutWeb.ApiRouter do
end
scope "/tokens" do
get("/", V2.TokenController, :tokens_list)
get("/:address_hash", V2.TokenController, :token)
get("/:address_hash/counters", V2.TokenController, :counters)
get("/:address_hash/transfers", V2.TokenController, :transfers)

@ -5,6 +5,7 @@ defmodule BlockScoutWeb.AddressChannel do
use BlockScoutWeb, :channel
alias BlockScoutWeb.API.V2.AddressView, as: AddressViewAPI
alias BlockScoutWeb.API.V2.TransactionView, as: TransactionViewAPI
alias BlockScoutWeb.{
AddressCoinBalanceView,
@ -120,10 +121,16 @@ defmodule BlockScoutWeb.AddressChannel do
def handle_out(
"internal_transaction",
%{address: _address, internal_transaction: _internal_transaction},
%{address: _address, internal_transaction: internal_transaction},
%Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket
) do
push(socket, "internal_transaction", %{internal_transaction: 1})
internal_transaction_json =
TransactionViewAPI.render("internal_transaction.json", %{
internal_transaction: internal_transaction,
conn: nil
})
push(socket, "internal_transaction", %{internal_transaction: internal_transaction_json})
{:noreply, socket}
end
@ -236,11 +243,13 @@ defmodule BlockScoutWeb.AddressChannel do
end
def handle_transaction(
%{address: _address, transaction: _transaction},
%{address: _address, transaction: transaction},
%Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket,
event
) do
push(socket, event, %{transaction: 1})
transaction_json = TransactionViewAPI.render("transaction.json", %{transaction: transaction, conn: nil})
push(socket, event, %{transaction: transaction_json})
{:noreply, socket}
end
@ -269,11 +278,13 @@ defmodule BlockScoutWeb.AddressChannel do
end
def handle_token_transfer(
%{address: _address, token_transfer: _token_transfer},
%{address: _address, token_transfer: token_transfer},
%Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket,
event
) do
push(socket, event, %{token_transfer: 1})
token_transfer_json = TransactionViewAPI.render("token_transfer.json", %{token_transfer: token_transfer, conn: nil})
push(socket, event, %{token_transfer: token_transfer_json})
{:noreply, socket}
end

@ -13,7 +13,8 @@ defmodule BlockScoutWeb.API.V2.AddressController do
only: [delete_parameters_from_next_page_params: 1, token_transfers_types_options: 1]
alias BlockScoutWeb.AccessHelpers
alias BlockScoutWeb.API.V2.{AddressView, BlockView, TransactionView}
alias BlockScoutWeb.API.V2.{BlockView, TransactionView}
alias Explorer.ExchangeRates.Token
alias Explorer.{Chain, Market}
alias Indexer.Fetcher.TokenBalanceOnDemand
@ -33,7 +34,8 @@ defmodule BlockScoutWeb.API.V2.AddressController do
necessity_by_association: %{
:to_address => :optional,
:from_address => :optional,
:block => :optional
:block => :optional,
:transaction => :optional
}
]
@ -70,7 +72,8 @@ defmodule BlockScoutWeb.API.V2.AddressController do
def token_balances(conn, %{"address_hash" => address_hash_string} = params) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)},
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params) do
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params),
{:not_found, {:ok, _address}} <- {:not_found, Chain.hash_to_address(address_hash, [], false)} do
token_balances =
address_hash
|> Chain.fetch_last_token_balances()
@ -91,7 +94,8 @@ defmodule BlockScoutWeb.API.V2.AddressController do
def transactions(conn, %{"address_hash" => address_hash_string} = params) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)},
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params) do
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params),
{:not_found, {:ok, _address}} <- {:not_found, Chain.hash_to_address(address_hash, [], false)} do
options =
@transaction_necessity_by_association
|> Keyword.merge(paging_options(params))
@ -110,9 +114,51 @@ defmodule BlockScoutWeb.API.V2.AddressController do
end
end
def token_transfers(
conn,
%{"address_hash" => address_hash_string, "token" => token_address_hash_string} = params
) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)},
{:format, {:ok, token_address_hash}} <- {:format, Chain.string_to_address_hash(token_address_hash_string)},
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params),
{:ok, false} <- AccessHelpers.restricted_access?(token_address_hash_string, params),
{:not_found, {:ok, _address}} <- {:not_found, Chain.hash_to_address(address_hash, [], false)},
{:not_found, {:ok, _}} <- {:not_found, Chain.token_from_address_hash(token_address_hash)} do
options =
[
necessity_by_association: %{
:to_address => :optional,
:from_address => :optional,
:block => :optional,
:token => :optional,
:transaction => :optional
}
]
|> Keyword.merge(paging_options(params))
results_plus_one =
Chain.address_hash_to_token_transfers_by_token_address_hash(
address_hash,
token_address_hash,
options
)
{token_transfers, next_page} = split_list_by_page(results_plus_one)
next_page_params =
next_page |> next_page_params(token_transfers, params) |> delete_parameters_from_next_page_params()
conn
|> put_status(200)
|> put_view(TransactionView)
|> render(:token_transfers, %{token_transfers: token_transfers, next_page_params: next_page_params})
end
end
def token_transfers(conn, %{"address_hash" => address_hash_string} = params) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)},
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params) do
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params),
{:not_found, {:ok, _address}} <- {:not_found, Chain.hash_to_address(address_hash, [], false)} do
options =
@token_transfer_necessity_by_association
|> Keyword.merge(paging_options(params))
@ -139,7 +185,8 @@ defmodule BlockScoutWeb.API.V2.AddressController do
def internal_transactions(conn, %{"address_hash" => address_hash_string} = params) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)},
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params) do
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params),
{:not_found, {:ok, _address}} <- {:not_found, Chain.hash_to_address(address_hash, [], false)} do
full_options =
[
necessity_by_association: %{
@ -172,7 +219,8 @@ defmodule BlockScoutWeb.API.V2.AddressController do
def logs(conn, %{"address_hash" => address_hash_string, "topic" => topic} = params) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)},
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params) do
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params),
{:not_found, {:ok, _address}} <- {:not_found, Chain.hash_to_address(address_hash, [], false)} do
prepared_topic = String.trim(topic)
formatted_topic = if String.starts_with?(prepared_topic, "0x"), do: prepared_topic, else: "0x" <> prepared_topic
@ -192,7 +240,8 @@ defmodule BlockScoutWeb.API.V2.AddressController do
def logs(conn, %{"address_hash" => address_hash_string} = params) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)},
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params) do
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params),
{:not_found, {:ok, _address}} <- {:not_found, Chain.hash_to_address(address_hash, [], false)} do
results_plus_one = Chain.address_to_logs(address_hash, paging_options(params))
{logs, next_page} = split_list_by_page(results_plus_one)
@ -207,7 +256,8 @@ defmodule BlockScoutWeb.API.V2.AddressController do
def blocks_validated(conn, %{"address_hash" => address_hash_string} = params) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)},
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params) do
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params),
{:not_found, {:ok, _address}} <- {:not_found, Chain.hash_to_address(address_hash, [], false)} do
full_options =
Keyword.merge(
[
@ -236,8 +286,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do
def coin_balance_history(conn, %{"address_hash" => address_hash_string} = params) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)},
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params),
{:not_found, {:ok, _address}, _} <-
{:not_found, Chain.hash_to_address(address_hash), :empty_items_with_next_page_params} do
{:not_found, {:ok, _address}} <- {:not_found, Chain.hash_to_address(address_hash)} do
full_options = paging_options(params)
results_plus_one = Chain.address_to_coin_balances(address_hash, full_options)
@ -249,22 +298,67 @@ defmodule BlockScoutWeb.API.V2.AddressController do
conn
|> put_status(200)
|> put_view(AddressView)
|> render(:coin_balances, %{coin_balances: coin_balances, next_page_params: next_page_params})
end
end
def coin_balance_history_by_day(conn, %{"address_hash" => address_hash_string} = params) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)},
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params) do
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params),
{:not_found, {:ok, _address}} <- {:not_found, Chain.hash_to_address(address_hash, [], false)} do
balances_by_day =
address_hash
|> Chain.address_to_balances_by_day(true)
conn
|> put_status(200)
|> put_view(AddressView)
|> render(:coin_balances_by_day, %{coin_balances_by_day: balances_by_day})
end
end
def tokens(conn, %{"address_hash" => address_hash_string} = params) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)},
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params),
{:not_found, {:ok, _address}} <- {:not_found, Chain.hash_to_address(address_hash, [], false)} do
results_plus_one =
address_hash
|> Chain.fetch_last_token_balances(
params
|> delete_parameters_from_next_page_params()
|> paging_options()
|> Keyword.merge(token_transfers_types_options(params))
)
|> Market.add_price()
{tokens, next_page} = split_list_by_page(results_plus_one)
next_page_params = next_page |> next_page_params(tokens, params) |> delete_parameters_from_next_page_params()
conn
|> put_status(200)
|> render(:tokens, %{tokens: tokens, next_page_params: next_page_params})
end
end
def addresses_list(conn, params) do
{addresses, next_page} =
params
|> paging_options()
|> Chain.list_top_addresses()
|> split_list_by_page()
next_page_params = next_page_params(next_page, addresses, params)
exchange_rate = Market.get_exchange_rate(Explorer.coin()) || Token.null()
total_supply = Chain.total_supply()
conn
|> put_status(200)
|> render(:addresses, %{
addresses: addresses,
next_page_params: next_page_params,
exchange_rate: exchange_rate,
total_supply: total_supply
})
end
end

@ -21,4 +21,15 @@ defmodule BlockScoutWeb.API.V2.SearchController do
|> put_status(200)
|> render(:search_results, %{search_results: search_results, next_page_params: next_page_params})
end
def check_redirect(conn, %{"q" => query}) do
result =
query
|> String.trim()
|> BlockScoutWeb.Chain.from_param()
conn
|> put_status(200)
|> render(:search_results, %{result: result})
end
end

@ -0,0 +1,174 @@
defmodule BlockScoutWeb.API.V2.SmartContractController do
use BlockScoutWeb, :controller
import Explorer.SmartContract.Solidity.Verifier, only: [parse_boolean: 1]
alias BlockScoutWeb.{AccessHelpers, AddressView}
alias BlockScoutWeb.AddressContractVerificationController, as: VerificationController
alias Ecto.Association.NotLoaded
alias Explorer.Chain
alias Explorer.Chain.SmartContract
alias Explorer.SmartContract.{Reader, Writer}
@smart_contract_address_options [
necessity_by_association: %{
:contracts_creation_internal_transaction => :optional,
:smart_contract => :optional,
:contracts_creation_transaction => :optional
}
]
@burn_address "0x0000000000000000000000000000000000000000"
action_fallback(BlockScoutWeb.API.V2.FallbackController)
def smart_contract(conn, %{"address_hash" => address_hash_string} = params) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)},
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params),
_ <- VerificationController.check_and_verify(address_hash_string),
{:not_found, {:ok, address}} <-
{:not_found, Chain.find_contract_address(address_hash, @smart_contract_address_options, true)} do
conn
|> put_status(200)
|> render(:smart_contract, %{address: address})
end
end
def methods_read(conn, %{"address_hash" => address_hash_string, "is_custom_abi" => "true"} = params) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)},
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params),
custom_abi <- AddressView.fetch_custom_abi(conn, address_hash_string),
{:not_found, true} <- {:not_found, AddressView.check_custom_abi_for_having_read_functions(custom_abi)} do
read_only_functions_from_abi =
Reader.read_only_functions_from_abi_with_sender(custom_abi.abi, address_hash, params["from"])
read_functions_required_wallet_from_abi = Reader.read_functions_required_wallet_from_abi(custom_abi.abi)
conn
|> put_status(200)
|> render(:read_functions, %{functions: read_only_functions_from_abi ++ read_functions_required_wallet_from_abi})
end
end
def methods_read(conn, %{"address_hash" => address_hash_string} = params) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)},
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params),
smart_contract <- Chain.address_hash_to_smart_contract(address_hash),
{:not_found, false} <- {:not_found, is_nil(smart_contract)} do
read_only_functions_from_abi = Reader.read_only_functions(address_hash, params["from"])
read_functions_required_wallet_from_abi = Reader.read_functions_required_wallet(address_hash)
conn
|> put_status(200)
|> render(:read_functions, %{functions: read_only_functions_from_abi ++ read_functions_required_wallet_from_abi})
end
end
def methods_write(conn, %{"address_hash" => address_hash_string, "is_custom_abi" => "true"} = params) do
with {:format, {:ok, _address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)},
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params),
custom_abi <- AddressView.fetch_custom_abi(conn, address_hash_string),
{:not_found, true} <- {:not_found, AddressView.check_custom_abi_for_having_write_functions(custom_abi)} do
conn
|> put_status(200)
|> json(Writer.filter_write_functions(custom_abi.abi))
end
end
def methods_write(conn, %{"address_hash" => address_hash_string} = params) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)},
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params),
smart_contract <- Chain.address_hash_to_smart_contract(address_hash),
{:not_found, false} <- {:not_found, is_nil(smart_contract)} do
conn
|> put_status(200)
|> json(Writer.write_functions(address_hash))
end
end
def methods_read_proxy(conn, %{"address_hash" => address_hash_string} = params) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)},
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params),
{:not_found, {:ok, address}} <-
{:not_found, Chain.find_contract_address(address_hash, @smart_contract_address_options)},
{:not_found, false} <- {:not_found, is_nil(address.smart_contract)} do
implementation_address_hash_string =
address.smart_contract
|> SmartContract.get_implementation_address_hash()
|> Tuple.to_list()
|> List.first() || @burn_address
conn
|> put_status(200)
|> render(:read_functions, %{
functions: Reader.read_only_functions_proxy(address_hash, implementation_address_hash_string)
})
end
end
def methods_write_proxy(conn, %{"address_hash" => address_hash_string} = params) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)},
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params),
{:not_found, {:ok, address}} <-
{:not_found, Chain.find_contract_address(address_hash, @smart_contract_address_options)},
{:not_found, false} <- {:not_found, is_nil(address.smart_contract)} do
implementation_address_hash_string =
address.smart_contract
|> SmartContract.get_implementation_address_hash()
|> Tuple.to_list()
|> List.first() || @burn_address
conn
|> put_status(200)
|> json(Writer.write_functions_proxy(implementation_address_hash_string))
end
end
def query_read_method(
conn,
%{"address_hash" => address_hash_string, "contract_type" => type, "args" => args} = params
) do
custom_abi =
if parse_boolean(params["is_custom_abi"]), do: AddressView.fetch_custom_abi(conn, address_hash_string), else: nil
contract_type = if type == "proxy", do: :proxy, else: :regular
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)},
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params),
{:not_found, {:ok, address}} <-
{:not_found,
Chain.find_contract_address(address_hash,
necessity_by_association: %{
:smart_contract => :optional
}
)},
{:not_found, true} <-
{:not_found,
!is_nil(custom_abi) || (address.smart_contract && !match?(%NotLoaded{}, address.smart_contract))} do
%{output: output, names: names} =
if custom_abi do
Reader.query_function_with_names_custom_abi(
address_hash,
%{method_id: params["method_id"], args: prepare_args(args)},
params["from"],
custom_abi.abi
)
else
Reader.query_function_with_names(
address_hash,
%{method_id: params["method_id"], args: prepare_args(args)},
contract_type,
params["from"]
)
end
conn
|> put_status(200)
|> render(:function_response, %{output: output, names: names, contract_address_hash: address_hash})
end
end
def prepare_args(list) when is_list(list), do: list
def prepare_args(other), do: [other]
end

@ -3,10 +3,12 @@ defmodule BlockScoutWeb.API.V2.TokenController do
alias BlockScoutWeb.AccessHelpers
alias BlockScoutWeb.API.V2.TransactionView
alias Explorer.Chain
alias Explorer.{Chain, Market}
import BlockScoutWeb.Chain, only: [split_list_by_page: 1, paging_options: 1, next_page_params: 3]
import BlockScoutWeb.PagingHelper, only: [delete_parameters_from_next_page_params: 1]
import BlockScoutWeb.PagingHelper,
only: [delete_parameters_from_next_page_params: 1, token_transfers_types_options: 1]
action_fallback(BlockScoutWeb.API.V2.FallbackController)
@ -16,7 +18,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do
{:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash)} do
conn
|> put_status(200)
|> render(:token, %{token: token})
|> render(:token, %{token: Market.add_price(token)})
end
end
@ -32,7 +34,8 @@ defmodule BlockScoutWeb.API.V2.TokenController do
def transfers(conn, %{"address_hash" => address_hash_string} = params) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)},
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params) do
{:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params),
{:not_found, {:ok, _}} <- {:not_found, Chain.token_from_address_hash(address_hash)} do
results_plus_one = Chain.fetch_token_transfers_from_token_hash(address_hash, paging_options(params))
{token_transfers, next_page} = split_list_by_page(results_plus_one)
@ -65,4 +68,26 @@ defmodule BlockScoutWeb.API.V2.TokenController do
|> render(:token_balances, %{token_balances: token_balances, next_page_params: next_page_params, token: token})
end
end
def tokens_list(conn, params) do
filter =
if Map.has_key?(params, "filter") do
Map.get(params, "filter")
else
nil
end
paging_params =
params
|> paging_options()
|> Keyword.merge(token_transfers_types_options(params))
{tokens, next_page} = filter |> Chain.list_top_tokens(paging_params) |> Market.add_price() |> split_list_by_page()
next_page_params = next_page |> next_page_params(tokens, params) |> delete_parameters_from_next_page_params()
conn
|> put_status(200)
|> render(:tokens, %{tokens: tokens, next_page_params: next_page_params})
end
end

@ -44,7 +44,7 @@ defmodule BlockScoutWeb.SmartContractController do
if contract_type == "proxy" do
Reader.read_only_functions_proxy(address_hash, implementation_address_hash_string)
else
Reader.read_only_functions(address_hash)
Reader.read_only_functions(address_hash, params["from"])
end
end
@ -101,7 +101,7 @@ defmodule BlockScoutWeb.SmartContractController do
def index(conn, _), do: not_found(conn)
defp custom_abi_render(conn, %{"hash" => address_hash_string, "type" => contract_type, "action" => action}) do
defp custom_abi_render(conn, %{"hash" => address_hash_string, "type" => contract_type, "action" => action} = params) do
with custom_abi <- AddressView.fetch_custom_abi(conn, address_hash_string),
false <- is_nil(custom_abi),
abi <- custom_abi.abi,
@ -110,7 +110,7 @@ defmodule BlockScoutWeb.SmartContractController do
if action == "write" do
Writer.filter_write_functions(abi)
else
Reader.read_only_functions_from_abi(abi, address_hash)
Reader.read_only_functions_from_abi_with_sender(abi, address_hash, params["from"])
end
read_functions_required_wallet =

@ -133,6 +133,7 @@ defmodule BlockScoutWeb.PagingHelper do
|> Map.delete("type")
|> Map.delete("method")
|> Map.delete("filter")
|> Map.delete("token_address_hash")
end
def delete_parameters_from_next_page_params(_), do: nil

@ -0,0 +1,31 @@
# This file in ignore list of `sobelow`, be careful while adding new endpoints here
defmodule BlockScoutWeb.SmartContractsApiV2Router do
@moduledoc """
Router for /api/v2/smart-contracts. This route has separate router in order to ignore solelow's warning about missing CSRF protection
"""
use BlockScoutWeb, :router
alias BlockScoutWeb.Plug.CheckApiV2
pipeline :api do
plug(:accepts, ["json"])
end
pipeline :api_v2_no_forgery_protect do
plug(CheckApiV2)
plug(:fetch_session)
end
scope "/", as: :api_v2 do
pipe_through(:api)
pipe_through(:api_v2_no_forgery_protect)
alias BlockScoutWeb.API.V2
get("/:address_hash", V2.SmartContractController, :smart_contract)
get("/:address_hash/methods-read", V2.SmartContractController, :methods_read)
get("/:address_hash/methods-write", V2.SmartContractController, :methods_write)
get("/:address_hash/methods-read-proxy", V2.SmartContractController, :methods_read_proxy)
get("/:address_hash/methods-write-proxy", V2.SmartContractController, :methods_write_proxy)
post("/:address_hash/query-read-method", V2.SmartContractController, :query_read_method)
end
end

@ -207,7 +207,8 @@ defmodule BlockScoutWeb.ABIEncodedValueView do
hex(value)
end
defp base_value_json(_, value), do: value
defp base_value_json(_, value), do: to_string(value)
defp hex("0x" <> value), do: "0x" <> value
defp hex(value), do: "0x" <> Base.encode16(value, case: :lower)
end

@ -83,11 +83,11 @@ defmodule BlockScoutWeb.AddressContractView do
end
end
defp decode_data("0x" <> encoded_data, types) do
def decode_data("0x" <> encoded_data, types) do
decode_data(encoded_data, types)
end
defp decode_data(encoded_data, types) do
def decode_data(encoded_data, types) do
encoded_data
|> Base.decode16!(case: :mixed)
|> TypeDecoder.decode_raw(types)

@ -32,6 +32,31 @@ defmodule BlockScoutWeb.API.V2.AddressView do
Enum.map(coin_balances_by_day, &prepare_coin_balance_history_by_day_entry/1)
end
def render("tokens.json", %{tokens: tokens, next_page_params: next_page_params}) do
%{"items" => Enum.map(tokens, &prepare_token_balance/1), "next_page_params" => next_page_params}
end
def render("addresses.json", %{
addresses: addresses,
next_page_params: next_page_params,
exchange_rate: exchange_rate,
total_supply: total_supply
}) do
%{
items: Enum.map(addresses, &prepare_address/1),
next_page_params: next_page_params,
exchange_rate: exchange_rate.usd_value,
total_supply: total_supply && to_string(total_supply)
}
end
def prepare_address({address, nonce}) do
nil
|> Helper.address_with_info(address, address.hash)
|> Map.put(:tx_count, to_string(nonce))
|> Map.put(:coin_balance, if(address.fetched_coin_balance, do: address.fetched_coin_balance.value))
end
def prepare_address(address, conn \\ nil) do
base_info = Helper.address_with_info(conn, address, address.hash)
is_proxy = AddressView.smart_contract_is_proxy?(address)
@ -55,6 +80,9 @@ defmodule BlockScoutWeb.API.V2.AddressView do
creation_tx = creator_hash && AddressView.transaction_hash(address)
token = address.token && TokenView.render("token.json", %{token: Market.add_price(address.token)})
write_custom_abi? = AddressView.has_address_custom_abi_with_write_functions?(conn, address.hash)
read_custom_abi? = AddressView.has_address_custom_abi_with_read_functions?(conn, address.hash)
Map.merge(base_info, %{
"creator_address_hash" => creator_hash && Address.checksum(creator_hash),
"creation_tx_hash" => creation_tx,
@ -63,7 +91,18 @@ defmodule BlockScoutWeb.API.V2.AddressView do
"exchange_rate" => exchange_rate,
"implementation_name" => implementation_name,
"implementation_address" => implementation_address,
"block_number_balance_updated_at" => address.fetched_coin_balance_block_number
"block_number_balance_updated_at" => address.fetched_coin_balance_block_number,
"has_custom_methods_read" => read_custom_abi?,
"has_custom_methods_write" => write_custom_abi?,
"has_methods_read" => AddressView.smart_contract_with_read_only_functions?(address) || read_custom_abi?,
"has_methods_write" => AddressView.smart_contract_with_write_functions?(address) || write_custom_abi?,
"has_methods_read_proxy" => is_proxy,
"has_methods_write_proxy" => AddressView.smart_contract_with_write_functions?(address) && is_proxy,
"has_decompiled_code" => AddressView.has_decompiled_code?(address),
"has_validated_blocks" => Chain.check_if_validated_blocks_at_address(address.hash),
"has_logs" => Chain.check_if_logs_at_address(address.hash),
"has_tokens" => Chain.check_if_tokens_at_address(address.hash),
"has_token_transfers" => Chain.check_if_token_transfers_at_address(address.hash)
})
end

@ -81,6 +81,7 @@ defmodule BlockScoutWeb.API.V2.Helper do
def is_smart_contract(_), do: false
def is_verified(%Address{smart_contract: nil}), do: false
def is_verified(%Address{smart_contract: %{metadata_from_verified_twin: true}}), do: false
def is_verified(%Address{smart_contract: %NotLoaded{}}), do: nil
def is_verified(%Address{smart_contract: _}), do: true

@ -2,11 +2,20 @@ defmodule BlockScoutWeb.API.V2.SearchView do
use BlockScoutWeb, :view
alias BlockScoutWeb.Endpoint
alias Explorer.Chain.{Address, Block, Transaction}
def render("search_results.json", %{search_results: search_results, next_page_params: next_page_params}) do
%{"items" => Enum.map(search_results, &prepare_search_result/1), "next_page_params" => next_page_params}
end
def render("search_results.json", %{result: {:ok, result}}) do
Map.merge(%{"redirect" => true}, redirect_search_results(result))
end
def render("search_results.json", %{result: {:error, :not_found}}) do
%{"redirect" => false, "type" => nil, "parameter" => nil}
end
def prepare_search_result(%{type: "token"} = search_result) do
%{
"type" => search_result.type,
@ -50,4 +59,16 @@ defmodule BlockScoutWeb.API.V2.SearchView do
end
defp hash_to_string(hash), do: "0x" <> Base.encode16(hash, case: :lower)
defp redirect_search_results(%Address{} = item) do
%{"type" => "address", "parameter" => Address.checksum(item.hash)}
end
defp redirect_search_results(%Block{} = item) do
%{"type" => "block", "parameter" => to_string(item.hash)}
end
defp redirect_search_results(%Transaction{} = item) do
%{"type" => "transaction", "parameter" => to_string(item.hash)}
end
end

@ -0,0 +1,231 @@
defmodule BlockScoutWeb.API.V2.SmartContractView do
use BlockScoutWeb, :view
alias ABI.FunctionSelector
alias BlockScoutWeb.API.V2.TransactionView
alias BlockScoutWeb.SmartContractView
alias BlockScoutWeb.{ABIEncodedValueView, AddressContractView, AddressView}
alias Explorer.Chain
alias Explorer.Chain.{Address, SmartContract}
alias Explorer.Visualize.Sol2uml
require Logger
def render("smart_contract.json", %{address: address}) do
prepare_smart_contract(address)
end
def render("read_functions.json", %{functions: functions}) do
Enum.map(functions, &prepare_read_function/1)
end
def render("function_response.json", %{output: output, names: names, contract_address_hash: contract_address_hash}) do
prepare_function_response(output, names, contract_address_hash)
end
def prepare_function_response(outputs, names, contract_address_hash) do
case outputs do
{:error, %{code: code, message: message, data: data}} ->
revert_reason = Chain.format_revert_reason_message(data)
case SmartContractView.decode_revert_reason(contract_address_hash, revert_reason) do
{:ok, method_id, text, mapping} ->
%{
result:
render(TransactionView, "decoded_input.json",
method_id: method_id,
text: text,
mapping: mapping,
error?: true
),
is_error: true
}
{:error, _contract_verified, []} ->
%{
result:
Map.merge(render(TransactionView, "revert_reason.json", raw: revert_reason), %{
code: code,
message: message
}),
is_error: true
}
{:error, _contract_verified, candidates} ->
{:ok, method_id, text, mapping} = Enum.at(candidates, 0)
%{
result:
render(TransactionView, "decoded_input.json",
method_id: method_id,
text: text,
mapping: mapping,
error?: true
),
is_error: true
}
_ ->
%{
result:
Map.merge(render(TransactionView, "revert_reason.json", raw: revert_reason), %{
code: code,
message: message
}),
is_error: true
}
end
{:error, %{code: code, message: message}} ->
%{result: %{code: code, message: message}, is_error: true}
{:error, error} ->
%{result: %{error: error}, is_error: true}
_ ->
%{result: %{output: outputs, names: names}, is_error: false}
end
end
def prepare_read_function(function) do
case function["outputs"] do
{:error, text_error} ->
function
|> Map.put("error", text_error)
|> Map.replace("outputs", function["abi_outputs"])
|> Map.drop(["abi_outputs"])
_ ->
result =
function
|> Map.drop(["abi_outputs"])
outputs = Enum.map(result["outputs"], &prepare_output/1)
Map.replace(result, "outputs", outputs)
end
end
defp prepare_output(%{"type" => type, "value" => value} = output) do
Map.replace(output, "value", ABIEncodedValueView.value_json(type, value))
end
defp prepare_output(output), do: output
# credo:disable-for-next-line
def prepare_smart_contract(%Address{smart_contract: %SmartContract{}} = address) do
minimal_proxy_template = Chain.get_minimal_proxy_template(address.hash)
metadata_for_verification =
minimal_proxy_template || Chain.get_address_verified_twin_contract(address.hash).verified_contract
smart_contract_verified = AddressView.smart_contract_verified?(address)
additional_sources_from_twin = Chain.get_address_verified_twin_contract(address.hash).additional_sources
fully_verified = Chain.smart_contract_fully_verified?(address.hash)
additional_sources =
if smart_contract_verified, do: address.smart_contract_additional_sources, else: additional_sources_from_twin
visualize_sol2uml_enabled = Sol2uml.enabled?()
target_contract = if smart_contract_verified, do: address.smart_contract, else: metadata_for_verification
%{
"verified_twin_address_hash" =>
metadata_for_verification && Address.checksum(metadata_for_verification.address_hash),
"is_verified" => smart_contract_verified,
"is_changed_bytecode" => smart_contract_verified && address.smart_contract.is_changed_bytecode,
"is_partially_verified" => address.smart_contract.partially_verified && smart_contract_verified,
"is_fully_verified" => fully_verified,
"is_verified_via_sourcify" => address.smart_contract.verified_via_sourcify && smart_contract_verified,
"is_vyper_contract" => target_contract.is_vyper_contract,
"minimal_proxy_address_hash" =>
minimal_proxy_template && Address.checksum(metadata_for_verification.address_hash),
"sourcify_repo_url" =>
if(address.smart_contract.verified_via_sourcify && smart_contract_verified,
do: AddressContractView.sourcify_repo_url(address.hash, address.smart_contract.partially_verified)
),
"can_be_visualized_via_sol2uml" =>
visualize_sol2uml_enabled && !target_contract.is_vyper_contract && !is_nil(target_contract.abi),
"name" => target_contract && target_contract.name,
"compiler_version" => target_contract.compiler_version,
"optimization_enabled" => if(target_contract.is_vyper_contract, do: nil, else: target_contract.optimization),
"optimization_runs" => target_contract.optimization_runs,
"evm_version" => target_contract.evm_version,
"verified_at" => target_contract.inserted_at,
"abi" => target_contract.abi,
"source_code" => target_contract.contract_source_code,
"file_path" => target_contract.file_path,
"additional_sources" => Enum.map(additional_sources, &prepare_additional_sourse/1),
"compiler_settings" => target_contract.compiler_settings,
"external_libraries" => prepare_external_libraries(target_contract.external_libraries),
"constructor_args" => if(smart_contract_verified, do: target_contract.constructor_arguments),
"decoded_constructor_args" =>
if(smart_contract_verified,
do: format_constructor_arguments(target_contract.abi, target_contract.constructor_arguments)
)
}
|> Map.merge(bytecode_info(address))
end
def prepare_smart_contract(address) do
bytecode_info(address)
end
defp bytecode_info(address) do
case AddressContractView.contract_creation_code(address) do
{:selfdestructed, init} ->
%{
"is_self_destructed" => true,
"deployed_bytecode" => nil,
"creation_bytecode" => init
}
{:ok, contract_code} ->
%{
"is_self_destructed" => false,
"deployed_bytecode" => contract_code,
"creation_bytecode" => AddressContractView.creation_code(address)
}
end
end
defp prepare_external_libraries(libraries) when is_list(libraries) do
Enum.map(libraries, fn %Explorer.Chain.SmartContract.ExternalLibrary{name: name, address_hash: address_hash} ->
{:ok, hash} = Chain.string_to_address_hash(address_hash)
%{name: name, address_hash: Address.checksum(hash)}
end)
end
defp prepare_additional_sourse(source) do
%{
"source_code" => source.contract_source_code,
"file_path" => source.file_name
}
end
def format_constructor_arguments(abi, constructor_arguments) do
constructor_abi = Enum.find(abi, fn el -> el["type"] == "constructor" && el["inputs"] != [] end)
input_types = Enum.map(constructor_abi["inputs"], &FunctionSelector.parse_specification_type/1)
result =
constructor_arguments
|> AddressContractView.decode_data(input_types)
|> Enum.zip(constructor_abi["inputs"])
|> Enum.map(fn {value, %{"type" => type} = input_arg} ->
[ABIEncodedValueView.value_json(type, value), input_arg]
end)
result
rescue
exception ->
Logger.warn(fn ->
[
"Error formating constructor arguments for abi: #{inspect(abi)}, args: #{inspect(constructor_arguments)}: ",
Exception.format(:error, exception)
]
end)
nil
end
end

@ -27,6 +27,10 @@ defmodule BlockScoutWeb.API.V2.TokenView do
}
end
def render("tokens.json", %{tokens: tokens, next_page_params: next_page_params}) do
%{"items" => Enum.map(tokens, &render("token.json", %{token: &1})), "next_page_params" => next_page_params}
end
def exchange_rate(%{usd_value: usd_value}) when not is_nil(usd_value), do: to_string(usd_value)
def exchange_rate(_), do: nil

@ -86,6 +86,8 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
end
def prepare_token_transfer(token_transfer, conn) do
decoded_input = token_transfer.transaction |> Transaction.decoded_input_data() |> format_decoded_input()
%{
"tx_hash" => token_transfer.transaction_hash,
"from" => Helper.address_with_info(conn, token_transfer.from_address, token_transfer.from_address_hash),
@ -93,7 +95,14 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
"total" => prepare_token_transfer_total(token_transfer),
"token" => TokenView.render("token.json", %{token: Market.add_price(token_transfer.token)}),
"type" => Chain.get_token_transfer_type(token_transfer),
"timestamp" => block_timestamp(token_transfer.block)
"timestamp" =>
if(match?(%NotLoaded{}, token_transfer.block),
do: block_timestamp(token_transfer.transaction),
else: block_timestamp(token_transfer.block)
),
"method" => method_name(token_transfer.transaction, decoded_input, true),
"block_hash" => to_string(token_transfer.block_hash),
"log_index" => to_string(token_transfer.log_index)
}
end
@ -157,6 +166,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
decoded = decode_log(log, transaction_or_hash)
%{
"tx_hash" => get_tx_hash(transaction_or_hash),
"address" => Helper.address_with_info(log.address, log.address_hash),
"topics" => [
log.first_topic,
@ -171,6 +181,9 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
}
end
defp get_tx_hash(%Transaction{} = tx), do: to_string(tx.hash)
defp get_tx_hash(hash), do: to_string(hash)
defp smart_contract_info(%Transaction{} = tx), do: Helper.address_with_info(tx.to_address, tx.to_address_hash)
defp smart_contract_info(_), do: nil
@ -390,19 +403,25 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
|> Timex.diff(right, :milliseconds)
end
defp method_name(_, {:ok, _method_id, text, _mapping}) do
defp method_name(_, _, skip_sc_check? \\ false)
defp method_name(_, {:ok, _method_id, text, _mapping}, _) do
Transaction.parse_method_name(text, false)
end
defp method_name(%Transaction{to_address: to_address, input: %{bytes: <<method_id::binary-size(4), _::binary>>}}, _) do
if Helper.is_smart_contract(to_address) do
defp method_name(
%Transaction{to_address: to_address, input: %{bytes: <<method_id::binary-size(4), _::binary>>}},
_,
skip_sc_check?
) do
if Helper.is_smart_contract(to_address) || skip_sc_check? do
"0x" <> Base.encode16(method_id, case: :lower)
else
nil
end
end
defp method_name(_, _) do
defp method_name(_, _, _) do
nil
end
@ -464,6 +483,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
end
end
defp block_timestamp(%Transaction{block: %Block{} = block}), do: block.timestamp
defp block_timestamp(%Block{} = block), do: block.timestamp
defp block_timestamp(_), do: nil
end

@ -13,7 +13,13 @@ defmodule BlockScoutWeb.CurrencyHelpers do
iex> BlockScoutWeb.CurrencyHelpers.format_integer_to_currency(1000000)
"1,000,000"
"""
@spec format_integer_to_currency(non_neg_integer()) :: String.t()
@spec format_integer_to_currency(non_neg_integer() | nil) :: String.t()
def format_integer_to_currency(value)
def format_integer_to_currency(nil) do
"-"
end
def format_integer_to_currency(value) do
{:ok, formatted} = Number.to_string(value, format: "#,##0")

@ -23,7 +23,7 @@ defmodule BlockScoutWeb.Mixfile do
dialyzer: :test
],
start_permanent: Mix.env() == :prod,
version: "4.1.8"
version: "5.0.0"
]
end
@ -83,7 +83,7 @@ defmodule BlockScoutWeb.Mixfile do
# HTML CSS selectors for Phoenix controller tests
{:floki, "~> 0.31"},
{:flow, "~> 1.2"},
{:gettext, "~> 0.20.0"},
{:gettext, "~> 0.21.0"},
{:hammer, "~> 6.0"},
{:httpoison, "~> 1.6"},
{:indexer, in_umbrella: true, runtime: false},

@ -7,6 +7,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
Address,
Address.CoinBalance,
Block,
Data,
InternalTransaction,
Log,
Token,
@ -36,7 +37,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
correct_reponse = %{
"hash" => Address.checksum(address.hash),
"implementation_name" => nil,
"is_contract" => false,
"is_verified" => false,
"name" => nil,
@ -50,7 +50,18 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
"exchange_rate" => nil,
"implementation_name" => nil,
"implementation_address" => nil,
"block_number_balance_updated_at" => nil
"block_number_balance_updated_at" => nil,
"has_custom_methods_read" => false,
"has_custom_methods_write" => false,
"has_methods_read" => false,
"has_methods_write" => false,
"has_methods_read_proxy" => false,
"has_methods_write_proxy" => false,
"has_decompiled_code" => false,
"has_validated_blocks" => false,
"has_logs" => false,
"has_tokens" => false,
"has_token_transfers" => false
}
request = get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}")
@ -135,9 +146,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
request = get(conn, "/api/v2/addresses/#{address.hash}/transactions")
assert response = json_response(request, 200)
assert response["items"] == []
assert response["next_page_params"] == nil
assert %{"message" => "Not found"} = json_response(request, 404)
end
test "get 422 on invalid address", %{conn: conn} do
@ -331,14 +340,12 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
end
describe "/addresses/{address_hash}/token-transfers" do
test "get empty list on non existing address", %{conn: conn} do
test "get 404 on non existing address", %{conn: conn} do
address = build(:address)
request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers")
assert response = json_response(request, 200)
assert response["items"] == []
assert response["next_page_params"] == nil
assert %{"message" => "Not found"} = json_response(request, 404)
end
test "get 422 on invalid address", %{conn: conn} do
@ -347,10 +354,28 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
end
test "get 404 on non existing address of token", %{conn: conn} do
address = insert(:address)
token = build(:address)
request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", %{"token" => to_string(token.hash)})
assert %{"message" => "Not found"} = json_response(request, 404)
end
test "get 422 on invalid token address hash", %{conn: conn} do
address = insert(:address)
request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", %{"token" => "0x"})
assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
end
test "get relevant token transfer", %{conn: conn} do
address = insert(:address)
tx = insert(:transaction) |> with_block()
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number)
@ -365,10 +390,115 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
compare_item(token_transfer, Enum.at(response["items"], 0))
end
test "method in token transer could be decoded", %{conn: conn} do
insert(:contract_method,
identifier: Base.decode16!("731133e9", case: :lower),
abi: %{
"constant" => false,
"inputs" => [
%{"name" => "account", "type" => "address"},
%{"name" => "id", "type" => "uint256"},
%{"name" => "amount", "type" => "uint256"},
%{"name" => "data", "type" => "bytes"}
],
"name" => "mint",
"outputs" => [],
"payable" => false,
"stateMutability" => "nonpayable",
"type" => "function"
}
)
address = insert(:address)
tx =
insert(:transaction,
input:
"0x731133e9000000000000000000000000bb36c792b9b45aaf8b848a1392b0d6559202729e000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001700000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000"
)
|> with_block()
insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number)
token_transfer =
insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address)
request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers")
assert response = json_response(request, 200)
assert Enum.count(response["items"]) == 1
assert response["next_page_params"] == nil
compare_item(token_transfer, Enum.at(response["items"], 0))
assert Enum.at(response["items"], 0)["method"] == "mint"
end
test "get relevant token transfer filtered by token", %{conn: conn} do
token = insert(:token)
address = insert(:address)
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number)
insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address)
token_transfer =
insert(:token_transfer,
transaction: tx,
block: tx.block,
block_number: tx.block_number,
from_address: address,
token_contract_address: token.contract_address
)
request =
get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", %{
"token" => to_string(token.contract_address)
})
assert response = json_response(request, 200)
assert Enum.count(response["items"]) == 1
assert response["next_page_params"] == nil
compare_item(token_transfer, Enum.at(response["items"], 0))
end
test "token transfers by token can paginate", %{conn: conn} do
address = insert(:address)
token = insert(:token)
token_tranfers =
for _ <- 0..50 do
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address)
insert(:token_transfer,
transaction: tx,
block: tx.block,
block_number: tx.block_number,
from_address: address,
token_contract_address: token.contract_address
)
end
params = %{"token" => to_string(token.contract_address)}
request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", params)
assert response = json_response(request, 200)
request_2nd_page =
get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(params, response["next_page_params"]))
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, token_tranfers)
end
test "get only :to token transfer", %{conn: conn} do
address = insert(:address)
tx = insert(:transaction) |> with_block()
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address)
@ -386,7 +516,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
test "get only :from token transfer", %{conn: conn} do
address = insert(:address)
tx = insert(:transaction) |> with_block()
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
token_transfer =
insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address)
@ -405,7 +535,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
token_tranfers =
for _ <- 0..50 do
tx = insert(:transaction) |> with_block()
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address)
end
@ -423,13 +553,13 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
address = insert(:address)
for _ <- 0..50 do
tx = insert(:transaction) |> with_block()
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address)
end
token_tranfers =
for _ <- 0..50 do
tx = insert(:transaction) |> with_block()
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, to_address: address)
end
@ -450,13 +580,13 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
token_tranfers =
for _ <- 0..50 do
tx = insert(:transaction) |> with_block()
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address)
end
for _ <- 0..50 do
tx = insert(:transaction) |> with_block()
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, to_address: address)
end
@ -477,14 +607,14 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
tt_from =
for _ <- 0..49 do
tx = insert(:transaction) |> with_block()
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address)
end
tt_to =
for _ <- 0..50 do
tx = insert(:transaction) |> with_block()
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, to_address: address)
end
@ -518,7 +648,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
erc_20_tt =
for _ <- 0..50 do
tx = insert(:transaction) |> with_block()
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
insert(:token_transfer,
transaction: tx,
@ -533,7 +663,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
erc_721_tt =
for x <- 0..50 do
tx = insert(:transaction) |> with_block()
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
insert(:token_transfer,
transaction: tx,
@ -549,7 +679,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
erc_1155_tt =
for x <- 0..50 do
tx = insert(:transaction) |> with_block()
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
insert(:token_transfer,
transaction: tx,
@ -643,7 +773,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
erc_20_tt =
for _ <- 0..50 do
tx = insert(:transaction) |> with_block()
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
insert(:token_transfer,
transaction: tx,
@ -658,7 +788,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
erc_721_tt =
for x <- 0..50 do
tx = insert(:transaction) |> with_block()
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
insert(:token_transfer,
transaction: tx,
@ -717,9 +847,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions")
assert response = json_response(request, 200)
assert response["items"] == []
assert response["next_page_params"] == nil
assert %{"message" => "Not found"} = json_response(request, 404)
end
test "get 422 on invalid address", %{conn: conn} do
@ -864,9 +992,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
request = get(conn, "/api/v2/addresses/#{address.hash}/blocks-validated")
assert response = json_response(request, 200)
assert response["items"] == []
assert response["next_page_params"] == nil
assert %{"message" => "Not found"} = json_response(request, 404)
end
test "get 422 on invalid address", %{conn: conn} do
@ -910,8 +1036,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
request = get(conn, "/api/v2/addresses/#{address.hash}/token-balances")
assert response = json_response(request, 200)
assert response == []
assert %{"message" => "Not found"} = json_response(request, 404)
end
test "get 422 on invalid address", %{conn: conn} do
@ -945,9 +1070,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
request = get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history")
assert response = json_response(request, 200)
assert response["items"] == []
assert response["next_page_params"] == nil
assert %{"message" => "Not found"} = json_response(request, 404)
end
test "get 422 on invalid address", %{conn: conn} do
@ -994,8 +1117,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
request = get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history-by-day")
assert response = json_response(request, 200)
assert response == []
assert %{"message" => "Not found"} = json_response(request, 404)
end
test "get 422 on invalid address", %{conn: conn} do
@ -1032,9 +1154,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
request = get(conn, "/api/v2/addresses/#{address.hash}/logs")
assert response = json_response(request, 200)
assert response["items"] == []
assert response["next_page_params"] == nil
assert %{"message" => "Not found"} = json_response(request, 404)
end
test "get 422 on invalid address", %{conn: conn} do
@ -1136,6 +1256,129 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
end
end
describe "/addresses/{address_hash}/tokens" do
test "get empty list on non existing address", %{conn: conn} do
address = build(:address)
request = get(conn, "/api/v2/addresses/#{address.hash}/tokens")
assert %{"message" => "Not found"} = json_response(request, 404)
end
test "get 422 on invalid address", %{conn: conn} do
request = get(conn, "/api/v2/addresses/0x/tokens")
assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
end
test "get tokens", %{conn: conn} do
address = insert(:address)
ctbs_erc_20 =
for _ <- 0..50 do
insert(:address_current_token_balance_with_token_id, address: address, token_type: "ERC-20", token_id: nil)
|> Repo.preload([:token])
end
|> Enum.sort_by(fn x -> x.value end, :asc)
ctbs_erc_721 =
for _ <- 0..50 do
insert(:address_current_token_balance_with_token_id, address: address, token_type: "ERC-721", token_id: nil)
|> Repo.preload([:token])
end
|> Enum.sort_by(fn x -> x.value end, :asc)
ctbs_erc_1155 =
for _ <- 0..50 do
insert(:address_current_token_balance_with_token_id,
address: address,
token_type: "ERC-1155",
token_id: Enum.random(1..100_000)
)
|> Repo.preload([:token])
end
|> Enum.sort_by(fn x -> x.value end, :asc)
filter = %{"type" => "ERC-20"}
request = get(conn, "/api/v2/addresses/#{address.hash}/tokens", filter)
assert response = json_response(request, 200)
request_2nd_page =
get(conn, "/api/v2/addresses/#{address.hash}/tokens", Map.merge(response["next_page_params"], filter))
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, ctbs_erc_20)
filter = %{"type" => "ERC-721"}
request = get(conn, "/api/v2/addresses/#{address.hash}/tokens", filter)
assert response = json_response(request, 200)
request_2nd_page =
get(conn, "/api/v2/addresses/#{address.hash}/tokens", Map.merge(response["next_page_params"], filter))
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, ctbs_erc_721)
filter = %{"type" => "ERC-1155"}
request = get(conn, "/api/v2/addresses/#{address.hash}/tokens", filter)
assert response = json_response(request, 200)
request_2nd_page =
get(conn, "/api/v2/addresses/#{address.hash}/tokens", Map.merge(response["next_page_params"], filter))
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, ctbs_erc_1155)
end
end
describe "/addresses" do
test "get empty list", %{conn: conn} do
request = get(conn, "/api/v2/addresses")
total_supply = to_string(Chain.total_supply())
assert %{"items" => [], "next_page_params" => nil, "exchange_rate" => nil, "total_supply" => ^total_supply} =
json_response(request, 200)
end
test "check pagination", %{conn: conn} do
addresses =
for i <- 0..50 do
insert(:address, nonce: i, fetched_coin_balance: i + 1)
end
request = get(conn, "/api/v2/addresses")
assert response = json_response(request, 200)
request_2nd_page = get(conn, "/api/v2/addresses", response["next_page_params"])
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, addresses)
assert Enum.at(response["items"], 0)["coin_balance"] ==
to_string(Enum.at(addresses, 50).fetched_coin_balance.value)
end
test "check nil", %{conn: conn} do
address = insert(:address, nonce: 1, fetched_coin_balance: 1)
request = get(conn, "/api/v2/addresses")
assert %{"items" => [address_json], "next_page_params" => nil} = json_response(request, 200)
compare_item(address, address_json)
end
end
defp compare_item(%Address{} = address, json) do
assert Address.checksum(address.hash) == json["hash"]
assert to_string(address.nonce + 1) == json["tx_count"]
end
defp compare_item(%Transaction{} = transaction, json) do
assert to_string(transaction.hash) == json["hash"]
assert transaction.block_number == json["block"]
@ -1148,6 +1391,10 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
assert Address.checksum(token_transfer.from_address_hash) == json["from"]["hash"]
assert Address.checksum(token_transfer.to_address_hash) == json["to"]["hash"]
assert to_string(token_transfer.transaction_hash) == json["tx_hash"]
assert json["timestamp"] != nil
assert json["method"] != nil
assert to_string(token_transfer.block_hash) == json["block_hash"]
assert to_string(token_transfer.log_index) == json["log_index"]
end
defp compare_item(%InternalTransaction{} = internal_tx, json) do
@ -1189,9 +1436,10 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
end
defp compare_item(%Log{} = log, json) do
assert to_string(log.data) == json["data"]
assert log.index == json["index"]
assert to_string(log.data) == json["data"]
assert Address.checksum(log.address_hash) == json["address"]["hash"]
assert to_string(log.transaction_hash) == json["tx_hash"]
end
defp check_paginated_response(first_page_resp, second_page_resp, list) do

@ -1,7 +1,7 @@
defmodule BlockScoutWeb.API.V2.SearchControllerTest do
use BlockScoutWeb.ConnCase
alias Explorer.Chain.Address
alias Explorer.Chain.{Address, Block}
setup do
insert(:block)
@ -144,4 +144,102 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do
assert item["url"] =~ to_string(tx.hash)
end
end
describe "/search/check-redirect" do
test "finds a consensus block by block number", %{conn: conn} do
block = insert(:block)
hash = to_string(block.hash)
request = get(conn, "/api/v2/search/check-redirect?q=#{block.number}")
assert %{"redirect" => true, "type" => "block", "parameter" => ^hash} = json_response(request, 200)
end
test "redirects to search results page even for searching non-consensus block by number", %{conn: conn} do
%Block{number: number} = insert(:block, consensus: false)
request = get(conn, "/api/v2/search/check-redirect?q=#{number}")
%{"redirect" => false, "type" => nil, "parameter" => nil} = json_response(request, 200)
end
test "finds non-consensus block by hash", %{conn: conn} do
%Block{hash: hash} = insert(:block, consensus: false)
conn = get(conn, "/search?q=#{hash}")
hash = to_string(hash)
request = get(conn, "/api/v2/search/check-redirect?q=#{hash}")
assert %{"redirect" => true, "type" => "block", "parameter" => ^hash} = json_response(request, 200)
end
test "finds a transaction by hash", %{conn: conn} do
transaction =
:transaction
|> insert()
|> with_block()
hash = to_string(transaction.hash)
request = get(conn, "/api/v2/search/check-redirect?q=#{hash}")
assert %{"redirect" => true, "type" => "transaction", "parameter" => ^hash} = json_response(request, 200)
end
test "finds a transaction by hash when there are not 0x prefix", %{conn: conn} do
transaction =
:transaction
|> insert()
|> with_block()
hash = to_string(transaction.hash)
"0x" <> non_prefix_hash = to_string(transaction.hash)
request = get(conn, "/api/v2/search/check-redirect?q=#{non_prefix_hash}")
assert %{"redirect" => true, "type" => "transaction", "parameter" => ^hash} = json_response(request, 200)
end
test "finds an address by hash", %{conn: conn} do
address = insert(:address)
hash = Address.checksum(address.hash)
request = get(conn, "/api/v2/search/check-redirect?q=#{to_string(address.hash)}")
assert %{"redirect" => true, "type" => "address", "parameter" => ^hash} = json_response(request, 200)
end
test "finds an address by hash when there are extra spaces", %{conn: conn} do
address = insert(:address)
hash = Address.checksum(address.hash)
request = get(conn, "/api/v2/search/check-redirect?q=#{to_string(address.hash)} ")
assert %{"redirect" => true, "type" => "address", "parameter" => ^hash} = json_response(request, 200)
end
test "finds an address by hash when there are not 0x prefix", %{conn: conn} do
address = insert(:address)
"0x" <> non_prefix_hash = to_string(address.hash)
hash = Address.checksum(address.hash)
request = get(conn, "/api/v2/search/check-redirect?q=#{non_prefix_hash}")
assert %{"redirect" => true, "type" => "address", "parameter" => ^hash} = json_response(request, 200)
end
test "redirects to result page when it finds nothing", %{conn: conn} do
request = get(conn, "/api/v2/search/check-redirect?q=qwerty")
%{"redirect" => false, "type" => nil, "parameter" => nil} = json_response(request, 200)
end
end
end

@ -75,7 +75,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do
token_contract_address: contract_token_address
)
second_page_token_balances =
_second_page_token_balances =
1..5
|> Enum.map(
&insert(
@ -100,7 +100,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do
request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers")
assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200)
assert %{"message" => "Not found"} = json_response(request, 404)
end
test "get 422 on invalid address", %{conn: conn} do
@ -122,7 +122,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do
token_tranfers =
for _ <- 0..50 do
tx = insert(:transaction) |> with_block()
tx = insert(:transaction, input: "0xabcd010203040506") |> with_block()
insert(:token_transfer,
transaction: tx,
@ -191,13 +191,50 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do
end
end
describe "/tokens" do
test "get empty list", %{conn: conn} do
request = get(conn, "/api/v2/tokens")
assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200)
end
test "check pagination", %{conn: conn} do
tokens =
for i <- 0..50 do
insert(:token, holder_count: i)
end
request = get(conn, "/api/v2/tokens")
assert response = json_response(request, 200)
request_2nd_page = get(conn, "/api/v2/tokens", response["next_page_params"])
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, tokens)
end
test "check nil", %{conn: conn} do
token = insert(:token)
request = get(conn, "/api/v2/tokens")
assert %{"items" => [token_json], "next_page_params" => nil} = json_response(request, 200)
compare_item(token, token_json)
end
end
def compare_item(%Token{} = token, json) do
assert Address.checksum(token.contract_address.hash) == json["address"]
assert token.symbol == json["symbol"]
assert token.name == json["name"]
assert to_string(token.decimals) == json["decimals"]
assert token.type == json["type"]
assert token.holder_count == json["holders"]
assert (is_nil(token.holder_count) and is_nil(json["holders"])) or
(to_string(token.holder_count) == json["holders"] and !is_nil(token.holder_count))
assert to_string(token.total_supply) == json["total_supply"]
assert Map.has_key?(json, "exchange_rate")
end
@ -206,6 +243,10 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do
assert Address.checksum(token_transfer.from_address_hash) == json["from"]["hash"]
assert Address.checksum(token_transfer.to_address_hash) == json["to"]["hash"]
assert to_string(token_transfer.transaction_hash) == json["tx_hash"]
assert json["timestamp"] != nil
assert json["method"] != nil
assert to_string(token_transfer.block_hash) == json["block_hash"]
assert to_string(token_transfer.log_index) == json["log_index"]
end
def compare_item(%CurrentTokenBalance{} = ctb, json) do

@ -555,12 +555,15 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do
assert to_string(log.data) == json["data"]
assert log.index == json["index"]
assert Address.checksum(log.address_hash) == json["address"]["hash"]
assert to_string(log.transaction_hash) == json["tx_hash"]
end
defp compare_item(%TokenTransfer{} = token_transfer, json) do
assert Address.checksum(token_transfer.from_address_hash) == json["from"]["hash"]
assert Address.checksum(token_transfer.to_address_hash) == json["to"]["hash"]
assert to_string(token_transfer.transaction_hash) == json["tx_hash"]
assert to_string(token_transfer.block_hash) == json["block_hash"]
assert to_string(token_transfer.log_index) == json["log_index"]
end
defp check_paginated_response(first_page_resp, second_page_resp, txs) do

@ -29,7 +29,7 @@ defmodule BlockScoutWeb.ViewingAppTest do
# assert Decimal.compare(Explorer.Chain.indexed_ratio_blocks(), Decimal.from_float(0.5)) == :eq
# insert(:pending_block_operation, block_hash: block.hash)
# insert(:pending_block_operation, block_hash: block.hash, block_number: block.number)
# session
# |> AppPage.visit_page()
@ -48,7 +48,7 @@ defmodule BlockScoutWeb.ViewingAppTest do
# assert Decimal.compare(Explorer.Chain.indexed_ratio_blocks(), 1) == :eq
# insert(:pending_block_operation, block_hash: block.hash)
# insert(:pending_block_operation, block_hash: block.hash, block_number: block.number)
# session
# |> AppPage.visit_page()
@ -69,7 +69,7 @@ defmodule BlockScoutWeb.ViewingAppTest do
# assert Decimal.compare(Explorer.Chain.indexed_ratio_blocks(), Decimal.from_float(0.5)) == :eq
# insert(:pending_block_operation, block_hash: block.hash)
# insert(:pending_block_operation, block_hash: block.hash, block_number: block.number)
# session
# |> AppPage.visit_page()
@ -96,7 +96,7 @@ defmodule BlockScoutWeb.ViewingAppTest do
# assert Decimal.compare(Explorer.Chain.indexed_ratio(), Decimal.from_float(0.9)) == :eq
# insert(:pending_block_operation, block_hash: block.hash)
# insert(:pending_block_operation, block_hash: block.hash, block_number: block.number)
# session
# |> AppPage.visit_page()
@ -121,7 +121,7 @@ defmodule BlockScoutWeb.ViewingAppTest do
# block_hash = block.hash
# insert(:pending_block_operation, block_hash: block_hash)
# insert(:pending_block_operation, block_hash: block_hash, block_number: block.number)
# BlocksIndexedCounter.calculate_blocks_indexed()

@ -192,7 +192,7 @@ defmodule BlockScoutWeb.TransactionViewTest do
|> insert()
|> with_block(block, status: :error)
insert(:pending_block_operation, block_hash: block.hash)
insert(:pending_block_operation, block_hash: block.hash, block_number: block.number)
status = TransactionView.transaction_status(transaction)
assert TransactionView.formatted_result(status) == "Error: (Awaiting internal transactions for reason)"

@ -23,7 +23,7 @@ defmodule EthereumJsonrpc.MixProject do
dialyzer: :test
],
start_permanent: Mix.env() == :prod,
version: "4.1.8"
version: "5.0.0"
]
end

@ -30,6 +30,7 @@ defmodule Explorer.Chain do
require Logger
alias ABI.TypeDecoder
alias Ecto.Association.NotLoaded
alias Ecto.{Changeset, Multi}
alias EthereumJSONRPC.Transaction, as: EthereumJSONRPCTransaction
@ -582,6 +583,23 @@ defmodule Explorer.Chain do
|> Repo.all()
end
@spec address_hash_to_token_transfers_by_token_address_hash(
Hash.Address.t() | String.t(),
Hash.Address.t() | String.t(),
Keyword.t()
) :: [TokenTransfer.t()]
def address_hash_to_token_transfers_by_token_address_hash(address_hash, token_address_hash, options \\ []) do
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
necessity_by_association = Keyword.get(options, :necessity_by_association)
address_hash
|> TokenTransfer.token_transfers_by_address_hash_and_token_address_hash(token_address_hash)
|> join_associations(necessity_by_association)
|> TokenTransfer.handle_paging_options(paging_options)
|> Repo.all()
end
@doc """
address_hash_to_token_transfers_including_contract/2 function returns token transfers on address (to/from/contract).
It is used by CSV export of token transfers button.
@ -1894,7 +1912,7 @@ defmodule Explorer.Chain do
case address_result do
%{smart_contract: smart_contract} ->
if smart_contract do
check_bytecode_matching(address_result)
check_bytecode_matching(address_result, smart_contract)
else
address_verified_twin_contract =
Chain.get_minimal_proxy_template(hash) ||
@ -1927,7 +1945,9 @@ defmodule Explorer.Chain do
end
end
defp check_bytecode_matching(address) do
defp check_bytecode_matching(address, %NotLoaded{}), do: address
defp check_bytecode_matching(address, _) do
now = DateTime.utc_now()
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
@ -2456,17 +2476,13 @@ defmodule Explorer.Chain do
@spec list_top_tokens(String.t()) :: [{Token.t(), non_neg_integer()}]
def list_top_tokens(filter, options \\ []) do
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
token_type = Keyword.get(options, :token_type, nil)
fetch_top_tokens(filter, paging_options)
fetch_top_tokens(filter, paging_options, token_type)
end
defp fetch_top_tokens(filter, paging_options) do
base_query =
from(t in Token,
where: t.total_supply > ^0,
order_by: [desc_nulls_last: t.holder_count, asc: t.name],
preload: [:contract_address]
)
defp fetch_top_tokens(filter, paging_options, token_type) do
base_query = base_token_query(token_type)
base_query_with_paging =
base_query
@ -2491,6 +2507,21 @@ defmodule Explorer.Chain do
|> Repo.all()
end
defp base_token_query(empty_type) when empty_type in [nil, []] do
from(t in Token,
order_by: [desc_nulls_last: t.holder_count, asc: t.name],
preload: [:contract_address]
)
end
defp base_token_query(token_types) when is_list(token_types) do
from(t in Token,
where: t.type in ^token_types,
order_by: [desc_nulls_last: t.holder_count, asc: t.name],
preload: [:contract_address]
)
end
@doc """
Calls `reducer` on a stream of `t:Explorer.Chain.Block.t/0` without `t:Explorer.Chain.Block.Reward.t/0`.
"""
@ -2728,20 +2759,14 @@ defmodule Explorer.Chain do
Returns a stream of all blocks with unfetched internal transactions, using
the `pending_block_operation` table.
Only blocks with consensus are returned.
iex> non_consensus = insert(:block, consensus: false)
iex> insert(:pending_block_operation, block: non_consensus)
iex> unfetched = insert(:block)
iex> insert(:pending_block_operation, block: unfetched)
iex> insert(:pending_block_operation, block: unfetched, block_number: unfetched.number)
iex> {:ok, number_set} = Explorer.Chain.stream_blocks_with_unfetched_internal_transactions(
...> MapSet.new(),
...> fn number, acc ->
...> MapSet.put(acc, number)
...> end
...> )
iex> non_consensus.number in number_set
false
iex> unfetched.number in number_set
true
@ -2754,10 +2779,9 @@ defmodule Explorer.Chain do
def stream_blocks_with_unfetched_internal_transactions(initial, reducer) when is_function(reducer, 2) do
query =
from(
b in Block,
join: pending_ops in assoc(b, :pending_operations),
where: b.consensus,
select: b.number
po in PendingBlockOperation,
where: not is_nil(po.block_number),
select: po.block_number
)
Repo.stream_reduce(query, initial, reducer)
@ -3029,46 +3053,34 @@ defmodule Explorer.Chain do
defp block_status(nil), do: {:error, :no_blocks}
def fetch_min_missing_block_cache do
max_block_number = BlockNumber.get_max()
def fetch_min_missing_block_cache(from \\ nil, to \\ nil) do
from_block_number = from || 0
to_block_number = to || BlockNumber.get_max()
if max_block_number > 0 do
if to_block_number > 0 do
query =
from(b in Block,
right_join:
missing_range in fragment(
"""
(SELECT b1.number
FROM generate_series(0, (?)::integer) AS b1(number)
FROM generate_series((?)::integer, (?)::integer) AS b1(number)
WHERE NOT EXISTS
(SELECT 1 FROM blocks b2 WHERE b2.number=b1.number AND b2.consensus))
""",
^max_block_number
^from_block_number,
^to_block_number
),
on: b.number == missing_range.number,
select: min(missing_range.number)
)
query
|> Repo.one(timeout: :infinity) || 0
Repo.one(query, timeout: :infinity)
else
0
nil
end
end
def remove_blocks_consensus(block_numbers) do
numbers = List.wrap(block_numbers)
query =
from(
block in Block,
where: block.number in ^numbers,
where: block.consensus
)
Repo.update_all(query, set: [consensus: false])
end
@doc """
Calculates the ranges of missing consensus blocks in `range`.
@ -3144,7 +3156,6 @@ defmodule Explorer.Chain do
WHERE NOT EXISTS
(SELECT 1 FROM blocks b2 WHERE b2.number=b1.number AND b2.consensus)
ORDER BY b1.number DESC
LIMIT 500000
)
""",
^range_min,
@ -5162,10 +5173,13 @@ defmodule Explorer.Chain do
end
@spec fetch_last_token_balances(Hash.Address.t(), [paging_options]) :: []
def fetch_last_token_balances(address_hash, paging_options) do
def fetch_last_token_balances(address_hash, options) do
filter = Keyword.get(options, :token_type)
options = Keyword.delete(options, :token_type)
address_hash
|> CurrentTokenBalance.last_token_balances(paging_options)
|> page_current_token_balances(paging_options)
|> CurrentTokenBalance.last_token_balances(options, filter)
|> page_current_token_balances(options)
|> Repo.all()
end

@ -158,7 +158,22 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do
@doc """
Builds an `t:Ecto.Query.t/0` to fetch the current token balances of the given address.
"""
def last_token_balances(address_hash) do
def last_token_balances(address_hash, type \\ [])
def last_token_balances(address_hash, [type | _]) do
from(
ctb in __MODULE__,
where: ctb.address_hash == ^address_hash,
where: ctb.value > 0,
where: ctb.token_type == ^type,
left_join: t in Token,
on: ctb.token_contract_address_hash == t.contract_address_hash,
select: {ctb, t},
order_by: [desc: ctb.value, asc: t.type, asc: t.name]
)
end
def last_token_balances(address_hash, _) do
from(
ctb in __MODULE__,
where: ctb.address_hash == ^address_hash,
@ -173,11 +188,11 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do
@doc """
Builds an `t:Ecto.Query.t/0` to fetch the current token balances of the given address (paginated version).
"""
def last_token_balances(address_hash, options) do
def last_token_balances(address_hash, options, type) do
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
address_hash
|> last_token_balances()
|> last_token_balances(type)
|> limit(^paging_options.page_size)
end

@ -6,6 +6,12 @@ defmodule Explorer.Chain.Cache.MinMissingBlockNumber do
use GenServer
alias Explorer.Chain
alias Explorer.Chain.Cache.BlockNumber
@default_batch_size 100_000
@normal_interval 10
@increased_interval :timer.minutes(20)
@default_last_fetched_number -1
@doc """
Starts a process to periodically update the % of blocks indexed.
@ -16,37 +22,52 @@ defmodule Explorer.Chain.Cache.MinMissingBlockNumber do
end
@impl true
def init(args) do
Task.start_link(&fetch_min_missing_block/0)
def init(_) do
schedule_next_consolidation(@normal_interval)
{:ok, %{last_fetched_number: @default_last_fetched_number}}
end
schedule_next_consolidation()
def fetch_min_missing_block(last_fetched_number) do
from = last_fetched_number + 1
to = last_fetched_number + batch_size()
max_block_number = BlockNumber.get_max() - 1
{:ok, args}
end
{corrected_to, continue?} = if to >= max_block_number, do: {max_block_number, false}, else: {to, true}
result = Chain.fetch_min_missing_block_cache(from, corrected_to)
def fetch_min_missing_block do
result = Chain.fetch_min_missing_block_cache()
cond do
not is_nil(result) ->
params = %{
counter_type: "min_missing_block_number",
value: result
}
if result > 0 do
params = %{
counter_type: "min_missing_block_number",
value: result
}
Chain.upsert_last_fetched_counter(params)
schedule_next_consolidation(@increased_interval)
@default_last_fetched_number
Chain.upsert_last_fetched_counter(params)
continue? ->
schedule_next_consolidation(@normal_interval)
corrected_to
true ->
schedule_next_consolidation(@increased_interval)
@default_last_fetched_number
end
end
defp schedule_next_consolidation do
Process.send_after(self(), :fetch_min_missing_block, :timer.minutes(20))
defp schedule_next_consolidation(interval) do
Process.send_after(self(), :fetch_min_missing_block, interval)
end
@impl true
def handle_info(:fetch_min_missing_block, state) do
fetch_min_missing_block()
schedule_next_consolidation()
def handle_info(:fetch_min_missing_block, %{last_fetched_number: last_fetched_number} = state) do
new_last_number = fetch_min_missing_block(last_fetched_number)
{:noreply, %{state | last_fetched_number: new_last_number}}
end
{:noreply, state}
defp batch_size do
Application.get_env(:explorer, __MODULE__)[:batch_size] || @default_batch_size
end
end

@ -15,6 +15,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
alias Explorer.Chain.Import.Runner.Tokens
alias Explorer.Prometheus.Instrumenter
alias Explorer.Repo, as: ExplorerRepo
alias Explorer.Utility.MissingBlockRange
@behaviour Runner
@ -50,14 +51,11 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
minimal_block_height = trace_minimal_block_height()
hashes_for_pending_block_operations =
if minimal_block_height > 0 do
changes_list
|> Enum.filter(&(&1.number >= minimal_block_height))
|> Enum.map(& &1.hash)
else
hashes
end
items_for_pending_ops =
changes_list
|> filter_by_min_height(&(&1.number >= minimal_block_height))
|> Enum.filter(& &1.consensus)
|> Enum.map(&{&1.number, &1.hash})
consensus_block_numbers = consensus_block_numbers(changes_list)
@ -65,16 +63,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
run_func = fn repo ->
{:ok, nonconsensus_items} = lose_consensus(repo, hashes, consensus_block_numbers, changes_list, insert_options)
nonconsensus_hashes =
if minimal_block_height > 0 do
nonconsensus_items
|> Enum.filter(fn {number, _hash} -> number >= minimal_block_height end)
|> Enum.map(fn {_number, hash} -> hash end)
else
hashes
end
{:ok, nonconsensus_hashes}
{:ok, filter_by_min_height(nonconsensus_items, fn {number, _hash} -> number >= minimal_block_height end)}
end
multi
@ -97,10 +86,10 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
:blocks
)
end)
|> Multi.run(:new_pending_operations, fn repo, %{lose_consensus: nonconsensus_hashes} ->
|> Multi.run(:new_pending_operations, fn repo, %{lose_consensus: nonconsensus_items} ->
Instrumenter.block_import_stage_runner(
fn ->
new_pending_operations(repo, nonconsensus_hashes, hashes_for_pending_block_operations, insert_options)
new_pending_operations(repo, nonconsensus_items, items_for_pending_ops, insert_options)
end,
:address_referencing,
:blocks,
@ -404,6 +393,10 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
timeout: timeout
)
removed_consensus_block_hashes
|> Enum.map(fn {number, _hash} -> number end)
|> MissingBlockRange.add_ranges_by_block_numbers()
{:ok, removed_consensus_block_hashes}
rescue
postgrex_error in Postgrex.Error ->
@ -423,7 +416,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
EthereumJSONRPC.first_block_to_fetch(:trace_first_block)
end
defp new_pending_operations(repo, nonconsensus_hashes, hashes, %{
defp new_pending_operations(repo, nonconsensus_items, items, %{
timeout: timeout,
timestamps: timestamps
}) do
@ -431,12 +424,12 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
{:ok, []}
else
sorted_pending_ops =
nonconsensus_hashes
items
|> MapSet.new()
|> MapSet.union(MapSet.new(hashes))
|> MapSet.difference(MapSet.new(nonconsensus_items))
|> Enum.sort()
|> Enum.map(fn hash ->
%{block_hash: hash}
|> Enum.map(fn {number, hash} ->
%{block_hash: hash, block_number: number}
end)
Import.insert_changes_list(
@ -739,4 +732,14 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
end
end)
end
defp filter_by_min_height(blocks, filter_func) do
minimal_block_height = trace_minimal_block_height()
if minimal_block_height > 0 do
Enum.filter(blocks, &filter_func.(&1))
else
blocks
end
end
end

@ -12,8 +12,9 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do
alias Explorer.Chain.Import.Runner
alias Explorer.Prometheus.Instrumenter
alias Explorer.Repo, as: ExplorerRepo
alias Explorer.Utility.MissingBlockRange
import Ecto.Query, only: [from: 2, or_where: 3]
import Ecto.Query, only: [from: 2]
@behaviour Runner
@ -116,17 +117,6 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do
:valid_internal_transactions_without_first_traces_of_trivial_transactions
)
end)
|> Multi.run(:remove_left_over_internal_transactions, fn repo,
%{
valid_internal_transactions: valid_internal_transactions
} ->
Instrumenter.block_import_stage_runner(
fn -> remove_left_over_internal_transactions(repo, valid_internal_transactions) end,
:block_pending,
:internal_transactions,
:remove_left_over_internal_transactions
)
end)
|> Multi.run(:internal_transactions, fn repo,
%{
valid_internal_transactions_without_first_traces_of_trivial_transactions:
@ -416,42 +406,6 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do
SQL.query(repo, "SET CONSTRAINTS internal_transactions_pkey DEFERRED")
end
def remove_left_over_internal_transactions(repo, valid_internal_transactions) do
# Removes internal transactions that were part of a block before a refetch
# and have not been upserted with new ones (if any exist).
case valid_internal_transactions do
[] ->
{:ok, []}
_ ->
try do
delete_query_for_block_hash_block_index =
valid_internal_transactions
|> Enum.group_by(& &1.block_hash, & &1.block_index)
|> Enum.map(fn {block_hash, indexes} -> {block_hash, Enum.max(indexes)} end)
|> Enum.reduce(InternalTransaction, fn {block_hash, max_index}, acc ->
or_where(acc, [it], it.block_hash == ^block_hash and it.block_index > ^max_index)
end)
# removes old records with the same primary key (transaction hash, transaction index)
delete_query =
valid_internal_transactions
|> Enum.map(fn params -> {params.transaction_hash, params.index} end)
|> Enum.reduce(delete_query_for_block_hash_block_index, fn {transaction_hash, index}, acc ->
or_where(acc, [it], it.transaction_hash == ^transaction_hash and it.index == ^index)
end)
# ShareLocks order already enforced by `acquire_pending_internal_txs` (see docs: sharelocks.md)
{count, result} = repo.delete_all(delete_query, [])
{:ok, {count, result}}
rescue
postgrex_error in Postgrex.Error -> {:error, %{exception: postgrex_error}}
end
end
end
defp update_transactions(repo, valid_internal_transactions, transactions, %{
timeout: timeout,
timestamps: timestamps
@ -686,6 +640,8 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do
try do
{_num, result} = repo.update_all(update_query, [])
MissingBlockRange.add_ranges_by_block_numbers(invalid_block_numbers)
Logger.debug(fn ->
[
"consensus removed from blocks with numbers: ",

@ -11,6 +11,7 @@ defmodule Explorer.Chain.Import.Runner.Transactions do
alias Explorer.Chain.{Block, Hash, Import, Transaction}
alias Explorer.Chain.Import.Runner.TokenTransfers
alias Explorer.Prometheus.Instrumenter
alias Explorer.Utility.MissingBlockRange
@behaviour Import.Runner
@ -220,11 +221,13 @@ defmodule Explorer.Chain.Import.Runner.Transactions do
try do
{_, result} =
repo.update_all(
from(b in Block, join: s in subquery(query), on: b.hash == s.hash),
from(b in Block, join: s in subquery(query), on: b.hash == s.hash, select: b.number),
[set: [consensus: false, updated_at: updated_at]],
timeout: timeout
)
MissingBlockRange.add_ranges_by_block_numbers(result)
{:ok, result}
rescue
postgrex_error in Postgrex.Error ->

@ -7,7 +7,7 @@ defmodule Explorer.Chain.PendingBlockOperation do
alias Explorer.Chain.{Block, Hash}
@required_attrs ~w(block_hash)a
@required_attrs ~w(block_hash block_number)a
@typedoc """
* `block_hash` - the hash of the block that has pending operations.
@ -20,6 +20,8 @@ defmodule Explorer.Chain.PendingBlockOperation do
schema "pending_block_operations" do
timestamps()
field(:block_number, :integer)
belongs_to(:block, Block, foreign_key: :block_hash, primary_key: true, references: :hash, type: Hash.Full)
end

@ -315,6 +315,13 @@ defmodule Explorer.Chain.TokenTransfer do
)
end
def token_transfers_by_address_hash_and_token_address_hash(address_hash, token_address_hash) do
TokenTransfer
|> where([tt], tt.from_address_hash == ^address_hash or tt.to_address_hash == ^address_hash)
|> where([tt], tt.token_contract_address_hash == ^token_address_hash)
|> order_by([tt], desc: tt.block_number, desc: tt.log_index)
end
def token_transfers_by_address_hash(direction, address_hash, token_types) do
TokenTransfer
|> filter_by_direction(direction, address_hash)

@ -465,6 +465,7 @@ defmodule Explorer.Chain.Transaction do
# Because there is no contract association, we know the contract was not verified
def decoded_input_data(%__MODULE__{to_address: nil}), do: {:error, :no_to_address}
def decoded_input_data(%NotLoaded{}), do: {:error, :not_loaded}
def decoded_input_data(%__MODULE__{input: %{bytes: bytes}}) when bytes in [nil, <<>>], do: {:error, :no_input_data}
def decoded_input_data(%__MODULE__{to_address: %{contract_code: nil}}), do: {:error, :not_a_contract_call}
@ -480,6 +481,18 @@ defmodule Explorer.Chain.Transaction do
})
end
def decoded_input_data(%__MODULE__{
to_address: %NotLoaded{},
input: input,
hash: hash
}) do
decoded_input_data(%__MODULE__{
to_address: %{smart_contract: nil},
input: input,
hash: hash
})
end
def decoded_input_data(%__MODULE__{
to_address: %{smart_contract: nil},
input: %{bytes: <<method_id::binary-size(4), _::binary>> = data},

@ -217,7 +217,7 @@ defmodule Explorer.SmartContract.Reader do
]
"""
@spec read_only_functions(Hash.t()) :: [%{}]
def read_only_functions(contract_address_hash) do
def read_only_functions(contract_address_hash, from \\ nil) do
abi =
contract_address_hash
|> Chain.address_hash_to_smart_contract()
@ -228,11 +228,11 @@ defmodule Explorer.SmartContract.Reader do
[]
_ ->
read_only_functions_from_abi(abi, contract_address_hash)
read_only_functions_from_abi_with_sender(abi, contract_address_hash, from)
end
end
def read_only_functions_proxy(contract_address_hash, implementation_address_hash_string) do
def read_only_functions_proxy(contract_address_hash, implementation_address_hash_string, from \\ nil) do
implementation_abi = Chain.get_implementation_abi(implementation_address_hash_string)
case implementation_abi do
@ -240,7 +240,7 @@ defmodule Explorer.SmartContract.Reader do
[]
_ ->
read_only_functions_from_abi(implementation_abi, contract_address_hash)
read_only_functions_from_abi_with_sender(implementation_abi, contract_address_hash, from)
end
end
@ -279,15 +279,15 @@ defmodule Explorer.SmartContract.Reader do
end
end
def read_only_functions_from_abi([_ | _] = abi, contract_address_hash) do
def read_only_functions_from_abi_with_sender([_ | _] = abi, contract_address_hash, from) do
abi_with_method_id = get_abi_with_method_id(abi)
abi_with_method_id
|> Enum.filter(&Helper.queriable_method?(&1))
|> Enum.map(&fetch_current_value_from_blockchain(&1, abi_with_method_id, contract_address_hash, false))
|> Enum.map(&fetch_current_value_from_blockchain(&1, abi_with_method_id, contract_address_hash, false, from))
end
def read_only_functions_from_abi(_, _), do: []
def read_only_functions_from_abi_with_sender(_, _, _), do: []
def read_functions_required_wallet_from_abi([_ | _] = abi) do
abi_with_method_id = get_abi_with_method_id(abi)
@ -346,23 +346,26 @@ defmodule Explorer.SmartContract.Reader do
"tuple[#{tuple_types}]"
end
def fetch_current_value_from_blockchain(function, abi, contract_address_hash, leave_error_as_map) do
values =
case function do
%{"inputs" => []} ->
method_id = function["method_id"]
args = function["inputs"]
outputs = function["outputs"]
def fetch_current_value_from_blockchain(function, abi, contract_address_hash, leave_error_as_map, from \\ nil) do
case function do
%{"inputs" => []} ->
method_id = function["method_id"]
args = function["inputs"]
outputs = function["outputs"]
values =
contract_address_hash
|> query_verified_contract(%{method_id => normalize_args(args)}, leave_error_as_map, abi)
|> query_verified_contract(%{method_id => normalize_args(args)}, from, leave_error_as_map, abi)
|> link_outputs_and_values(outputs, method_id)
_ ->
link_outputs_and_values(%{}, Map.get(function, "outputs", []), function["method_id"])
end
function
|> Map.replace!("outputs", values)
|> Map.put("abi_outputs", Map.get(function, "outputs", []))
Map.replace!(function, "outputs", values)
_ ->
function
|> Map.put("abi_outputs", Map.get(function, "outputs", []))
end
end
@doc """
@ -434,9 +437,13 @@ defmodule Explorer.SmartContract.Reader do
abi
|> ABI.parse_specification()
%{outputs: outputs, method_id: method_id} = proccess_abi(parsed_final_abi, method_id)
case proccess_abi(parsed_final_abi, method_id) do
%{outputs: outputs, method_id: method_id} ->
query_contract_and_link_outputs(contract_address_hash, args, from, abi, outputs, method_id, leave_error_as_map)
query_contract_and_link_outputs(contract_address_hash, args, from, abi, outputs, method_id, leave_error_as_map)
{:error, message} ->
{:error, message}
end
end
@spec query_function_with_custom_abi(
@ -513,10 +520,15 @@ defmodule Explorer.SmartContract.Reader do
defp proccess_abi(abi, method_id) do
function_object = find_function_by_method(abi, method_id)
%ABI.FunctionSelector{returns: returns, method_id: method_id} = function_object
outputs = extract_outputs(returns)
%{outputs: outputs, method_id: method_id}
if function_object do
%ABI.FunctionSelector{returns: returns, method_id: method_id} = function_object
outputs = extract_outputs(returns)
%{outputs: outputs, method_id: method_id}
else
{:error, "method_id does not exist"}
end
end
defp query_contract_and_link_outputs(contract_address_hash, args, from, abi, outputs, method_id, leave_error_as_map) do
@ -608,6 +620,8 @@ defmodule Explorer.SmartContract.Reader do
end
end
defp parse_item(nil), do: nil
defp parse_item("true"), do: true
defp parse_item("false"), do: false

@ -0,0 +1,78 @@
defmodule Explorer.Utility.MissingBlockRange do
@moduledoc """
Module is responsible for keeping the ranges of blocks that need to be (re)fetched.
"""
use Explorer.Schema
alias Explorer.Repo
@default_returning_batch_size 10
schema "missing_block_ranges" do
field(:from_number, :integer)
field(:to_number, :integer)
end
@doc false
def changeset(range \\ %__MODULE__{}, params) do
cast(range, params, [:from_number, :to_number])
end
def fetch_min_max do
Repo.one(min_max_block_query())
end
def get_latest_batch(size \\ @default_returning_batch_size) do
size
|> get_latest_ranges_query()
|> Repo.all()
|> Enum.map(fn %{from_number: from, to_number: to} ->
%Range{first: from, last: to, step: if(from > to, do: -1, else: 1)}
end)
end
def add_ranges_by_block_numbers(numbers) do
numbers
|> Enum.map(fn number -> number..number end)
|> save_batch()
end
def clear_batch(batch) do
Enum.map(batch, fn from..to ->
__MODULE__
|> Repo.get_by(from_number: from, to_number: to)
|> Repo.delete()
end)
end
def save_batch([]), do: {0, nil}
def save_batch(batch) do
records =
batch
|> List.wrap()
|> Enum.map(fn from..to -> %{from_number: from, to_number: to} end)
Repo.insert_all(__MODULE__, records, on_conflict: :nothing, conflict_target: [:from_number, :to_number])
end
def min_max_block_query do
from(r in __MODULE__, select: %{min: min(r.to_number), max: max(r.from_number)})
end
def get_latest_ranges_query(size) do
from(r in __MODULE__, order_by: [desc: r.from_number], limit: ^size)
end
def from_number_below_query(lower_bound) do
from(r in __MODULE__, where: r.from_number < ^lower_bound)
end
def to_number_above_query(upper_bound) do
from(r in __MODULE__, where: r.to_number > ^upper_bound)
end
def include_bound_query(bound) do
from(r in __MODULE__, where: r.from_number > ^bound, where: r.to_number < ^bound)
end
end

@ -24,7 +24,7 @@ defmodule Explorer.Mixfile do
dialyzer: :test
],
start_permanent: Mix.env() == :prod,
version: "4.1.8",
version: "5.0.0",
xref: [exclude: [BlockScoutWeb.WebRouter.Helpers]]
]
end
@ -57,8 +57,8 @@ defmodule Explorer.Mixfile do
# Type `mix help deps` for examples and options.
defp deps do
[
{:bamboo, "~> 2.2.0"},
{:mime, "~> 1.4"},
{:bamboo, "~> 2.3.0"},
{:mime, "~> 2.0"},
{:bcrypt_elixir, "~> 3.0"},
# benchmark optimizations
{:benchee, "~> 1.1.0", only: :test},

@ -0,0 +1,13 @@
defmodule Explorer.Repo.Migrations.CreateMissingBlockRanges do
use Ecto.Migration
def change do
create table(:missing_block_ranges) do
add(:from_number, :integer)
add(:to_number, :integer)
end
create(index(:missing_block_ranges, ["from_number DESC"]))
create(unique_index(:missing_block_ranges, [:from_number, :to_number]))
end
end

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save