Merge branch 'master' into gettxinfo-gas-price-field

pull/2403/head
Victor Baranov 5 years ago committed by GitHub
commit aa7b68cefd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 24
      .circleci/config.yml
  2. 2
      .credo.exs
  3. 4
      .tool-versions
  4. 57
      CHANGELOG.md
  5. 2
      PULL_REQUEST_TEMPLATE.md
  6. 4
      apps/block_scout_web/assets/__tests__/lib/async_listing_load.js
  7. 1
      apps/block_scout_web/assets/__tests__/pages/pending_transactions.js
  8. 3
      apps/block_scout_web/assets/css/app.scss
  9. 6
      apps/block_scout_web/assets/css/components/_navbar.scss
  10. 37
      apps/block_scout_web/assets/css/components/_stakes_table.scss
  11. 217
      apps/block_scout_web/assets/css/components/_tile.scss
  12. 3
      apps/block_scout_web/assets/css/theme/_base_variables.scss
  13. 7
      apps/block_scout_web/assets/css/theme/_dai_variables.scss
  14. 688
      apps/block_scout_web/assets/css/theme/_dark-theme.scss
  15. 7
      apps/block_scout_web/assets/css/theme/_ethereum_classic_variables.scss
  16. 14
      apps/block_scout_web/assets/css/theme/_ethereum_variables.scss
  17. 5
      apps/block_scout_web/assets/css/theme/_goerli_variables.scss
  18. 7
      apps/block_scout_web/assets/css/theme/_kovan_variables.scss
  19. 17
      apps/block_scout_web/assets/css/theme/_lukso_variables.scss
  20. 7
      apps/block_scout_web/assets/css/theme/_neutral_variables.scss
  21. 7
      apps/block_scout_web/assets/css/theme/_poa_variables.scss
  22. 5
      apps/block_scout_web/assets/css/theme/_rinkeby_variables.scss
  23. 5
      apps/block_scout_web/assets/css/theme/_ropsten_variables.scss
  24. 5
      apps/block_scout_web/assets/css/theme/_rsk_variables.scss
  25. 7
      apps/block_scout_web/assets/css/theme/_sokol_variables.scss
  26. 1
      apps/block_scout_web/assets/js/app.js
  27. 28
      apps/block_scout_web/assets/js/lib/async_listing_load.js
  28. 637
      apps/block_scout_web/assets/js/lib/awesomplete-util.js
  29. 2
      apps/block_scout_web/assets/js/lib/awesomplete.js
  30. 4
      apps/block_scout_web/assets/js/lib/infinite_scroll_helpers.js
  31. 29
      apps/block_scout_web/assets/js/lib/list_morph.js
  32. 25
      apps/block_scout_web/assets/js/lib/market_history_chart.js
  33. 10
      apps/block_scout_web/assets/js/lib/redux_helpers.js
  34. 17
      apps/block_scout_web/assets/js/lib/utils.js
  35. 4
      apps/block_scout_web/assets/js/pages/address.js
  36. 4
      apps/block_scout_web/assets/js/pages/address/coin_balances.js
  37. 4
      apps/block_scout_web/assets/js/pages/address/internal_transactions.js
  38. 4
      apps/block_scout_web/assets/js/pages/address/logs.js
  39. 4
      apps/block_scout_web/assets/js/pages/address/transactions.js
  40. 4
      apps/block_scout_web/assets/js/pages/address/validations.js
  41. 19
      apps/block_scout_web/assets/js/pages/blocks.js
  42. 28
      apps/block_scout_web/assets/js/pages/chain.js
  43. 11
      apps/block_scout_web/assets/js/pages/dark-mode-switcher.js
  44. 4
      apps/block_scout_web/assets/js/pages/pending_transactions.js
  45. 4
      apps/block_scout_web/assets/js/pages/transaction.js
  46. 4
      apps/block_scout_web/assets/js/pages/transactions.js
  47. 4
      apps/block_scout_web/assets/js/pages/verification_form.js
  48. 4887
      apps/block_scout_web/assets/package-lock.json
  49. 13
      apps/block_scout_web/assets/package.json
  50. 65
      apps/block_scout_web/assets/webpack.config.js
  51. 3
      apps/block_scout_web/config/config.exs
  52. 44
      apps/block_scout_web/lib/block_scout_web/chain.ex
  53. 2
      apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex
  54. 12
      apps/block_scout_web/lib/block_scout_web/cldr.ex
  55. 12
      apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_controller.ex
  56. 12
      apps/block_scout_web/lib/block_scout_web/controllers/address_contract_controller.ex
  57. 10
      apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex
  58. 12
      apps/block_scout_web/lib/block_scout_web/controllers/address_read_contract_controller.ex
  59. 35
      apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex
  60. 5
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/block_controller.ex
  61. 329
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/eth_controller.ex
  62. 2
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/transaction_controller.ex
  63. 21
      apps/block_scout_web/lib/block_scout_web/controllers/api/v1/decompiled_smart_contract_controller.ex
  64. 11
      apps/block_scout_web/lib/block_scout_web/controllers/api/v1/health_controller.ex
  65. 21
      apps/block_scout_web/lib/block_scout_web/controllers/api/v1/verified_smart_contract_controller.ex
  66. 4
      apps/block_scout_web/lib/block_scout_web/controllers/api_docs_controller.ex
  67. 156
      apps/block_scout_web/lib/block_scout_web/controllers/block_transaction_controller.ex
  68. 4
      apps/block_scout_web/lib/block_scout_web/controllers/chain/market_history_chart_controller.ex
  69. 14
      apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex
  70. 4
      apps/block_scout_web/lib/block_scout_web/controllers/recent_transactions_controller.ex
  71. 17
      apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex
  72. 6
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex
  73. 8
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex
  74. 6
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_log_controller.ex
  75. 2
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_raw_trace_controller.ex
  76. 9
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_token_transfer_controller.ex
  77. 4
      apps/block_scout_web/lib/block_scout_web/csp_header.ex
  78. 2
      apps/block_scout_web/lib/block_scout_web/notifier.ex
  79. 2
      apps/block_scout_web/lib/block_scout_web/router.ex
  80. 2
      apps/block_scout_web/lib/block_scout_web/schema/types.ex
  81. 2
      apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex
  82. 2
      apps/block_scout_web/lib/block_scout_web/templates/address/index.html.eex
  83. 12
      apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex
  84. 4
      apps/block_scout_web/lib/block_scout_web/templates/address_coin_balance/index.html.eex
  85. 20
      apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex
  86. 4
      apps/block_scout_web/lib/block_scout_web/templates/address_internal_transaction/index.html.eex
  87. 74
      apps/block_scout_web/lib/block_scout_web/templates/address_logs/_logs.html.eex
  88. 4
      apps/block_scout_web/lib/block_scout_web/templates/address_logs/index.html.eex
  89. 4
      apps/block_scout_web/lib/block_scout_web/templates/address_token/index.html.eex
  90. 4
      apps/block_scout_web/lib/block_scout_web/templates/address_token_transfer/index.html.eex
  91. 4
      apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex
  92. 4
      apps/block_scout_web/lib/block_scout_web/templates/address_validation/index.html.eex
  93. 4
      apps/block_scout_web/lib/block_scout_web/templates/block/index.html.eex
  94. 16
      apps/block_scout_web/lib/block_scout_web/templates/block/overview.html.eex
  95. 4
      apps/block_scout_web/lib/block_scout_web/templates/block_transaction/index.html.eex
  96. 20
      apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex
  97. 17
      apps/block_scout_web/lib/block_scout_web/templates/common_components/_pagination_container.html.eex
  98. 85
      apps/block_scout_web/lib/block_scout_web/templates/common_components/_table-loader.html.eex
  99. 72
      apps/block_scout_web/lib/block_scout_web/templates/common_components/_tile-loader.html.eex
  100. 43
      apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex
  101. Some files were not shown because too many files have changed in this diff Show More

@ -3,7 +3,7 @@ jobs:
build:
docker:
# Ensure .tool-versions matches
- image: circleci/elixir:1.9.0-node-browsers
- image: circleci/elixir:1.9.1-node-browsers
environment:
MIX_ENV: test
# match POSTGRES_PASSWORD for postgres image below
@ -129,7 +129,7 @@ jobs:
check_formatted:
docker:
# Ensure .tool-versions matches
- image: circleci/elixir:1.9.0
- image: circleci/elixir:1.9.1
environment:
MIX_ENV: test
@ -143,7 +143,7 @@ jobs:
credo:
docker:
# Ensure .tool-versions matches
- image: circleci/elixir:1.9.0
- image: circleci/elixir:1.9.1
environment:
MIX_ENV: test
@ -177,7 +177,7 @@ jobs:
dialyzer:
docker:
# Ensure .tool-versions matches
- image: circleci/elixir:1.9.0
- image: circleci/elixir:1.9.1
environment:
MIX_ENV: test
@ -247,7 +247,7 @@ jobs:
gettext:
docker:
# Ensure .tool-versions matches
- image: circleci/elixir:1.9.0
- image: circleci/elixir:1.9.1
environment:
MIX_ENV: test
@ -286,7 +286,7 @@ jobs:
release:
docker:
# Ensure .tool-versions matches
- image: circleci/elixir:1.9.0
- image: circleci/elixir:1.9.1
environment:
MIX_ENV: prod
@ -312,7 +312,7 @@ jobs:
sobelow:
docker:
# Ensure .tool-versions matches
- image: circleci/elixir:1.9.0
- image: circleci/elixir:1.9.1
environment:
MIX_ENV: test
@ -336,7 +336,7 @@ jobs:
test_geth_http_websocket:
docker:
# Ensure .tool-versions matches
- image: circleci/elixir:1.9.0-node-browsers
- image: circleci/elixir:1.9.1-node-browsers
environment:
MIX_ENV: test
# match POSTGRES_PASSWORD for postgres image below
@ -390,7 +390,7 @@ jobs:
test_geth_mox:
docker:
# Ensure .tool-versions matches
- image: circleci/elixir:1.9.0-node-browsers
- image: circleci/elixir:1.9.1-node-browsers
environment:
MIX_ENV: test
# match POSTGRES_PASSWORD for postgres image below
@ -444,7 +444,7 @@ jobs:
test_parity_http_websocket:
docker:
# Ensure .tool-versions matches
- image: circleci/elixir:1.9.0-node-browsers
- image: circleci/elixir:1.9.1-node-browsers
environment:
MIX_ENV: test
# match POSTGRES_PASSWORD for postgres image below
@ -498,7 +498,7 @@ jobs:
test_parity_mox:
docker:
# Ensure .tool-versions matches
- image: circleci/elixir:1.9.0-node-browsers
- image: circleci/elixir:1.9.1-node-browsers
environment:
MIX_ENV: test
# match POSTGRES_PASSWORD for postgres image below
@ -552,7 +552,7 @@ jobs:
coveralls_merge:
docker:
# Ensure .tool-versions matches
- image: circleci/elixir:1.9.0
- image: circleci/elixir:1.9.1
environment:
MIX_ENV: test

@ -89,7 +89,7 @@
# or the `schema` macro in Ecto schemas to trigger DuplicatedCode, just
# set the `excluded_macros` parameter to `[:schema, :setup, :test]`.
#
{Credo.Check.Design.DuplicatedCode, excluded_macros: []},
{Credo.Check.Design.DuplicatedCode, excluded_macros: [], mass_threshold: 80},
# You can also customize the exit_status of each check.
# If you don't want TODO comments to cause `mix credo` to fail, just

@ -1,3 +1,3 @@
elixir 1.9
erlang 21.0.4
elixir 1.9.1-otp-22
erlang 22.0
nodejs 10.11.0

@ -1,13 +1,67 @@
## Current
### Features
- [#2477](https://github.com/poanetwork/blockscout/pull/2477) - aggregate token transfers on transaction page
- [#2458](https://github.com/poanetwork/blockscout/pull/2458) - Add LAST_BLOCK var to add ability indexing in the range of blocks
- [#2456](https://github.com/poanetwork/blockscout/pull/2456) - fetch pending transactions for geth
- [#2403](https://github.com/poanetwork/blockscout/pull/2403) - Return gasPrice field at the result of gettxinfo method
### Fixes
- [#2520](https://github.com/poanetwork/blockscout/pull/2520) - Hide loading message when fetching is failed
- [#2523](https://github.com/poanetwork/blockscout/pull/2523) - Avoid importing internal_transactions of pending transactions
- [#2519](https://github.com/poanetwork/blockscout/pull/2519) - enable `First` page button in pagination
- [#2515](https://github.com/poanetwork/blockscout/pull/2515) - do not aggregate NFT token transfers
- [#2512](https://github.com/poanetwork/blockscout/pull/2512) - alert link fix
- [#2508](https://github.com/poanetwork/blockscout/pull/2508) - logs view columns fix
- [#2506](https://github.com/poanetwork/blockscout/pull/2506) - fix two active tab in the top menu
- [#2503](https://github.com/poanetwork/blockscout/pull/2503) - Mitigate autocompletion library influence to page loading performance
- [#2502](https://github.com/poanetwork/blockscout/pull/2502) - increase reward task timeout
- [#2463](https://github.com/poanetwork/blockscout/pull/2463) - dark theme fixes
- [#2496](https://github.com/poanetwork/blockscout/pull/2496) - fix docker build
- [#2495](https://github.com/poanetwork/blockscout/pull/2495) - fix logs for indexed chain
- [#2459](https://github.com/poanetwork/blockscout/pull/2459) - fix top addresses query
- [#2425](https://github.com/poanetwork/blockscout/pull/2425) - Force to show address view for checksummed address even if it is not in DB
### Chore
- [#2507](https://github.com/poanetwork/blockscout/pull/2507) - update minor version of ecto, ex_machina, phoenix_live_reload
- [#2516](https://github.com/poanetwork/blockscout/pull/2516) - update absinthe plug from fork
- [#2473](https://github.com/poanetwork/blockscout/pull/2473) - get rid of cldr warnings
- [#2402](https://github.com/poanetwork/blockscout/pull/2402) - bump otp version to 22.0
- [#2492](https://github.com/poanetwork/blockscout/pull/2492) - hide decoded row if event is not decoded
- [#2490](https://github.com/poanetwork/blockscout/pull/2490) - enable credo duplicated code check
- [#2432](https://github.com/poanetwork/blockscout/pull/2432) - bump credo version
- [#2457](https://github.com/poanetwork/blockscout/pull/2457) - update mix.lock
- [#2435](https://github.com/poanetwork/blockscout/pull/2435) - Replace deprecated extract-text-webpack-plugin with mini-css-extract-plugin
- [#2450](https://github.com/poanetwork/blockscout/pull/2450) - Fix clearance of logs and node_modules folders in clearing script
- [#2434](https://github.com/poanetwork/blockscout/pull/2434) - get rid of timex warnings
- [#2402](https://github.com/poanetwork/blockscout/pull/2402) - bump otp version to 22.0
- [#2373](https://github.com/poanetwork/blockscout/pull/2373) - Add script to validate internal_transactions constraint for large DBs
## 2.0.2-beta
### Features
- [#2412](https://github.com/poanetwork/blockscout/pull/2412) - dark theme
- [#2399](https://github.com/poanetwork/blockscout/pull/2399) - decode verified smart contract's logs
- [#2391](https://github.com/poanetwork/blockscout/pull/2391) - Controllers Improvements
- [#2379](https://github.com/poanetwork/blockscout/pull/2379) - Disable network selector when is empty
- [#2374](https://github.com/poanetwork/blockscout/pull/2374) - decode constructor arguments for verified smart contracts
- [#2366](https://github.com/poanetwork/blockscout/pull/2366) - paginate eth logs
- [#2360](https://github.com/poanetwork/blockscout/pull/2360) - add default evm version to smart contract verification
- [#2352](https://github.com/poanetwork/blockscout/pull/2352) - Fetch rewards in parallel with transactions
- [#2294](https://github.com/poanetwork/blockscout/pull/2294) - add healthy block period checking endpoint
- [#2324](https://github.com/poanetwork/blockscout/pull/2324) - set timeout for loading message on the main page
### Fixes
- [#2421](https://github.com/poanetwork/blockscout/pull/2421) - Fix hiding of loader for txs on the main page
- [#2420](https://github.com/poanetwork/blockscout/pull/2420) - fetch data from cache in healthy endpoint
- [#2416](https://github.com/poanetwork/blockscout/pull/2416) - Fix "page not found" handling in the router
- [#2413](https://github.com/poanetwork/blockscout/pull/2413) - remove outer tables for decoded data
- [#2410](https://github.com/poanetwork/blockscout/pull/2410) - preload smart contract for logs decoding
- [#2405](https://github.com/poanetwork/blockscout/pull/2405) - added templates for table loader and tile loader
- [#2398](https://github.com/poanetwork/blockscout/pull/2398) - show only one decoded candidate
- [#2389](https://github.com/poanetwork/blockscout/pull/2389) - Reduce Lodash lib size (86% of lib methods are not used)
- [#2388](https://github.com/poanetwork/blockscout/pull/2388) - add create2 support to geth's js tracer
- [#2387](https://github.com/poanetwork/blockscout/pull/2387) - fix not existing keys in transaction json rpc
- [#2378](https://github.com/poanetwork/blockscout/pull/2378) - Page performance: exclude moment.js localization files except EN, remove unused css
- [#2368](https://github.com/poanetwork/blockscout/pull/2368) - add two columns of smart contract info
- [#2375](https://github.com/poanetwork/blockscout/pull/2375) - Update created_contract_code_indexed_at on transaction import conflict
@ -25,6 +79,9 @@
- [#2326](https://github.com/poanetwork/blockscout/pull/2326) - fix nested constructor arguments
### Chore
- [#2422](https://github.com/poanetwork/blockscout/pull/2422) - check if address_id is binary in token_transfers_csv endpoint
- [#2418](https://github.com/poanetwork/blockscout/pull/2418) - Remove parentheses in market cap percentage
- [#2401](https://github.com/poanetwork/blockscout/pull/2401) - add ENV vars to manage updating period of average block time and market history cache
- [#2363](https://github.com/poanetwork/blockscout/pull/2363) - add parameters example for eth rpc
- [#2342](https://github.com/poanetwork/blockscout/pull/2342) - Upgrade Postgres image version in Docker setup
- [#2325](https://github.com/poanetwork/blockscout/pull/2325) - Reduce function input to address' hash only where possible

@ -35,3 +35,5 @@
- [ ] If I added new functionality, I added tests covering it.
- [ ] If I fixed a bug, I added a regression test to prevent the bug from silently reappearing again.
- [ ] I checked whether I should update the docs and did so if necessary
- [ ] If I added/changed/removed ENV var, I should update the list of env vars in https://github.com/poanetwork/blockscout/blob/master/docs/env-variables.md to reflect changes in the table here https://poanetwork.github.io/blockscout/#/env-variables?id=blockscout-env-variables. I've set `master` in the `Version` column.
- [ ] If I add new indices into DB, I checked, that they don't redundant with PGHero or other tools

@ -46,14 +46,12 @@ describe('REQUEST_ERROR', () => {
describe('FINISH_REQUEST', () => {
test('sets loading status to false', () => {
const state = Object.assign({}, asyncInitialState, {
loading: true,
loadingFirstPage: true
loading: true
})
const action = { type: 'FINISH_REQUEST' }
const output = asyncReducer(state, action)
expect(output.loading).toEqual(false)
expect(output.loadingFirstPage).toEqual(false)
})
})

@ -1,4 +1,3 @@
import _ from 'lodash'
import { reducer, initialState } from '../../js/pages/pending_transactions'
test('CHANNEL_DISCONNECTED', () => {

@ -128,6 +128,7 @@ $fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
@import "components/new_smart_contract";
@import "components/radio_big";
@import "components/btn_no_border";
@import "theme/dark-theme";
:export {
dashboardBannerChartAxisFontColor: $dashboard-banner-chart-axis-font-color;
@ -135,4 +136,6 @@ $fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
dashboardLineColorPrice: $dashboard-line-color-price;
primary: $primary;
secondary: $secondary;
darkprimary: $dark-primary;
darksecondary: $dark-secondary;
}

@ -252,4 +252,10 @@ $navbar-logo-width: auto !default;
@include media-breakpoint-up(xl) {
padding-right: 0;
}
}
.nav-item-networks {
.topnav-nav-link {
transition: none !important;
}
}

@ -26,6 +26,43 @@ $stakes-table-cell-separation: 25px !default;
}
}
// Loader
.table-content-loader {
display: inline-block;
height: 24px;
width: 100%;
border-radius: 4px;
background-color: #f5f6fa;
overflow: hidden;
position: relative;
&:before {
width: inherit;
height: inherit;
content: '';
position: absolute;
background: linear-gradient(to right, #f5f6fa 2%, #eee 18%, #f5f6fa 33%);
animation-duration: 1.7s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
animation-timing-function: linear;
animation-name: placeholderAnimate;
background-size: 1300px;
}
}
@keyframes placeholderAnimate {
0%{ background-position: -650px 0; }
100% { background-position: 650px 0; }
}
.table-content-pseudo {
td {
&:last-child {
padding-right: 24px !important;
}
}
}
.stakes-table {
min-width: fit-content;
width: 100%;

@ -339,9 +339,9 @@ $tile-body-a-color: #5959d8 !default;
padding-left: 6px;
padding-right: 6px;
}
.tile-type-block {
overflow: hidden;
}
}
.tile-type-block {
overflow: hidden;
}
.row {
@include media-breakpoint-down(lg) {
@ -351,3 +351,214 @@ $tile-body-a-color: #5959d8 !default;
}
}
}
// Loader
.tile-type-loading {
background-color: #fff;
padding-top: 30px;
padding-bottom: 28px;
}
.tile-loader {
display: inline-block;
height: 20px;
width: 100%;
border-radius: 4px;
background-color: #f5f6fa;
overflow: hidden;
position: relative;
&:before {
width: inherit;
height: inherit;
content: '';
position: absolute;
background: linear-gradient(to right, #f5f6fa 2%, #eee 18%, #f5f6fa 33%);
animation-duration: 1.7s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
animation-timing-function: linear;
animation-name: tilePlaceholderAnimate;
background-size: 1300px;
}
}
.tile-label-loader {
height: 14px;
width: 80px;
}
.tile-address-loader {
& + .tile-address-loader {
margin-top: 6px;
}
}
@keyframes tilePlaceholderAnimate {
0%{ background-position: -650px 0; }
100% { background-position: 650px 0; }
}
// Loading Animation
@keyframes playBlockLoadingAnimation {
0%, 90% {
opacity: 1;
}
100% {
opacity: 0;
}
}
[data-selector="chain-block-list"] {
.col-lg-3:first-child {
.tile-type-block-animation {
animation: playBlockLoadingAnimation 2.1s linear forwards;
}
}
}
.fade-up-blocks-chain {
.tile-type-block {
position: relative;
}
.tile-type-block-animation {
opacity: 0;
position: absolute;
top: -1px;
left: -4px;
width: calc(100% + 5px);
height: calc(100% + 2px);
background-color: #F6F7F9;
border-radius: 4px;
overflow: hidden;
transition: .24s ease-out;
border-top: 1px solid #dee2e6;
border-right: 1px solid #dee2e6;
border-bottom: 1px solid #dee2e6;
pointer-events: none;
.tile-type-line-up {
position: absolute;
bottom: -1px;
left: 0;
height: calc(100% + 2px);
width: 4px;
background-color: $tile-type-block-color;
transform: scaleY(0);
transform-origin: center bottom;
animation: blockLoaderLine 2s linear forwards;
z-index: 2;
}
&:after {
content: "";
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 1px;
background-color: #dee2e6;
}
}
}
.cube-animation-title {
font-size: 12px;
color: #a3a9b5;
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
}
.fade-up-blocks-chain:first-child {
.tile-type-block-animation {
opacity: 1;
}
}
@keyframes blockLoaderLine {
0% {
transform: scaleY(0);
}
100% {
transform: scaleY(1);
}
}
$cube-bezier: cubic-bezier(.25,.8,.25,1);
$cube-quantity: 5;
.cube-animation-wrapper {
width: 560px;
height: 290px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.26);
svg {
width: 50px;
margin-top: -29px;
.side {
fill: $tile-type-block-color;
opacity: 1;
&:nth-of-type(2) {
fill: lighten($tile-type-block-color, 30);
opacity: 0.5;
}
&:nth-of-type(3) {
fill: lighten($tile-type-block-color, 80);
opacity: 0.5;
}
}
}
@while $cube-quantity > 0 {
.cube-animation-row:nth-of-type(#{$cube-quantity}) {
left: 25px * $cube-quantity;
top: 15px * $cube-quantity;
}
.cube-animation-column:nth-of-type(#{$cube-quantity}) {
position: relative;
top: 14px * $cube-quantity;
left: 25px * $cube-quantity;
}
.cube-animation-column:nth-of-type(#{$cube-quantity}) svg {
transform: translate3d(0,0,0);
animation: shrink-expand 2.8s $cube-bezier forwards;
animation-delay: -0.16s * $cube-quantity;
}
$cube-quantity: $cube-quantity - 1;
}
}
.cube-animation-center {
position: absolute;
top: 6%;
left: 20%;
}
.cube-animation-row {
display: flex;
flex-direction: row-reverse;
position: absolute;
}
.cube-animation-column {
display: flex;
flex-direction: column-reverse;
}
@keyframes shrink-expand {
0% {
transform: scale(0);
}
50% {
transform: scale(1);
}
100% {
transform: scale(0);
}
}

@ -70,7 +70,10 @@ $colors: map-merge(
);
$primary: $indigo !default;
$dark-primary: #9b62ff !default;
$dark-primary-alternate: #9b62ff !default;
$secondary: #7dd79f !default;
$dark-secondary: #87e1a9 !default;
$tertiary: $purple !default;
$success: $green !default;
$info: $cyan !default;

@ -62,4 +62,9 @@ $card-tab-active: $secondary;
// Badges
$badge-neutral-color: #20446e;
$badge-neutral-background-color: rgba(#20446e, .1);
$api-text-monospace-color: #20446e;
$api-text-monospace-color: #20446e;
// Dark theme
$dark-primary: #15bba6;
$dark-secondary: #93d7ff;
$dark-primary-alternate: #15bba6;

@ -0,0 +1,688 @@
$body-dark: #1c1d31; // body background
$dark-bg: #22223a; // hero shade
$dark-light-bg: #282945; // pills bg shade
$dark-light: #313355; // tile light top share
$labels-dark: #8a8dba; // header nav, labels
// Switcher
.dark-mode-changer {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background: transparent;
border: none;
cursor: pointer;
margin-right: 5px;
outline: none !important;
box-shadow: none !important;
transition: .2s ease-in;
&:hover {
opacity: .8;
}
svg path {
fill: #828ba0;
}
&--dark {
svg path {
fill: $dark-primary;
}
}
}
.dark-theme-applied {
color: #fff;
// navbar
.navbar.navbar-primary {
background-color: $dark-light-bg;
}
.navbar-brand .navbar-logo {
filter: brightness(0) invert(1);
}
.navbar.navbar-primary .navbar-nav .nav-link {
color: $labels-dark;
.nav-link-icon {
svg path {
fill: $labels-dark;
}
}
&.active, &:hover {
.nav-link-icon {
svg path {
fill: $dark-primary;
}
}
&:before {
background-color: $dark-primary;
}
}
}
.navbar.navbar-primary .form-control {
background-color: $dark-bg;
border-color: $dark-bg;
color: #fff;
&::placeholder {
color: $labels-dark;
}
}
.navbar.navbar-primary .navbar-toggler .navbar-toggler-icon {
filter: invert(1);
}
// footer
.footer {
background: $dark-light-bg;
color: $labels-dark;
}
.footer-social-icon,
.footer-link {
color: $labels-dark;
}
.footer-social-icon:hover,
.footer-link:hover {
color: #fff;
}
.footer-list ul li::before {
background-color: $dark-secondary;
}
// hero stats
.layout-container .dashboard-banner-container {
background-image: none;
background-color: $dark-bg;
}
.dashboard-banner-network-plain-container,
.dashboard-banner-network-plain-container::after {
background-color: $dark-light-bg;
}
.dashboard-banner-network-stats-label,
.dashboard-banner-chart-legend .dashboard-banner-chart-legend-label {
color: $labels-dark;
}
.dashboard-banner-chart-legend .dashboard-banner-chart-legend-item:nth-child(1)::before {
background-color: $dark-primary;
}
.dashboard-banner-network-stats-item::before {
background-color: $dark-secondary;
}
.dashboard-banner-chart-legend .dashboard-banner-chart-legend-item:nth-child(2)::before {
background-color: $dark-secondary;
}
// main container, layout, cards
.layout-container main {
background-color: $body-dark;
}
.card {
background-color: $dark-light-bg;
box-shadow: 0 0 30px 0 rgba(23, 24, 41, 0.5);
}
.card-header {
border-bottom-color: darken($labels-dark, 30);
}
.address-detail-hash-title {
color: #fff;
}
.card-tabs {
border-bottom-color: darken($labels-dark, 30);
}
.card-tab {
background-color: transparent;
&:hover:not(.active) {
background-color: rgba($dark-primary, .15);
color: $dark-primary;
}
&.active {
background-color: $dark-primary-alternate;
color: #fff;
}
}
.card-background-1 {
background-color: $dark-primary-alternate;
}
// Components
a {
color: $dark-primary;
}
.tile {
border-top-color: $dark-light;
border-bottom-color: $dark-light;
border-right-color: $dark-light;
background-color: $dark-light;
color: $labels-dark;
&:not([class^="tile tile-type"]) {
border-left-color: $dark-light;
}
&.tile-type-coin-balance {
border-left-color: $dark-light;
}
.tile-title {
color: #fff;
}
.tile-transaction-type-block {
background-color: transparent;
}
}
.tile-bottom-contents {
background-color: $dark-bg;
}
a.tile-title {
color: #fff !important;
}
.tile.tile-type-block .tile-transaction-type-block a {
color: #fff;
}
.fade-up-blocks-chain .tile-type-block-animation {
background-color: $dark-light;
border-top-color: $dark-light;
border-right-color: $dark-light;
border-bottom-color: $dark-light;
}
.fade-up-blocks-chain .tile-type-block-animation:after {
background-color: $dark-light;
}
.cube-animation-title {
color: $labels-dark;
}
.tile .tile-body a,
.tile span[data-address-hash] { color: $dark-primary; }
.fade-up-blocks-chain .tile-type-block-animation .tile-type-line-up {
background-color: $dark-primary;
}
.tile.tile-type-block {
border-left-color: $dark-primary
}
.tile.tile-type-block .tile-status-label {
color: $dark-primary;
}
.tile.tile-type-block .tile-transaction-type-block {
border-right-color: $dark-primary;
border-top-color: $dark-primary;
border-bottom-color: $dark-primary;
}
.tile .progress {
background-color: rgba(#fff, .2);
}
.tile .progress .progress-bar {
background-color: $dark-primary;
}
.tile .tile-title-lg:not([data-balance-change-sign]) {
color: $dark-primary;
}
// btns
.btn-line {
background-color: transparent;
border-color: $dark-primary;
color: $dark-primary;
&:hover {
border-color: $dark-primary;
background-color: $dark-primary;
color: #fff;
}
}
.btn-copy-icon, .btn-qr-icon {
border-color: $dark-primary;
path {
fill: $dark-primary;
}
&:hover {
background-color: $dark-primary;
path {
fill: #fff;
}
}
}
// pagination
.pagination-container .pagination .page-link {
color: $labels-dark;
border-color: $dark-light;
background-color: $dark-light;
&:not(.no-hover):hover {
color: #fff;
background-color: $dark-primary;
path {
fill: #fff;
}
}
&[disabled] {
color: $labels-dark;
border-color: $dark-light;
background-color: $dark-light;
}
}
// dropdown
.dropdown-menu {
background-color: $dark-light;
border-left-color: $dark-light;
border-right-color: $dark-light;
border-bottom-color: $dark-light;
}
.dropdown-item {
color: #fff;
&:hover {
background-color: rgba(#fff, .1);
}
}
.dropdown-item.active {
background-color: $dark-primary;
}
.btn-dropdown-line {
background-color: $dark-light;
border-color: $dark-light;
color: $labels-dark;
}
// table
.stakes-table-th {
background-color: $dark-light;
color: $labels-dark;
}
.stakes-td {
border-bottom-color: darken($labels-dark, 30);
}
.table th, .table td {
border-top-color: darken($labels-dark, 30);
}
hr {
border-top-color: darken($labels-dark, 30);
}
// api's
.api-anchors-list {
background-color: $dark-light;
}
.api-doc-list-item {
border-bottom-color: darken($labels-dark, 30);
}
.card-subtitle,
.api-anchors-list-item-title,
.api-doc-list-item-title {
color: #fff;
}
.api-text-monospace {
color: $dark-primary;
}
.api-text-monospace-background {
background-color: rgba($dark-primary, .15);
}
.badge.badge-neutral {
background-color: rgba($dark-primary, .15);
color: $dark-primary;
}
// download csv button
.download-all-transactions .download-all-transactions-link svg path {
fill: $dark-primary;
}
//tooltips
.tooltip .arrow:before {
border-top-color: $dark-primary;
border-bottom-color: $dark-primary;
}
.tooltip > .tooltip-inner {background-color: $dark-primary;}
//network select
.network-selector-overlay {
background-color: rgba($dark-bg, .9);
}
.network-selector {
background-color: $dark-light-bg;
}
.network-selector-title {
color: #fff;
}
.network-selector-text {
color: $labels-dark;
}
.network-selector-close path {
fill: #fff;
}
.network-selector-search-container {
background-color: $dark-light;
}
.network-selector-search-container path {
fill: $labels-dark;
}
.network-selector-search-input {
color: #fff !important;
&::placeholder {
color: $labels-dark;
}
}
.network-selector-tab {
color: $labels-dark;
&:hover, &.active {
color: #fff;
}
&.active {
&:after {
background-color: $dark-primary;
}
}
}
.network-selector-item,
.network-selector-tabs-container {
border-bottom-color: darken($labels-dark, 30);
}
.network-selector-item-title {
color: #fff;
}
.network-selector-item-type {
color: $labels-dark;
}
.radio .radio-icon {
border-color: $labels-dark
}
.network-selector-item-url:hover .network-selector-item-type {
color: #fff;
}
//coin dropdown
.token-balance-dropdown.dropdown-menu {
border-color: $dark-light !important;
box-shadow: 0 0 30px 0 rgba(23, 24, 41, 0.5) !important;
}
.token-balance-dropdown .dropdown-search-icon path {
fill: $labels-dark;
}
.token-balance-dropdown .dropdown-search-field {
background-color: $dark-light;
border-color: $dark-light;
color: #fff;
&::placeholder {
color: $labels-dark;
}
}
.token-balance-dropdown[aria-labelledby="dropdown-tokens"] .dropdown-items .dropdown-item:hover {
color: #fff !important;
}
.dropdown-header {
color: $labels-dark;
}
.border-bottom {
border-bottom-color: darken($labels-dark, 30) !important;
}
// coin balance history chart
.chartjs-render-monitor[data-chart="coinBalanceHistoryChart"] {
filter: brightness(0) invert(1) !important;
}
// logs search
.logs-search-input, .logs-search-btn, .logs-search-btn-cancel {
background-color: $dark-light;
border-color: $dark-light;
color: #fff;
}
.logs-search-btn {
color: $labels-dark;
}
.logs-search-btn {
&:hover {
background-color: $dark-primary;
color: #fff;
}
}
.logs-search-input {
&::placeholder {
color: $labels-dark;
}
}
// code
pre {
color: #fff;
}
// info allert
.alert-info {
color: $labels-dark;
background-color: $dark-light;
border-color: $dark-light;
}
// dark text
.text-dark {
color: #fff !important;
}
// Contract Verification
.new-smart-contract-container {
background-color: $dark-light-bg;
background-image: linear-gradient(to bottom right, $dark-light 100%, $dark-light 100%);
@media (max-width: 991.98px) {
background-image: none;
}
}
.smart-contract-form-group-inner-wrapper .smart-contract-form-group-tooltip {
color: $labels-dark;
}
.smart-contract-title {
color: #fff;
}
.smart-contract-form-group-inner-wrapper > label {
color: $labels-dark;
}
.smart-contract-form-buttons {
border-top-color: darken($labels-dark, 30);
.btn-no-border {
background-color: $dark-light;
border-color: $dark-light;
color: #fff;
&:hover {
background-color: $dark-primary;
color: #fff;
}
}
}
.add-contract-libraries-wrapper {
border-top-color: darken($labels-dark, 30);
}
.token-tile-view-more:before, .token-tile-view-more:after {
border-top-color: darken($labels-dark, 30);
border-bottom-color: darken($labels-dark, 30);
}
// Form Controlls
.form-control {
background-color: $dark-light;
border-color: $dark-light;
color: #fff;
&::placeholder {
color: $labels-dark;
}
&:focus {
background-color: $dark-light;
border-color: $dark-primary;
color: #fff;
}
}
.radio-big .radio-icon {
border-color: $labels-dark;
}
.radio-big input[type="radio"]:checked + .radio-icon {
border-color: $dark-primary;
}
.radio-big input[type="radio"]:checked + .radio-icon::before {
background: $dark-primary;
}
.radio-big .radio-text {
color: #fff;
}
// Content loading placeholders
.tile-loader, .table-content-loader {
background-color: $dark-bg !important;
&:before {
background: linear-gradient(to right, $dark-bg 2%, lighten($dark-bg, 3) 18%, $dark-bg 33%);
}
}
// Verify other explorers
.verify-other-explorers-elem {
border-color: darken($labels-dark, 30);
.exp-logo {
border-right-color: darken($labels-dark, 30);
}
.exp-content {
h3 {
color: #fff;
}
div {
color: $labels-dark;
}
}
}
.verify-other-explorers-more {
border-color: $dark-primary;
svg path {
fill: $dark-primary;
}
&:hover {
background-color: $dark-primary;
svg path {
fill: #fff;
}
}
}
.verify-other-explorers-elem {
&:hover {
text-decoration: none;
color: #fff;
.exp-content {
h3, div {
color: #fff;
}
}
}
}
.verify-other-explorers-cell {
.exp-logo {
color: #333 !important;
}
}
// API docs dropdown content
.api-doc-parameters-list-item-description,
.api-doc-parameters-list-item-title,
.api-doc-parameters-list-title,
.api-doc-list-item-controls-view-more {
color: #fff;
}
.api-doc-parameters-list {
border-bottom-color: darken($labels-dark, 30);
}
.api-doc-parameters-container {
border-top-color: darken($labels-dark, 30);
}
.api-doc-tab {
color: $dark-primary !important;
&.active {
border-bottom-color: $dark-primary;
}
}
// Common Buttons
.btn-secondary, .button-secondary {
background-color: transparent;
border-color: $dark-primary;
color: $dark-primary;
&:hover {
background-color: $dark-primary;
border-color: $dark-primary;
color: #fff;
}
}
.awesomplete {
& > ul {
background: $dark-light-bg;
&:before {
background: $dark-light-bg;
}
li {
&:hover {
background-color: $dark-primary;
color: #fff;
mark {
background: darken($dark-primary, 10);
color: #fff;
}
}
}
}
mark {
background: $dark-primary;
color: #fff;
}
}
// Decoded data
.table.thead-light.table-bordered {
color: #fff !important;
}
.table-bordered, .table-bordered td, .table-bordered th {
border-color: darken($labels-dark, 30);
}
.dark-theme-applied .table td, .dark-theme-applied .table th, .dark-theme-applied hr {
border-top-color: darken($labels-dark, 30);
}
.btn-copy-ico svg path {
fill: #fff;
}
// pre
.pre-scrollable.line-numbers, .hljs {
background: $dark-light;
color: #fff;
}
.hljs-comment {
color: $labels-dark;
}
.hljs-title, .hljs-section {
color: #ff2294;
}
.hljs-type, .hljs-string, .hljs-number, .hljs-selector-id, .hljs-selector-class, .hljs-quote, .hljs-template-tag, .hljs-deletion {
color: #ff2294;
}
.hljs-literal, .hljs-built_in, .hljs-bullet, .hljs-code, .hljs-addition {
color: #20dd94;
}
.line-numbers [data-line-number]:before {
color: #3f436b !important;
border-right-color: #3f436b !important;
}
// alert link
.alert-link {
color: $dark-secondary;
}
}

@ -74,4 +74,9 @@ $card-tab-active: $tertiary;
// Badges
$badge-neutral-color: $tertiary;
$badge-neutral-background-color: rgba($tertiary, .1);
$api-text-monospace-color: $tertiary;
$api-text-monospace-color: $tertiary;
// Dark theme
$dark-primary: #8588ff;
$dark-secondary: #4ad7a7;
$dark-primary-alternate: #5b5ed8;

@ -56,4 +56,18 @@ $card-tab-active: $secondary;
$dashboard-banner-gradient-end
);
}
}
// Dark theme
$dark-primary: #49a2ee;
$dark-secondary: #4ad7a7;
$dark-primary-alternate: #49a2ee;
.dark-theme-applied {
.dashboard-banner-chart-legend .dashboard-banner-chart-legend-item:nth-child(1)::before {
background-color: $dark-primary !important;
}
.dashboard-banner-chart-legend .dashboard-banner-chart-legend-item:nth-child(2)::before {
background-color: $dark-secondary !important;
}
}

@ -78,3 +78,8 @@ $card-tab-active: $sub-accent-color;
$badge-neutral-color: $sub-accent-color;
$badge-neutral-background-color: rgba($sub-accent-color, .1);
$api-text-monospace-color: $sub-accent-color;
// Dark theme
$dark-primary: #e1995a;
$dark-secondary: #aeaeae;
$dark-primary-alternate: #e1995a;

@ -74,4 +74,9 @@ $badge-success-color: #15bba6;
$badge-success-background-color: rgba(#15bba6, .1);
$badge-neutral-color: $tertiary;
$badge-neutral-background-color: rgba($tertiary, .1);
$api-text-monospace-color: $tertiary;
$api-text-monospace-color: $tertiary;
// Dark theme
$dark-primary: #42e2d7;
$dark-secondary: #1f857f;
$dark-primary-alternate: #1f857f;

@ -151,3 +151,20 @@ $dashboard-banner-network-plain-container-height: 150px;
$badge-neutral-color: $tertiary;
$badge-neutral-background-color: rgba($tertiary, .1);
$api-text-monospace-color: $tertiary;
// Dark theme
$dark-primary: #fdcec4;
$dark-secondary: #a96c55;
$dark-primary-alternate: #a96c55;
.dark-theme-applied {
.dashboard-banner-network-stats-value {
color: $dark-primary !important;
}
.layout-container .dashboard-banner-container {
background-color: #282945 !important;
}
.dashboard-banner-network-plain-container::after {
box-shadow: none !important;
}
}

@ -71,4 +71,9 @@ $api-text-monospace-color: $primary;
.dropdown-items .dropdown-item:hover {
color: $primary !important;
}
}
}
// Dark theme
$dark-primary: #9b62ff;
$dark-secondary: #87e1a9;
$dark-primary-alternate: #7e50d0;

@ -71,4 +71,9 @@ $api-text-monospace-color: $primary;
.dropdown-items .dropdown-item:hover {
color: $primary !important;
}
}
}
// Dark theme
$dark-primary: #9b62ff;
$dark-secondary: #87e1a9;
$dark-primary-alternate: #7e50d0;

@ -57,3 +57,8 @@ $card-tab-active: $secondary;
);
}
}
// Dark theme
$dark-primary: #38a9f5;
$dark-secondary: #76f1ff;
$dark-primary-alternate: #38a9f5;

@ -57,3 +57,8 @@ $card-tab-active: $secondary;
);
}
}
// Dark theme
$dark-primary: #38a9f5;
$dark-secondary: #76f1ff;
$dark-primary-alternate: #38a9f5;

@ -65,3 +65,8 @@ $card-tab-active: $secondary;
// Badges
$badge-neutral-color: #1a323b;
$badge-neutral-background-color: rgba(#1a323b, .1);
// Dark theme
$dark-primary: #38c5a4;
$dark-secondary: #e39a54;
$dark-primary-alternate: #30ab8d;

@ -72,4 +72,9 @@ $card-tab-active: $sub-accent-color;
// Badges
$badge-neutral-color: $tertiary;
$badge-neutral-background-color: rgba($tertiary, .1);
$badge-neutral-background-color: rgba($tertiary, .1);
// Dark theme
$dark-primary: #40bfb2;
$dark-secondary: #25c9ff;
$dark-primary-alternate: #1c9f90;

@ -35,6 +35,7 @@ import './pages/favorites'
import './pages/network-search'
import './pages/layout'
import './pages/verification_form'
import './pages/dark-mode-switcher'
import './pages/admin/tasks.js'

@ -1,5 +1,6 @@
import $ from 'jquery'
import _ from 'lodash'
import map from 'lodash/map'
import merge from 'lodash/merge'
import URI from 'urijs'
import humps from 'humps'
import listMorph from '../lib/list_morph'
@ -50,8 +51,6 @@ export const asyncInitialState = {
requestError: false,
/* if response has no items */
emptyResponse: false,
/* if it is loading the first page */
loadingFirstPage: true,
/* link to the next page */
nextPagePath: null,
/* link to the previous page */
@ -79,8 +78,7 @@ export function asyncReducer (state = asyncInitialState, action) {
}
case 'FINISH_REQUEST': {
return Object.assign({}, state, {
loading: false,
loadingFirstPage: false
loading: false
})
}
case 'ITEMS_FETCHED': {
@ -133,7 +131,7 @@ export const elements = {
},
'[data-async-listing] [data-loading-message]': {
render ($el, state) {
if (state.loadingFirstPage) return $el.show()
if (state.loading) return $el.show()
$el.hide()
}
@ -142,7 +140,7 @@ export const elements = {
render ($el, state) {
if (
!state.requestError &&
(!state.loading || !state.loadingFirstPage) &&
(!state.loading) &&
state.items.length === 0
) {
return $el.show()
@ -164,7 +162,7 @@ export const elements = {
if (state.itemKey) {
const container = $el[0]
const newElements = _.map(state.items, (item) => $(item)[0])
const newElements = map(state.items, (item) => $(item)[0])
listMorph(container, newElements, { key: state.itemKey })
return
}
@ -200,6 +198,16 @@ export const elements = {
$el.attr('href', state.prevPagePath)
}
},
'[data-async-listing] [data-first-page-button]': {
render ($el, state) {
if (state.pagesStack.length === 0) {
return $el.hide()
}
$el.show()
$el.attr('disabled', false)
$el.attr('href', window.location.href.split('?')[0])
}
},
'[data-async-listing] [data-page-number]': {
render ($el, state) {
if (state.emptyResponse) {
@ -215,7 +223,7 @@ export const elements = {
},
'[data-async-listing] [data-loading-button]': {
render ($el, state) {
if (!state.loadingFirstPage && state.loading) return $el.show()
if (state.loading) return $el.show()
$el.hide()
}
@ -244,7 +252,7 @@ export const elements = {
* adding or removing with the correct animation. Check list_morph.js for more informantion.
*/
export function createAsyncLoadStore (reducer, initialState, itemKey) {
const state = _.merge(asyncInitialState, initialState)
const state = merge(asyncInitialState, initialState)
const store = createStore(reduceReducers(asyncReducer, reducer, state))
if (typeof itemKey !== 'undefined') {

@ -0,0 +1,637 @@
/* eslint-env browser */
/* global Awesomplete */
/* exported AwesompleteUtil */
/*
* Library endorsing Lea Verou's Awesomplete widget, providing:
* - dynamic remote data loading
* - labels with HTML markup
* - events and styling for exact matches
* - events and styling for mismatches
* - select item when TAB key is used
*
* (c) Nico Hoogervorst
* License: MIT
*
*/
window.AwesompleteUtil = (function () {
//
// event names and css classes
//
var _AWE = 'awesomplete-'
var _AWE_LOAD = _AWE + 'loadcomplete'
var _AWE_CLOSE = _AWE + 'close'
var _AWE_MATCH = _AWE + 'match'
var _AWE_PREPOP = _AWE + 'prepop'
var _AWE_SELECT = _AWE + 'select'
var _CLS_FOUND = 'awe-found'
var _CLS_NOT_FOUND = 'awe-not-found'
var $ = Awesomplete.$ /* shortcut for document.querySelector */
//
// private functions
//
// Some parts are shamelessly copied from Awesomplete.js like the logic inside this _suggestion function.
// Returns an object with label and value properties. Data parameter is plain text or Object/Array with label and value.
function _suggestion (data) {
var lv = Array.isArray(data)
? { label: data[0], value: data[1] }
: typeof data === 'object' && 'label' in data && 'value' in data ? data : { label: data, value: data }
return {label: lv.label || lv.value, value: lv.value}
}
// Helper to send events with detail property.
function _fire (target, name, detail) {
// $.fire uses deprecated methods but other methods don't work in IE11.
return $.fire(target, name, {detail: detail})
}
// Look if there is an exact match or a mismatch, set awe-found, awe-not-found css class and send match events.
function _matchValue (awe, prepop) {
var input = awe.input /* the input field */
var classList = input.classList
var utilprops = awe.utilprops /* extra properties piggybacked on Awesomplete object */
var selected = utilprops.selected /* the exact selected Suggestion with label and value */
var val = utilprops.convertInput.call(awe, input.value) /* trimmed lowercased value */
var opened = awe.opened /* is the suggestion list opened? */
var result = [] /* matches with value */
var list = awe._list /* current list of suggestions */
var suggestion, fake, rec, j /* function scoped variables */
utilprops.prepop = false /* after the first call it's not a prepopulation phase anymore */
if (list) { /* if there is a suggestion list */
for (j = 0; j < list.length; j++) { /* loop all suggestions */
rec = list[j]
suggestion = _suggestion(awe.data(rec, val)) /* call data convert function */
// with maxItems = 0 cannot look if suggestion list is opened to determine if there are still matches,
// instead call the filter method to see if there are still some options.
if (awe.maxItems === 0) {
// Awesomplete.FILTER_CONTAINS and Awesomplete.FILTER_STARTSWITH use the toString method.
suggestion.toString = function () { return '' + this.label }
if (awe.filter(suggestion, val)) {
// filter returns true, so there is at least one partial match.
opened = true
}
}
// Don't want to change the real input field, emulate a fake one.
fake = {input: {value: ''}}
// Determine how this suggestion would look like if it is replaced in the input field,
// it is an exact match if somebody types exactly that.
// Use the fake input here. fake.input.value will contain the result of the replace function.
awe.replace.call(fake, suggestion)
// Trim and lowercase also the fake input and compare that with the currently typed-in value.
if (utilprops.convertInput.call(awe, fake.input.value) === val) {
// This is an exact match. However there might more suggestions with the same value.
// If the user selected a suggestion from the list, check if this one matches, assuming that
// value + label is unique (if not it will be difficult for the user to make an informed decision).
if (selected && selected.value === suggestion.value && selected.label === suggestion.label) {
// this surely is the selected one
result = [rec]
break
}
// add the matching record to the result set.
result.push(rec)
} // end if
} // end loop
// if the result differs from the previous result
if (utilprops.prevSelected !== result) {
// if there is an exact match
if (result.length > 0) {
// if prepopulation phase (initial/autofill value); not triggered by user input
if (prepop) {
_fire(input, _AWE_PREPOP, result)
} else if (utilprops.changed) { /* if input is changed */
utilprops.prevSelected = result /* new result */
classList.remove(_CLS_NOT_FOUND) /* remove class */
classList.add(_CLS_FOUND) /* add css class */
_fire(input, _AWE_MATCH, result) /* fire event */
}
} else if (prepop) { /* no exact match, if in prepopulation phase */
_fire(input, _AWE_PREPOP, [])
} else if (utilprops.changed) { /* no exact match, if input is changed */
utilprops.prevSelected = []
classList.remove(_CLS_FOUND)
// Mark as not-found if there are no suggestions anymore or if another field is now active
if (!opened || (input !== document.activeElement)) {
if (val.length > 0) {
classList.add(_CLS_NOT_FOUND)
_fire(input, _AWE_MATCH, [])
}
} else {
classList.remove(_CLS_NOT_FOUND)
}
}
}
}
}
// Listen to certain events of THIS awesomplete object to trigger input validation.
function _match (ev) {
var awe = this
if ((ev.type === _AWE_CLOSE || ev.type === _AWE_LOAD || ev.type === 'blur') && ev.target === awe.input) {
_matchValue(awe, awe.utilprops.prepop && ev.type === _AWE_LOAD)
}
}
// Select currently selected item if tab or shift-tab key is used.
function _onKeydown (ev) {
var awe = this
if (ev.target === awe.input && ev.keyCode === 9) { // TAB key
awe.select() // take current selected item
}
}
// Handle selection event. State changes when an item is selected.
function _select (ev) {
var awe = this
awe.utilprops.changed = true // yes, user made a change
awe.utilprops.selected = ev.text // Suggestion object
}
// check if the object is empty {} object
function _isEmpty (val) {
return Object.keys(val).length === 0 && val.constructor === Object
}
// Need an updated suggestion list if:
// - There is no result yet, or there is a result but not for the characters we entered
// - or there might be more specific results because the limit was reached.
function _ifNeedListUpdate (awe, val, queryVal) {
var utilprops = awe.utilprops
return (!utilprops.listQuery ||
(!utilprops.loadall && /* with loadall, if there is a result, there is no need for new lists */
val.lastIndexOf(queryVal, 0) === 0 &&
(val.lastIndexOf(utilprops.listQuery, 0) !== 0 ||
(typeof utilprops.limit === 'number' && awe._list.length >= utilprops.limit))))
}
// Set a new suggestion list. Trigger loadcomplete event.
function _loadComplete (awe, list, queryVal) {
awe.list = list
awe.utilprops.listQuery = queryVal
_fire(awe.input, _AWE_LOAD, queryVal)
}
// Handle ajax response. Expects HTTP OK (200) response with JSON object with suggestion(s) (array).
function _onLoad () {
var t = this
var awe = t.awe
var xhr = t.xhr
var queryVal = t.queryVal
var val = awe.utilprops.val
var data
var prop
if (xhr.status === 200) {
data = JSON.parse(xhr.responseText)
if (awe.utilprops.convertResponse) data = awe.utilprops.convertResponse(data)
if (!Array.isArray(data)) {
if (awe.utilprops.limit === 0 || awe.utilprops.limit === 1) {
// if there is max 1 result expected, the array is not needed.
// Fur further processing, take the whole result and put it as one element in an array.
data = _isEmpty(data) ? [] : [data]
} else {
// search for the first property that contains an array
for (prop in data) {
if (Array.isArray(data[prop])) {
data = data[prop]
break
}
}
}
}
// can only handle arrays
if (Array.isArray(data)) {
// are we still interested in this response?
if (_ifNeedListUpdate(awe, val, queryVal)) {
// accept the new suggestion list
_loadComplete(awe, data, queryVal || awe.utilprops.loadall)
}
}
}
}
// Perform suggestion list lookup for the current value and validate. Use ajax when there is an url specified.
function _lookup (awe, val) {
var xhr
if (awe.utilprops.url) {
// are we still interested in this response?
if (_ifNeedListUpdate(awe, val, val)) {
xhr = new XMLHttpRequest()
awe.utilprops.ajax.call(awe,
awe.utilprops.url,
awe.utilprops.urlEnd,
awe.utilprops.loadall ? '' : val,
_onLoad.bind({awe: awe, xhr: xhr, queryVal: val}),
xhr
)
} else {
_matchValue(awe, awe.utilprops.prepop)
}
} else {
_matchValue(awe, awe.utilprops.prepop)
}
}
// Restart autocomplete search: clear css classes and send match-event with empty list.
function _restart (awe) {
var elem = awe.input
var classList = elem.classList
// IE11 only handles the first parameter of the remove method.
classList.remove(_CLS_NOT_FOUND)
classList.remove(_CLS_FOUND)
_fire(elem, _AWE_MATCH, [])
}
// handle new input value
function _update (awe, val, prepop) {
// prepop parameter is optional. Default value is false.
awe.utilprops.prepop = prepop || false
// if value changed
if (awe.utilprops.val !== val) {
// new value, clear previous selection
awe.utilprops.selected = null
// yes, user made a change
awe.utilprops.changed = true
awe.utilprops.val = val
// value is empty or smaller than minChars
if (val.length < awe.minChars || val.length === 0) {
// restart autocomplete search
_restart(awe)
}
if (val.length >= awe.minChars) {
// lookup suggestions and validate input
_lookup(awe, val)
}
}
return awe
}
// handle input changed event for THIS awesomplete object
function _onInput (e) {
var awe = this
var val
if (e.target === awe.input) {
// lowercase and trim input value
val = awe.utilprops.convertInput.call(awe, awe.input.value)
_update(awe, val)
}
}
// item function (as specified in Awesomplete) which just creates the 'li' HTML tag.
function _item (html /* , input */) {
return $.create('li', {
innerHTML: html,
'aria-selected': 'false'
})
}
// Escape HTML characters in text.
function _htmlEscape (text) {
return text.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
}
// Function to copy a field from the selected autocomplete item to another DOM element.
function _copyFun (e) {
var t = this
var sourceId = t.sourceId
var dataField = t.dataField
var targetId = t.targetId
var elem
var val
if (e.target === $(sourceId)) {
if (typeof targetId === 'function') {
targetId(e, dataField)
} else {
// lookup target element if it isn't resolved yet
elem = $(targetId)
// don't override target inputs if user is currently editing it.
if (elem && elem !== document.activeElement) {
// event must contain 1 item from suggestion list
val = Array.isArray(e.detail) && e.detail.length === 1 ? e.detail[0] : null
// if a datafield is specified, take that value
val = (dataField && val ? val[dataField] : val) || ''
// if it is an input control
if (typeof elem.value !== 'undefined') {
// set new value
elem.value = val
// not really sure if it is an input control, check if it has a classList
if (elem.classList && elem.classList.remove) {
// it might be another awesomplete control, if so the input is not wrong anymore because it's changed now
elem.classList.remove(_CLS_NOT_FOUND)
}
} else if (typeof elem.src !== 'undefined') { /* is it an image tag? */
elem.src = val
} else {
// use innerHTML to set the new value, because value might intentionally contain HTML markup
elem.innerHTML = val
}
}
}
}
}
// click function for the combobox button
function _clickFun (e) {
var t = this
var awe
var minChars
if (e.target === $(t.btnId)) {
e.preventDefault()
awe = t.awe
// toggle open/close
if (awe.ul.childNodes.length === 0 || awe.ul.hasAttribute('hidden')) {
minChars = awe.minChars
// ignore that the input value is empty
awe.minChars = 0
// show the suggestion list
awe.evaluate()
awe.minChars = minChars
} else {
awe.close()
}
}
}
// Return text with mark tags arround matching input. Don't replace inside <HTML> tags.
// When startsWith is true, mark only the matching begin text.
function _mark (text, input, startsWith) {
var searchText = $.regExpEscape(_htmlEscape(input).trim())
var regExp = searchText.length <= 0 ? null : startsWith ? RegExp('^' + searchText, 'i') : RegExp('(?!<[^>]+?>)' + searchText + '(?![^<]*?>)', 'gi')
return text.replace(regExp, '<mark>$&</mark>')
}
// Recursive jsonFlatten function
function _jsonFlatten (result, cur, prop, level, opts) {
var root = opts.root /* filter resulting json tree on root property (optional) */
var value = opts.value /* search for this property and copy it's value to a new 'value' property
(optional, do not specify it if the json array contains plain strings) */
var label = opts.label || opts.value /* search this property and copy it's value to a new 'label' property.
If there is a 'opts.value' field but no 'opts.label', assume label is the same. */
var isEmpty = true
var arrayResult = []
var j
// at top level, look if there is a property which starts with root (if specified)
if (level === 0 && root && prop && (prop + '.').lastIndexOf(root + '.', 0) !== 0 && (root + '.').lastIndexOf(prop + '.', 0) !== 0) {
return result
}
// handle current part of the json tree
if (Object(cur) !== cur) {
if (prop) {
result[prop] = cur
} else {
result = cur
}
} else if (Array.isArray(cur)) {
for (j = 0; j < cur.length; j++) {
arrayResult.push(_jsonFlatten({}, cur[j], '', level + 1, opts))
}
if (prop) {
result[prop] = arrayResult
} else {
result = arrayResult
}
} else {
for (j in cur) {
isEmpty = false
_jsonFlatten(result, cur[j], prop ? prop + '.' + j : j, level, opts)
}
if (isEmpty && prop) result[prop] = {}
}
// for arrays at top and subtop level
if (level < 2 && prop) {
// if a 'value' is specified and found a mathing property, create extra 'value' property.
if (value && (prop + '.').lastIndexOf(value + '.', 0) === 0) { result['value'] = result[prop] }
// if a 'label' is specified and found a mathing property, create extra 'label' property.
if (label && (prop + '.').lastIndexOf(label + '.', 0) === 0) { result['label'] = result[prop] }
}
if (level === 0) {
// Make sure that both value and label properties exist, even if they are nil.
// This is handy with limit 0 or 1 when the result doesn't have to contain an array.
if (value && !('value' in result)) { result['value'] = null }
if (label && !('label' in result)) { result['label'] = null }
}
return result
}
// Stop AwesompleteUtil; detach event handlers from the Awesomplete object.
function _detach () {
var t = this
var elem = t.awe.input
var boundMatch = t.boundMatch
var boundOnInput = t.boundOnInput
var boundOnKeydown = t.boundOnKeydown
var boundSelect = t.boundSelect
elem.removeEventListener(_AWE_SELECT, boundSelect)
elem.removeEventListener(_AWE_LOAD, boundMatch)
elem.removeEventListener(_AWE_CLOSE, boundMatch)
elem.removeEventListener('blur', boundMatch)
elem.removeEventListener('input', boundOnInput)
elem.removeEventListener('keydown', boundOnKeydown)
}
//
// public methods
//
return {
// ajax call for url + val + urlEnd. fn is the callback function. xhr parameter is optional.
ajax: function (url, urlEnd, val, fn, xhr) {
xhr = xhr || new XMLHttpRequest()
xhr.open('GET', url + encodeURIComponent(val) + (urlEnd || ''))
xhr.onload = fn
xhr.send()
return xhr
},
// Convert input before comparing it with suggestion. lowercase and trim the text
convertInput: function (text) {
return typeof text === 'string' ? text.trim().toLowerCase() : ''
},
// item function as defined in Awesomplete.
// item(html, input). input is optional and ignored in this implementation
item: _item,
// Set a new suggestion list. Trigger loadcomplete event.
// load(awesomplete, list, queryVal)
load: _loadComplete,
// Return text with mark tags arround matching input. Don't replace inside <HTML> tags.
// When startsWith is true, mark only the matching begin text.
// mark(text, input, startsWith)
mark: _mark,
// highlight items: Marks input in the first line, not in the optional description
itemContains: function (text, input) {
var arr
if (input.trim().length > 0) {
arr = ('' + text).split(/<p>/)
arr[0] = _mark(arr[0], input)
text = arr.join('<p>')
}
return _item(text, input)
},
// highlight items: mark all occurrences of the input text
itemMarkAll: function (text, input) {
return _item(input.trim() === '' ? '' + text : _mark('' + text, input), input)
},
// highlight items: mark input in the begin text
itemStartsWith: function (text, input) {
return _item(input.trim() === '' ? '' + text : _mark('' + text, input, true), input)
},
// create Awesomplete object for input control elemId. opts are passed unchanged to Awesomplete.
create: function (elemId, utilOpts, opts) {
opts.item = opts.item || this.itemContains /* by default uses itemContains, can be overriden */
var awe = new Awesomplete(elemId, opts)
awe.utilprops = utilOpts || {}
// loadall is true if there is no url (there is a static data-list)
if (!awe.utilprops.url && typeof awe.utilprops.loadall === 'undefined') {
awe.utilprops.loadall = true
}
awe.utilprops.ajax = awe.utilprops.ajax || this.ajax /* default ajax function can be overriden */
awe.utilprops.convertInput = awe.utilprops.convertInput || this.convertInput /* the same applies for convertInput */
return awe
},
// attach Awesomplete object to event listeners
attach: function (awe) {
var elem = awe.input
var boundMatch = _match.bind(awe)
var boundOnKeydown = _onKeydown.bind(awe)
var boundOnInput = _onInput.bind(awe)
var boundSelect = _select.bind(awe)
var boundDetach = _detach.bind({awe: awe,
boundMatch: boundMatch,
boundOnInput: boundOnInput,
boundOnKeydown: boundOnKeydown,
boundSelect: boundSelect
})
var events = {
'keydown': boundOnKeydown,
'input': boundOnInput
}
events['blur'] = events[_AWE_CLOSE] = events[_AWE_LOAD] = boundMatch
events[_AWE_SELECT] = boundSelect
$.bind(elem, events)
awe.utilprops.detach = boundDetach
// Perform ajax call if prepop is true and there is an initial input value, or when all values must be loaded (loadall)
if (awe.utilprops.prepop && (awe.utilprops.loadall || elem.value.length > 0)) {
awe.utilprops.val = awe.utilprops.convertInput.call(awe, elem.value)
_lookup(awe, awe.utilprops.val)
}
return awe
},
// update input value via javascript. Use prepop=true when this is an initial/prepopulation value.
update: function (awe, value, prepop) {
awe.input.value = value
return _update(awe, value, prepop)
},
// create and attach Awesomplete object for input control elemId. opts are passed unchanged to Awesomplete.
start: function (elemId, utilOpts, opts) {
return this.attach(this.create(elemId, utilOpts, opts))
},
// Stop AwesompleteUtil; detach event handlers from the Awesomplete object.
detach: function (awe) {
if (awe.utilprops.detach) {
awe.utilprops.detach()
delete awe.utilprops.detach
}
return awe
},
// Create function to copy a field from the selected autocomplete item to another DOM element.
// dataField can be null.
createCopyFun: function (sourceId, dataField, targetId) {
return _copyFun.bind({sourceId: sourceId, dataField: dataField, targetId: $(targetId) || targetId})
},
// attach copy function to event listeners. prepop is optional and by default true.
// if true the copy function will also listen to awesomplete-prepop events.
// The optional listenEl is the element that listens, defaults to document.body.
attachCopyFun: function (fun, prepop, listenEl) {
// prepop parameter defaults to true
prepop = typeof prepop === 'boolean' ? prepop : true
listenEl = listenEl || document.body
listenEl.addEventListener(_AWE_MATCH, fun)
if (prepop) listenEl.addEventListener(_AWE_PREPOP, fun)
return fun
},
// Create and attach copy function.
startCopy: function (sourceId, dataField, targetId, prepop) {
var sourceEl = $(sourceId)
return this.attachCopyFun(this.createCopyFun(sourceEl || sourceId, dataField, targetId), prepop, sourceEl)
},
// Stop copy function. Detach it from event listeners.
// The optional listenEl must be the same element that was used during startCopy/attachCopyFun;
// in general: Awesomplete.$(sourceId). listenEl defaults to document.body.
detachCopyFun: function (fun, listenEl) {
listenEl = listenEl || document.body
listenEl.removeEventListener(_AWE_PREPOP, fun)
listenEl.removeEventListener(_AWE_MATCH, fun)
return fun
},
// Create function for combobox button (btnId) to toggle dropdown list.
createClickFun: function (btnId, awe) {
return _clickFun.bind({btnId: btnId, awe: awe})
},
// Attach click function for combobox to click event.
// The optional listenEl is the element that listens, defaults to document.body.
attachClickFun: function (fun, listenEl) {
listenEl = listenEl || document.body
listenEl.addEventListener('click', fun)
return fun
},
// Create and attach click function for combobox button. Toggles open/close of suggestion list.
startClick: function (btnId, awe) {
var btnEl = $(btnId)
return this.attachClickFun(this.createClickFun(btnEl || btnId, awe), btnEl)
},
// Stop click function. Detach it from event listeners.
// The optional listenEl must be the same element that was used during startClick/attachClickFun;
// in general: Awesomplete.$(btnId). listenEl defaults to document.body.
detachClickFun: function (fun, listenEl) {
listenEl = listenEl || document.body
listenEl.removeEventListener('click', fun)
return fun
},
// filter function as specified in Awesomplete. Filters suggestion list on items containing input value.
// Awesomplete.FILTER_CONTAINS filters on data.label, however
// this function filters on value and not on the shown label which may contain markup.
filterContains: function (data, input) {
return Awesomplete.FILTER_CONTAINS(data.value, input)
},
// filter function as specified in Awesomplete. Filters suggestion list on matching begin text.
// Awesomplete.FILTER_STARTSWITH filters on data.label, however
// this function filters on value and not on the shown label which may contain markup.
filterStartsWith: function (data, input) {
return Awesomplete.FILTER_STARTSWITH(data.value, input)
},
// Flatten JSON.
// { "a":{"b":{"c":[{"d":{"e":1}}]}}} becomes {"a.b.c":[{"d.e":1}]}.
// This function can be bind to configure it with extra options;
// bind({root: '<root path>', value: '<value property>', label: '<label property>'})
jsonFlatten: function (data) {
// start json tree recursion
return _jsonFlatten({}, data, '', 0, this)
}
}
}())

@ -0,0 +1,2 @@
import 'awesomplete/awesomplete.css'
import 'awesomplete'

@ -1,5 +1,5 @@
import $ from 'jquery'
import _ from 'lodash'
import omit from 'lodash/omit'
import humps from 'humps'
import { connectElements } from './redux_helpers.js'
@ -12,7 +12,7 @@ const initialState = {
function infiniteScrollReducer (state = initialState, action) {
switch (action.type) {
case 'INFINITE_SCROLL_ELEMENTS_LOAD': {
return Object.assign({}, state, _.omit(action, 'type'))
return Object.assign({}, state, omit(action, 'type'))
}
case 'LOADING_NEXT_PAGE': {
return Object.assign({}, state, {

@ -1,5 +1,10 @@
import $ from 'jquery'
import _ from 'lodash'
import map from 'lodash/map'
import get from 'lodash/get'
import noop from 'lodash/noop'
import find from 'lodash/find'
import intersectionBy from 'lodash/intersectionBy'
import differenceBy from 'lodash/differenceBy'
import morph from 'nanomorph'
import { updateAllAges } from './from_now'
@ -25,12 +30,12 @@ import { updateAllAges } from './from_now'
export default function (container, newElements, { key, horizontal } = {}) {
if (!container) return
const oldElements = $(container).children().get()
let currentList = _.map(oldElements, (el) => ({ id: _.get(el, key), el }))
const newList = _.map(newElements, (el) => ({ id: _.get(el, key), el }))
const overlap = _.intersectionBy(newList, currentList, 'id').map(({ id, el }) => ({ id, el: updateAllAges($(el))[0] }))
let currentList = map(oldElements, (el) => ({ id: get(el, key), el }))
const newList = map(newElements, (el) => ({ id: get(el, key), el }))
const overlap = intersectionBy(newList, currentList, 'id').map(({ id, el }) => ({ id, el: updateAllAges($(el))[0] }))
// remove old items
const removals = _.differenceBy(currentList, newList, 'id')
const removals = differenceBy(currentList, newList, 'id')
let canAnimate = !horizontal && removals.length <= 1
removals.forEach(({ el }) => {
if (!canAnimate) return el.remove()
@ -38,7 +43,7 @@ export default function (container, newElements, { key, horizontal } = {}) {
$el.addClass('shrink-out')
setTimeout(() => { slideUpRemove($el) }, 400)
})
currentList = _.differenceBy(currentList, removals, 'id')
currentList = differenceBy(currentList, removals, 'id')
// update kept items
currentList = currentList.map(({ el }, i) => ({
@ -47,14 +52,14 @@ export default function (container, newElements, { key, horizontal } = {}) {
}))
// add new items
const finalList = newList.map(({ id, el }) => _.get(_.find(currentList, { id }), 'el', el)).reverse()
const finalList = newList.map(({ id, el }) => get(find(currentList, { id }), 'el', el)).reverse()
canAnimate = !horizontal
finalList.forEach((el, i) => {
if (el.parentElement) return
if (!canAnimate) return container.insertBefore(el, _.get(finalList, `[${i - 1}]`))
if (!canAnimate) return container.insertBefore(el, get(finalList, `[${i - 1}]`))
canAnimate = false
if (!_.get(finalList, `[${i - 1}]`)) return slideDownAppend($(container), el)
slideDownBefore($(_.get(finalList, `[${i - 1}]`)), el)
if (!get(finalList, `[${i - 1}]`)) return slideDownAppend($(container), el)
slideDownBefore($(get(finalList, `[${i - 1}]`)), el)
})
}
@ -80,7 +85,7 @@ function slideUpRemove ($el) {
})
}
function smarterSlideDown ($el, { insert = _.noop } = {}) {
function smarterSlideDown ($el, { insert = noop } = {}) {
if (!$el.length) return
const originalScrollHeight = document.body.scrollHeight
const scrollPosition = window.scrollY
@ -100,7 +105,7 @@ function smarterSlideDown ($el, { insert = _.noop } = {}) {
}
}
function smarterSlideUp ($el, { complete = _.noop } = {}) {
function smarterSlideUp ($el, { complete = noop } = {}) {
if (!$el.length) return
const originalScrollHeight = document.body.scrollHeight
const scrollPosition = window.scrollY

@ -4,6 +4,7 @@ import humps from 'humps'
import numeral from 'numeral'
import { formatUsdValue } from '../lib/currency'
import sassVariables from '../../css/app.scss'
import { showLoader } from '../lib/utils'
const config = {
type: 'line',
@ -87,6 +88,17 @@ function getMarketCapData (marketHistoryData, availableSupply) {
}
}
// colors for light and dark theme
var priceLineColor
var mcapLineColor
if (localStorage.getItem('current-color-mode') === 'dark') {
priceLineColor = sassVariables.darkprimary
mcapLineColor = sassVariables.darksecondary
} else {
priceLineColor = sassVariables.dashboardLineColorPrice
mcapLineColor = sassVariables.dashboardLineColorMarket
}
class MarketHistoryChart {
constructor (el, availableSupply, marketHistoryData) {
this.price = {
@ -95,8 +107,8 @@ class MarketHistoryChart {
data: getPriceData(marketHistoryData),
fill: false,
pointRadius: 0,
backgroundColor: sassVariables.dashboardLineColorPrice,
borderColor: sassVariables.dashboardLineColorPrice,
backgroundColor: priceLineColor,
borderColor: priceLineColor,
lineTension: 0
}
this.marketCap = {
@ -105,8 +117,8 @@ class MarketHistoryChart {
data: getMarketCapData(marketHistoryData, availableSupply),
fill: false,
pointRadius: 0,
backgroundColor: sassVariables.dashboardLineColorMarket,
borderColor: sassVariables.dashboardLineColorMarket,
backgroundColor: mcapLineColor,
borderColor: mcapLineColor,
lineTension: 0
}
this.availableSupply = availableSupply
@ -129,6 +141,10 @@ class MarketHistoryChart {
export function createMarketHistoryChart (el) {
const dataPath = el.dataset.market_history_chart_path
const $chartLoading = $('[data-chart-loading-message]')
const isTimeout = true
const timeoutID = showLoader(isTimeout, $chartLoading)
const $chartError = $('[data-chart-error-message]')
const chart = new MarketHistoryChart(el, 0, [])
$.getJSON(dataPath, {type: 'JSON'})
@ -143,6 +159,7 @@ export function createMarketHistoryChart (el) {
})
.always(() => {
$chartLoading.hide()
clearTimeout(timeoutID)
})
return chart
}

@ -1,5 +1,7 @@
import $ from 'jquery'
import _ from 'lodash'
import reduce from 'lodash/reduce'
import isObject from 'lodash/isObject'
import forIn from 'lodash/forIn'
import { createStore as reduxCreateStore } from 'redux'
/**
@ -97,17 +99,17 @@ export function createStore (reducer) {
*/
export function connectElements ({ elements, store, action = 'ELEMENTS_LOAD' }) {
function loadElements () {
return _.reduce(elements, (pageLoadParams, { load }, selector) => {
return reduce(elements, (pageLoadParams, { load }, selector) => {
if (!load) return pageLoadParams
const $el = $(selector)
if (!$el.length) return pageLoadParams
const morePageLoadParams = load($el, store)
return _.isObject(morePageLoadParams) ? Object.assign(pageLoadParams, morePageLoadParams) : pageLoadParams
return isObject(morePageLoadParams) ? Object.assign(pageLoadParams, morePageLoadParams) : pageLoadParams
}, {})
}
function renderElements (state, oldState) {
_.forIn(elements, ({ render }, selector) => {
forIn(elements, ({ render }, selector) => {
if (!render) return
const $el = $(selector)
if (!$el.length) return

@ -1,8 +1,8 @@
import _ from 'lodash'
import debounce from 'lodash/debounce'
export function batchChannel (func) {
let msgs = []
const debouncedFunc = _.debounce(() => {
const debouncedFunc = debounce(() => {
func.apply(this, [msgs])
msgs = []
}, 1000, { maxWait: 5000 })
@ -11,3 +11,16 @@ export function batchChannel (func) {
debouncedFunc()
}
}
export function showLoader (isTimeout, loader) {
if (isTimeout) {
const timeout = setTimeout(function () {
loader.removeAttr('hidden')
loader.show()
}, 1000)
return timeout
} else {
loader.hide()
return null
}
}

@ -1,5 +1,5 @@
import $ from 'jquery'
import _ from 'lodash'
import omit from 'lodash/omit'
import URI from 'urijs'
import humps from 'humps'
import numeral from 'numeral'
@ -25,7 +25,7 @@ export function reducer (state = initialState, action) {
switch (action.type) {
case 'PAGE_LOAD':
case 'ELEMENTS_LOAD': {
return Object.assign({}, state, _.omit(action, 'type'))
return Object.assign({}, state, omit(action, 'type'))
}
case 'CHANNEL_DISCONNECTED': {
if (state.beyondPageOne) return state

@ -1,5 +1,5 @@
import $ from 'jquery'
import _ from 'lodash'
import omit from 'lodash/omit'
import humps from 'humps'
import socket from '../../socket'
import { connectElements } from '../../lib/redux_helpers.js'
@ -14,7 +14,7 @@ export function reducer (state, action) {
switch (action.type) {
case 'PAGE_LOAD':
case 'ELEMENTS_LOAD': {
return Object.assign({}, state, _.omit(action, 'type'))
return Object.assign({}, state, omit(action, 'type'))
}
case 'CHANNEL_DISCONNECTED': {
if (state.beyondPageOne) return state

@ -1,5 +1,5 @@
import $ from 'jquery'
import _ from 'lodash'
import omit from 'lodash/omit'
import humps from 'humps'
import numeral from 'numeral'
import socket from '../../socket'
@ -20,7 +20,7 @@ export function reducer (state, action) {
switch (action.type) {
case 'PAGE_LOAD':
case 'ELEMENTS_LOAD': {
return Object.assign({}, state, _.omit(action, 'type'))
return Object.assign({}, state, omit(action, 'type'))
}
case 'CHANNEL_DISCONNECTED': {
if (state.beyondPageOne) return state

@ -1,5 +1,5 @@
import $ from 'jquery'
import _ from 'lodash'
import omit from 'lodash/omit'
import humps from 'humps'
import { connectElements } from '../../lib/redux_helpers.js'
import { createAsyncLoadStore } from '../../lib/async_listing_load'
@ -13,7 +13,7 @@ export function reducer (state, action) {
switch (action.type) {
case 'PAGE_LOAD':
case 'ELEMENTS_LOAD': {
return Object.assign({}, state, _.omit(action, 'type'))
return Object.assign({}, state, omit(action, 'type'))
}
case 'START_SEARCH': {
return Object.assign({}, state, {pagesStack: [], isSearch: true})

@ -1,5 +1,5 @@
import $ from 'jquery'
import _ from 'lodash'
import omit from 'lodash/omit'
import URI from 'urijs'
import humps from 'humps'
import { subscribeChannel } from '../../socket'
@ -16,7 +16,7 @@ export function reducer (state, action) {
switch (action.type) {
case 'PAGE_LOAD':
case 'ELEMENTS_LOAD': {
return Object.assign({}, state, _.omit(action, 'type'))
return Object.assign({}, state, omit(action, 'type'))
}
case 'CHANNEL_DISCONNECTED': {
if (state.beyondPageOne) return state

@ -1,5 +1,5 @@
import $ from 'jquery'
import _ from 'lodash'
import omit from 'lodash/omit'
import humps from 'humps'
import socket from '../../socket'
import { connectElements } from '../../lib/redux_helpers.js'
@ -14,7 +14,7 @@ export function reducer (state = initialState, action) {
switch (action.type) {
case 'PAGE_LOAD':
case 'ELEMENTS_LOAD': {
return Object.assign({}, state, _.omit(action, 'type'))
return Object.assign({}, state, omit(action, 'type'))
}
case 'CHANNEL_DISCONNECTED': {
return Object.assign({}, state, { channelDisconnected: true })

@ -1,5 +1,10 @@
import $ from 'jquery'
import _ from 'lodash'
import omit from 'lodash/omit'
import last from 'lodash/last'
import min from 'lodash/min'
import max from 'lodash/max'
import keys from 'lodash/keys'
import rangeRight from 'lodash/rangeRight'
import humps from 'humps'
import socket from '../socket'
import { connectElements } from '../lib/redux_helpers.js'
@ -14,7 +19,7 @@ export const blockReducer = withMissingBlocks(baseReducer)
function baseReducer (state = initialState, action) {
switch (action.type) {
case 'ELEMENTS_LOAD': {
return Object.assign({}, state, _.omit(action, 'type'))
return Object.assign({}, state, omit(action, 'type'))
}
case 'CHANNEL_DISCONNECTED': {
return Object.assign({}, state, {
@ -25,7 +30,7 @@ function baseReducer (state = initialState, action) {
if (state.channelDisconnected || state.beyondPageOne || state.blockType !== 'block') return state
const blockNumber = getBlockNumber(action.msg.blockHtml)
const minBlock = getBlockNumber(_.last(state.items))
const minBlock = getBlockNumber(last(state.items))
if (state.items.length && blockNumber < minBlock) return state
@ -62,12 +67,12 @@ function withMissingBlocks (reducer) {
return acc
}, {})
const blockNumbers = _(blockNumbersToItems).keys().map(x => parseInt(x, 10)).value()
const minBlock = _.min(blockNumbers)
const maxBlock = _.max(blockNumbers)
const blockNumbers = keys(blockNumbersToItems).map(x => parseInt(x, 10))
const minBlock = min(blockNumbers)
const maxBlock = max(blockNumbers)
return Object.assign({}, result, {
items: _.rangeRight(minBlock, maxBlock + 1)
items: rangeRight(minBlock, maxBlock + 1)
.map((blockNumber) => blockNumbersToItems[blockNumber] || placeHolderBlock(blockNumber))
})
}

@ -1,11 +1,15 @@
import $ from 'jquery'
import _ from 'lodash'
import omit from 'lodash/omit'
import first from 'lodash/first'
import rangeRight from 'lodash/rangeRight'
import find from 'lodash/find'
import map from 'lodash/map'
import humps from 'humps'
import numeral from 'numeral'
import socket from '../socket'
import { exchangeRateChannel, formatUsdValue } from '../lib/currency'
import { createStore, connectElements } from '../lib/redux_helpers.js'
import { batchChannel } from '../lib/utils'
import { batchChannel, showLoader } from '../lib/utils'
import listMorph from '../lib/list_morph'
import { createMarketHistoryChart } from '../lib/market_history_chart'
@ -33,7 +37,7 @@ export const reducer = withMissingBlocks(baseReducer)
function baseReducer (state = initialState, action) {
switch (action.type) {
case 'ELEMENTS_LOAD': {
return Object.assign({}, state, _.omit(action, 'type'))
return Object.assign({}, state, omit(action, 'type'))
}
case 'RECEIVED_NEW_ADDRESS_COUNT': {
return Object.assign({}, state, {
@ -122,12 +126,12 @@ function withMissingBlocks (reducer) {
if (!result.blocks || result.blocks.length < 2) return result
const maxBlock = _.first(result.blocks).blockNumber
const maxBlock = first(result.blocks).blockNumber
const minBlock = maxBlock - (result.blocks.length - 1)
return Object.assign({}, result, {
blocks: _.rangeRight(minBlock, maxBlock + 1)
.map((blockNumber) => _.find(result.blocks, ['blockNumber', blockNumber]) || {
blocks: rangeRight(minBlock, maxBlock + 1)
.map((blockNumber) => find(result.blocks, ['blockNumber', blockNumber]) || {
blockNumber,
chainBlockHtml: placeHolderBlock(blockNumber)
})
@ -194,7 +198,7 @@ const elements = {
const container = $el[0]
if (state.blocksLoading === false) {
const blocks = _.map(state.blocks, ({ chainBlockHtml }) => $(chainBlockHtml)[0])
const blocks = map(state.blocks, ({ chainBlockHtml }) => $(chainBlockHtml)[0])
listMorph(container, blocks, { key: 'dataset.blockNumber', horizontal: true })
}
}
@ -210,11 +214,7 @@ const elements = {
},
'[data-selector="chain-block-list"] [data-selector="loading-message"]': {
render ($el, state, oldState) {
if (state.blocksLoading) {
$el.show()
} else {
$el.hide()
}
showLoader(state.blocksLoading, $el)
}
},
'[data-selector="transactions-list"] [data-selector="error-message"]': {
@ -224,7 +224,7 @@ const elements = {
},
'[data-selector="transactions-list"] [data-selector="loading-message"]': {
render ($el, state, oldState) {
$el.toggle(state.transactionsLoading)
showLoader(state.transactionsLoading, $el)
}
},
'[data-selector="transactions-list"]': {
@ -234,7 +234,7 @@ const elements = {
render ($el, state, oldState) {
if (oldState.transactions === state.transactions) return
const container = $el[0]
const newElements = _.map(state.transactions, ({ transactionHtml }) => $(transactionHtml)[0])
const newElements = map(state.transactions, ({ transactionHtml }) => $(transactionHtml)[0])
listMorph(container, newElements, { key: 'dataset.identifierHash' })
}
},

@ -0,0 +1,11 @@
import $ from 'jquery'
$('.dark-mode-changer').click(function () {
if (localStorage.getItem('current-color-mode') === 'dark') {
localStorage.setItem('current-color-mode', 'light')
} else {
localStorage.setItem('current-color-mode', 'dark')
}
// reload each theme switch
document.location.reload(true)
})

@ -1,5 +1,5 @@
import $ from 'jquery'
import _ from 'lodash'
import omit from 'lodash/omit'
import humps from 'humps'
import numeral from 'numeral'
import socket from '../socket'
@ -20,7 +20,7 @@ export const initialState = {
export function reducer (state = initialState, action) {
switch (action.type) {
case 'ELEMENTS_LOAD': {
return Object.assign({}, state, _.omit(action, 'type'))
return Object.assign({}, state, omit(action, 'type'))
}
case 'CHANNEL_DISCONNECTED': {
return Object.assign({}, state, {

@ -1,5 +1,5 @@
import $ from 'jquery'
import _ from 'lodash'
import omit from 'lodash/omit'
import humps from 'humps'
import numeral from 'numeral'
import socket from '../socket'
@ -13,7 +13,7 @@ export const initialState = {
export function reducer (state = initialState, action) {
switch (action.type) {
case 'ELEMENTS_LOAD': {
return Object.assign({}, state, _.omit(action, 'type'))
return Object.assign({}, state, omit(action, 'type'))
}
case 'RECEIVED_NEW_BLOCK': {
if ((action.msg.blockNumber - state.blockNumber) > state.confirmations) {

@ -1,5 +1,5 @@
import $ from 'jquery'
import _ from 'lodash'
import omit from 'lodash/omit'
import humps from 'humps'
import numeral from 'numeral'
import socket from '../socket'
@ -18,7 +18,7 @@ export const initialState = {
export function reducer (state = initialState, action) {
switch (action.type) {
case 'ELEMENTS_LOAD': {
return Object.assign({}, state, _.omit(action, 'type'))
return Object.assign({}, state, omit(action, 'type'))
}
case 'CHANNEL_DISCONNECTED': {
return Object.assign({}, state, {

@ -1,5 +1,5 @@
import $ from 'jquery'
import _ from 'lodash'
import omit from 'lodash/omit'
import URI from 'urijs'
import humps from 'humps'
import { subscribeChannel } from '../socket'
@ -15,7 +15,7 @@ export function reducer (state = initialState, action) {
switch (action.type) {
case 'PAGE_LOAD':
case 'ELEMENTS_LOAD': {
return Object.assign({}, state, _.omit(action, 'type'))
return Object.assign({}, state, omit(action, 'type'))
}
case 'CHANNEL_DISCONNECTED': {
if (state.beyondPageOne) return state

File diff suppressed because it is too large Load Diff

@ -20,12 +20,13 @@
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.1.0-4",
"highlight.js": "^9.13.1",
"highlightjs-solidity": "^1.0.6",
"awesomplete": "1.1.2",
"bignumber.js": "^7.2.1",
"bootstrap": "^4.1.3",
"chart.js": "^2.7.2",
"clipboard": "^2.0.1",
"highlight.js": "^9.13.1",
"highlightjs-solidity": "^1.0.6",
"humps": "^2.0.1",
"jquery": "^3.3.1",
"lodash": "^4.17.11",
@ -47,20 +48,22 @@
"babel-loader": "^7.1.4",
"babel-preset-env": "^1.6.1",
"copy-webpack-plugin": "^4.5.1",
"css-loader": "^0.28.11",
"css-loader": "^3.1.0",
"eslint": "^4.15.0",
"eslint-config-standard": "^11.0.0-beta.0",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-node": "^5.2.1",
"eslint-plugin-promise": "^3.6.0",
"eslint-plugin-standard": "^3.0.1",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "^1.1.11",
"jest": "^23.2.0",
"mini-css-extract-plugin": "^0.8.0",
"node-sass": "^4.9.3",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"postcss-loader": "^2.1.4",
"sass-loader": "^7.0.1",
"sass-loader": "^7.1.0",
"style-loader": "^0.21.0",
"terser-webpack-plugin": "^1.3.0",
"webpack": "^4.6.0",
"webpack-cli": "^3.0.8"
},

@ -1,7 +1,9 @@
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const TerserJSPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { ContextReplacementPlugin } = require('webpack')
const { ContextReplacementPlugin } = require('webpack');
const glob = require("glob");
function transpileViewScript(file) {
@ -25,6 +27,46 @@ function transpileViewScript(file) {
}
};
const jsOptimizationParams = {
cache: true,
parallel: true,
sourceMap: true,
}
const awesompleteJs = {
entry: {
awesomplete: './js/lib/awesomplete.js',
'awesomplete-util': './js/lib/awesomplete-util.js',
},
output: {
filename: '[name].min.js',
path: path.resolve(__dirname, '../priv/static/js')
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader",
}
],
},
],
},
optimization: {
minimizer: [
new TerserJSPlugin(jsOptimizationParams),
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '../css/awesomplete.css'
}),
]
}
const appJs =
{
entry: './js/app.js',
@ -32,6 +74,9 @@ const appJs =
filename: 'app.js',
path: path.resolve(__dirname, '../priv/static/js')
},
optimization: {
minimizer: [new TerserJSPlugin(jsOptimizationParams), new OptimizeCSSAssetsPlugin({})],
},
module: {
rules: [
{
@ -43,8 +88,9 @@ const appJs =
},
{
test: /\.scss$/,
use: ExtractTextPlugin.extract({
use: [{
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader"
}, {
loader: "postcss-loader"
@ -57,9 +103,8 @@ const appJs =
'node_modules/@fortawesome/fontawesome-free/scss'
]
}
}],
fallback: 'style-loader'
})
}
]
}, {
test: /\.(svg|ttf|eot|woff|woff2)$/,
use: {
@ -74,7 +119,9 @@ const appJs =
]
},
plugins: [
new ExtractTextPlugin('../css/app.css'),
new MiniCssExtractPlugin({
filename: '../css/app.css'
}),
new CopyWebpackPlugin([{ from: 'static/', to: '../' }]),
new ContextReplacementPlugin(/moment[\/\\]locale$/, /en/)
]
@ -82,4 +129,4 @@ const appJs =
const viewScripts = glob.sync('./js/view_specific/**/*.js').map(transpileViewScript);
module.exports = viewScripts.concat(appJs);
module.exports = viewScripts.concat(appJs, awesompleteJs);

@ -58,8 +58,7 @@ config :block_scout_web, BlockScoutWeb.SocialMedia,
config :ex_cldr,
default_locale: "en",
locales: ["en"],
gettext: BlockScoutWeb.Gettext
default_backend: BlockScoutWeb.Cldr
config :logger, :block_scout_web,
# keep synced with `config/config.exs`

@ -124,18 +124,20 @@ defmodule BlockScoutWeb.Chain do
end
def paging_options(%{"block_number" => block_number_string}) do
with {block_number, ""} <- Integer.parse(block_number_string) do
[paging_options: %{@default_paging_options | key: {block_number}}]
else
case Integer.parse(block_number_string) do
{block_number, ""} ->
[paging_options: %{@default_paging_options | key: {block_number}}]
_ ->
[paging_options: @default_paging_options]
end
end
def paging_options(%{"index" => index_string}) when is_binary(index_string) do
with {index, ""} <- Integer.parse(index_string) do
[paging_options: %{@default_paging_options | key: {index}}]
else
case Integer.parse(index_string) do
{index, ""} ->
[paging_options: %{@default_paging_options | key: {index}}]
_ ->
[paging_options: @default_paging_options]
end
@ -174,10 +176,12 @@ defmodule BlockScoutWeb.Chain do
def split_list_by_page(list_plus_one), do: Enum.split(list_plus_one, @page_size)
defp address_from_param(param) do
with {:ok, hash} <- string_to_address_hash(param) do
find_or_insert_address_from_hash(hash)
else
:error -> {:error, :not_found}
case string_to_address_hash(param) do
{:ok, hash} ->
find_or_insert_address_from_hash(hash)
:error ->
{:error, :not_found}
end
end
@ -246,18 +250,22 @@ defmodule BlockScoutWeb.Chain do
end
defp transaction_from_param(param) do
with {:ok, hash} <- string_to_transaction_hash(param) do
hash_to_transaction(hash)
else
:error -> {:error, :not_found}
case string_to_transaction_hash(param) do
{:ok, hash} ->
hash_to_transaction(hash)
:error ->
{:error, :not_found}
end
end
defp hash_string_to_block(hash_string) do
with {:ok, hash} <- string_to_block_hash(hash_string) do
hash_to_block(hash)
else
:error -> {:error, :not_found}
case string_to_block_hash(hash_string) do
{:ok, hash} ->
hash_to_block(hash)
:error ->
{:error, :not_found}
end
end
end

@ -73,7 +73,7 @@ defmodule BlockScoutWeb.AddressChannel do
def handle_out("count", %{count: count}, socket) do
Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale)
push(socket, "count", %{count: Cldr.Number.to_string!(count, format: "#,###")})
push(socket, "count", %{count: BlockScoutWeb.Cldr.Number.to_string!(count, format: "#,###")})
{:noreply, socket}
end

@ -0,0 +1,12 @@
defmodule BlockScoutWeb.Cldr do
@moduledoc """
Cldr global configuration.
"""
use Cldr,
default_locale: "en",
locales: ["en"],
gettext: BlockScoutWeb.Gettext,
generate_docs: false,
providers: [Cldr.Number, Cldr.Unit]
end

@ -16,7 +16,7 @@ defmodule BlockScoutWeb.AddressCoinBalanceController do
def index(conn, %{"address_id" => address_hash_string, "type" => "JSON"} = params) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.hash_to_address(address_hash, [], false) do
:ok <- Chain.check_address_exists(address_hash) do
full_options = paging_options(params)
coin_balances_plus_one = Chain.address_to_coin_balances(address_hash, full_options)
@ -32,7 +32,7 @@ defmodule BlockScoutWeb.AddressCoinBalanceController do
address_coin_balance_path(
conn,
:index,
address,
address_hash,
Map.delete(next_page_params, "type")
)
end
@ -52,7 +52,7 @@ defmodule BlockScoutWeb.AddressCoinBalanceController do
:error ->
unprocessable_entity(conn)
{:error, :not_found} ->
:not_found ->
not_found(conn)
end
end
@ -68,6 +68,12 @@ defmodule BlockScoutWeb.AddressCoinBalanceController do
validation_count: validation_count(address_hash),
current_path: current_path(conn)
)
else
:error ->
unprocessable_entity(conn)
{:error, :not_found} ->
not_found(conn)
end
end
end

@ -8,8 +8,18 @@ defmodule BlockScoutWeb.AddressContractController do
alias Indexer.Fetcher.CoinBalanceOnDemand
def index(conn, %{"address_id" => address_hash_string}) do
address_options = [
necessity_by_association: %{
:contracts_creation_internal_transaction => :optional,
:names => :optional,
:smart_contract => :optional,
:token => :optional,
:contracts_creation_transaction => :optional
}
]
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.find_contract_address(address_hash) do
{:ok, address} <- Chain.find_contract_address(address_hash, address_options, true) do
render(
conn,
"index.html",

@ -16,7 +16,7 @@ defmodule BlockScoutWeb.AddressLogsController do
def index(conn, %{"address_id" => address_hash_string, "type" => "JSON"} = params) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.hash_to_address(address_hash, [], false) do
:ok <- Chain.check_address_exists(address_hash) do
logs_plus_one = Chain.address_to_logs(address_hash, paging_options(params))
{results, next_page} = split_list_by_page(logs_plus_one)
@ -26,7 +26,7 @@ defmodule BlockScoutWeb.AddressLogsController do
nil
next_page_params ->
address_logs_path(conn, :index, address, Map.delete(next_page_params, "type"))
address_logs_path(conn, :index, address_hash, Map.delete(next_page_params, "type"))
end
items =
@ -74,7 +74,7 @@ defmodule BlockScoutWeb.AddressLogsController do
def search_logs(conn, %{"topic" => topic, "address_id" => address_hash_string} = params) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.hash_to_address(address_hash, [], false) do
:ok <- Chain.check_address_exists(address_hash) do
topic = String.trim(topic)
formatted_topic = if String.starts_with?(topic, "0x"), do: topic, else: "0x" <> topic
@ -89,7 +89,7 @@ defmodule BlockScoutWeb.AddressLogsController do
nil
next_page_params ->
address_logs_path(conn, :index, address, Map.delete(next_page_params, "type"))
address_logs_path(conn, :index, address_hash, Map.delete(next_page_params, "type"))
end
items =
@ -115,4 +115,6 @@ defmodule BlockScoutWeb.AddressLogsController do
not_found(conn)
end
end
def search_logs(conn, _), do: not_found(conn)
end

@ -15,8 +15,18 @@ defmodule BlockScoutWeb.AddressReadContractController do
import BlockScoutWeb.AddressController, only: [transaction_count: 1, validation_count: 1]
def index(conn, %{"address_id" => address_hash_string}) do
address_options = [
necessity_by_association: %{
:contracts_creation_internal_transaction => :optional,
:names => :optional,
:smart_contract => :optional,
:token => :optional,
:contracts_creation_transaction => :optional
}
]
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.find_contract_address(address_hash) do
{:ok, address} <- Chain.find_contract_address(address_hash, address_options, true) do
render(
conn,
"index.html",

@ -83,7 +83,13 @@ defmodule BlockScoutWeb.AddressTransactionController do
unprocessable_entity(conn)
{:error, :not_found} ->
not_found(conn)
case Chain.Hash.Address.validate(address_hash_string) do
{:ok, _} ->
json(conn, %{items: [], next_page_path: ""})
_ ->
not_found(conn)
end
end
end
@ -106,11 +112,30 @@ defmodule BlockScoutWeb.AddressTransactionController do
unprocessable_entity(conn)
{:error, :not_found} ->
not_found(conn)
{:ok, address_hash} = Chain.string_to_address_hash(address_hash_string)
address = %Chain.Address{hash: address_hash, smart_contract: nil, token: nil}
case Chain.Hash.Address.validate(address_hash_string) do
{:ok, _} ->
render(
conn,
"index.html",
address: address,
coin_balance_status: nil,
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
filter: params["filter"],
transaction_count: 0,
validation_count: 0,
current_path: current_path(conn)
)
_ ->
not_found(conn)
end
end
end
def token_transfers_csv(conn, %{"address_id" => address_hash_string}) do
def token_transfers_csv(conn, %{"address_id" => address_hash_string}) when is_binary(address_hash_string) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.hash_to_address(address_hash) do
address
@ -130,6 +155,8 @@ defmodule BlockScoutWeb.AddressTransactionController do
end
end
def token_transfers_csv(conn, _), do: not_found(conn)
def transactions_csv(conn, %{"address_id" => address_hash_string}) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.hash_to_address(address_hash) do
@ -149,4 +176,6 @@ defmodule BlockScoutWeb.AddressTransactionController do
not_found(conn)
end
end
def transactions_csv(conn, _), do: not_found(conn)
end

@ -8,9 +8,8 @@ defmodule BlockScoutWeb.API.RPC.BlockController do
def getblockreward(conn, params) do
with {:block_param, {:ok, unsafe_block_number}} <- {:block_param, Map.fetch(params, "blockno")},
{:ok, block_number} <- ChainWeb.param_to_block_number(unsafe_block_number),
block_options = [necessity_by_association: %{transactions: :optional}],
{:ok, block} <- Chain.number_to_block(block_number, block_options) do
reward = Chain.block_reward(block)
{:ok, block} <- Chain.number_to_block(block_number) do
reward = Chain.block_reward(block_number)
render(conn, :block_reward, block: block, reward: reward)
else

@ -1,44 +1,10 @@
defmodule BlockScoutWeb.API.RPC.EthController do
use BlockScoutWeb, :controller
alias Ecto.Type, as: EctoType
alias Explorer.{Chain, Repo}
alias Explorer.Chain.{Block, Data, Hash, Hash.Address, Wei}
alias Explorer.Etherscan.Logs
@methods %{
"eth_getBalance" => %{
action: :eth_get_balance,
notes: """
the `earliest` parameter will not work as expected currently, because genesis block balances
are not currently imported
""",
example: """
{"id": 0, "jsonrpc": "2.0", "method": "eth_getBalance", "params": ["0x0000000000000000000000000000000000000007", "2"]}
"""
},
"eth_getLogs" => %{
action: :eth_get_logs,
notes: """
Will never return more than 1000 log entries.
""",
example: """
{"id": 0, "jsonrpc": "2.0", "method": "eth_getLogs", "params": [{"address": "0x0000000000000000000000000000000000000026","topics": ["0x01"]}]}
"""
}
}
@index_to_word %{
0 => "first",
1 => "second",
2 => "third",
3 => "fourth"
}
def methods, do: @methods
alias Explorer.EthRPC
def eth_request(%{body_params: %{"_json" => requests}} = conn, _) when is_list(requests) do
responses = responses(requests)
responses = EthRPC.responses(requests)
conn
|> put_status(200)
@ -46,7 +12,7 @@ defmodule BlockScoutWeb.API.RPC.EthController do
end
def eth_request(%{body_params: %{"_json" => request}} = conn, _) do
[response] = responses([request])
[response] = EthRPC.responses([request])
conn
|> put_status(200)
@ -65,297 +31,10 @@ defmodule BlockScoutWeb.API.RPC.EthController do
_ -> request
end
[response] = responses([decoded_request])
[response] = EthRPC.responses([decoded_request])
conn
|> put_status(200)
|> render("response.json", %{response: response})
end
def eth_get_balance(address_param, block_param \\ nil) do
with {:address, {:ok, address}} <- {:address, Chain.string_to_address_hash(address_param)},
{:block, {:ok, block}} <- {:block, block_param(block_param)},
{:balance, {:ok, balance}} <- {:balance, Chain.get_balance_as_of_block(address, block)} do
{:ok, Wei.hex_format(balance)}
else
{:address, :error} ->
{:error, "Query parameter 'address' is invalid"}
{:block, :error} ->
{:error, "Query parameter 'block' is invalid"}
{:balance, {:error, :not_found}} ->
{:error, "Balance not found"}
end
end
def eth_get_logs(filter_options) do
with {:ok, address_or_topic_params} <- address_or_topic_params(filter_options),
{:ok, from_block_param, to_block_param} <- logs_blocks_filter(filter_options),
{:ok, from_block} <- cast_block(from_block_param),
{:ok, to_block} <- cast_block(to_block_param) do
filter =
address_or_topic_params
|> Map.put(:from_block, from_block)
|> Map.put(:to_block, to_block)
|> Map.put(:allow_non_consensus, true)
{:ok, filter |> Logs.list_logs() |> Enum.map(&render_log/1)}
else
{:error, message} when is_bitstring(message) ->
{:error, message}
{:error, :empty} ->
{:ok, []}
_ ->
{:error, "Something went wrong."}
end
end
defp render_log(log) do
topics =
Enum.reject(
[log.first_topic, log.second_topic, log.third_topic, log.fourth_topic],
&is_nil/1
)
%{
"address" => to_string(log.address_hash),
"blockHash" => to_string(log.block_hash),
"blockNumber" => Integer.to_string(log.block_number, 16),
"data" => to_string(log.data),
"logIndex" => Integer.to_string(log.index, 16),
"removed" => log.block_consensus == false,
"topics" => topics,
"transactionHash" => to_string(log.transaction_hash),
"transactionIndex" => log.transaction_index,
"transactionLogIndex" => log.index,
"type" => "mined"
}
end
defp cast_block("0x" <> hexadecimal_digits = input) do
case Integer.parse(hexadecimal_digits, 16) do
{integer, ""} -> {:ok, integer}
_ -> {:error, input <> " is not a valid block number"}
end
end
defp cast_block(integer) when is_integer(integer), do: {:ok, integer}
defp cast_block(_), do: {:error, "invalid block number"}
defp address_or_topic_params(filter_options) do
address_param = Map.get(filter_options, "address")
topics_param = Map.get(filter_options, "topics")
with {:ok, address} <- validate_address(address_param),
{:ok, topics} <- validate_topics(topics_param) do
address_and_topics(address, topics)
end
end
defp address_and_topics(nil, nil), do: {:error, "Must supply one of address and topics"}
defp address_and_topics(address, nil), do: {:ok, %{address_hash: address}}
defp address_and_topics(nil, topics), do: {:ok, topics}
defp address_and_topics(address, topics), do: {:ok, Map.put(topics, :address_hash, address)}
defp validate_address(nil), do: {:ok, nil}
defp validate_address(address) do
case Address.cast(address) do
{:ok, address} -> {:ok, address}
:error -> {:error, "invalid address"}
end
end
defp validate_topics(nil), do: {:ok, nil}
defp validate_topics([]), do: []
defp validate_topics(topics) when is_list(topics) do
topics
|> Stream.with_index()
|> Enum.reduce({:ok, %{}}, fn {topic, index}, {:ok, acc} ->
case cast_topics(topic) do
{:ok, data} ->
with_filter = Map.put(acc, String.to_existing_atom("#{@index_to_word[index]}_topic"), data)
{:ok, add_operator(with_filter, index)}
:error ->
{:error, "invalid topics"}
end
end)
end
defp add_operator(filters, 0), do: filters
defp add_operator(filters, index) do
Map.put(filters, String.to_existing_atom("topic#{index - 1}_#{index}_opr"), "and")
end
defp cast_topics(topics) when is_list(topics) do
case EctoType.cast({:array, Data}, topics) do
{:ok, data} -> {:ok, Enum.map(data, &to_string/1)}
:error -> :error
end
end
defp cast_topics(topic) do
case Data.cast(topic) do
{:ok, data} -> {:ok, to_string(data)}
:error -> :error
end
end
defp responses(requests) do
Enum.map(requests, fn request ->
with {:id, {:ok, id}} <- {:id, Map.fetch(request, "id")},
{:request, {:ok, result}} <- {:request, do_eth_request(request)} do
format_success(result, id)
else
{:id, :error} -> format_error("id is a required field", 0)
{:request, {:error, message}} -> format_error(message, Map.get(request, "id"))
end
end)
end
defp logs_blocks_filter(filter_options) do
with {:filter, %{"blockHash" => block_hash_param}} <- {:filter, filter_options},
{:block_hash, {:ok, block_hash}} <- {:block_hash, Hash.Full.cast(block_hash_param)},
{:block, %{number: number}} <- {:block, Repo.get(Block, block_hash)} do
{:ok, number, number}
else
{:filter, filters} ->
from_block = Map.get(filters, "fromBlock", "latest")
to_block = Map.get(filters, "toBlock", "latest")
max_block_number =
if from_block == "latest" || to_block == "latest" do
max_consensus_block_number()
end
pending_block_number =
if from_block == "pending" || to_block == "pending" do
max_non_consensus_block_number(max_block_number)
end
if is_nil(pending_block_number) && from_block == "pending" && to_block == "pending" do
{:error, :empty}
else
to_block_numbers(from_block, to_block, max_block_number, pending_block_number)
end
{:block, _} ->
{:error, "Invalid Block Hash"}
{:block_hash, _} ->
{:error, "Invalid Block Hash"}
end
end
defp to_block_numbers(from_block, to_block, max_block_number, pending_block_number) do
actual_pending_block_number = pending_block_number || max_block_number
with {:ok, from} <-
to_block_number(from_block, max_block_number, actual_pending_block_number),
{:ok, to} <- to_block_number(to_block, max_block_number, actual_pending_block_number) do
{:ok, from, to}
end
end
defp to_block_number(integer, _, _) when is_integer(integer), do: {:ok, integer}
defp to_block_number("latest", max_block_number, _), do: {:ok, max_block_number || 0}
defp to_block_number("earliest", _, _), do: {:ok, 0}
defp to_block_number("pending", max_block_number, nil), do: {:ok, max_block_number || 0}
defp to_block_number("pending", _, pending), do: {:ok, pending}
defp to_block_number("0x" <> number, _, _) do
case Integer.parse(number, 16) do
{integer, ""} -> {:ok, integer}
_ -> {:error, "invalid block number"}
end
end
defp to_block_number(number, _, _) when is_bitstring(number) do
case Integer.parse(number, 16) do
{integer, ""} -> {:ok, integer}
_ -> {:error, "invalid block number"}
end
end
defp to_block_number(_, _, _), do: {:error, "invalid block number"}
defp max_non_consensus_block_number(max) do
case Chain.max_non_consensus_block_number(max) do
{:ok, number} -> number
_ -> nil
end
end
defp max_consensus_block_number do
case Chain.max_consensus_block_number() do
{:ok, number} -> number
_ -> nil
end
end
defp format_success(result, id) do
%{result: result, id: id}
end
defp format_error(message, id) do
%{error: message, id: id}
end
defp do_eth_request(%{"jsonrpc" => rpc_version}) when rpc_version != "2.0" do
{:error, "invalid rpc version"}
end
defp do_eth_request(%{"jsonrpc" => "2.0", "method" => method, "params" => params})
when is_list(params) do
with {:ok, action} <- get_action(method),
{:correct_arity, true} <-
{:correct_arity, :erlang.function_exported(__MODULE__, action, Enum.count(params))} do
apply(__MODULE__, action, params)
else
{:correct_arity, _} ->
{:error, "Incorrect number of params."}
_ ->
{:error, "Action not found."}
end
end
defp do_eth_request(%{"params" => _params, "method" => _}) do
{:error, "Invalid params. Params must be a list."}
end
defp do_eth_request(_) do
{:error, "Method, params, and jsonrpc, are all required parameters."}
end
defp get_action(action) do
case Map.get(@methods, action) do
%{action: action} ->
{:ok, action}
_ ->
:error
end
end
defp block_param("latest"), do: {:ok, :latest}
defp block_param("earliest"), do: {:ok, :earliest}
defp block_param("pending"), do: {:ok, :pending}
defp block_param(string_integer) when is_bitstring(string_integer) do
case Integer.parse(string_integer) do
{integer, ""} -> {:ok, integer}
_ -> :error
end
end
defp block_param(nil), do: {:ok, :latest}
defp block_param(_), do: :error
end

@ -10,7 +10,7 @@ defmodule BlockScoutWeb.API.RPC.TransactionController do
{:format, {:ok, transaction_hash}} <- to_transaction_hash(txhash_param),
{:transaction, {:ok, transaction}} <- transaction_from_hash(transaction_hash),
paging_options <- paging_options(params) do
logs = Chain.transaction_to_logs(transaction, paging_options)
logs = Chain.transaction_to_logs(transaction_hash, paging_options)
{logs, next_page} = split_list_by_page(logs)
render(conn, :gettxinfo, %{

@ -7,8 +7,9 @@ defmodule BlockScoutWeb.API.V1.DecompiledSmartContractController do
def create(conn, params) do
if auth_token(conn) == actual_token() do
with {:ok, hash} <- validate_address_hash(params["address_hash"]),
:ok <- smart_contract_exists?(hash),
:ok <- decompiled_contract_exists?(params["address_hash"], params["decompiler_version"]) do
:ok <- Chain.check_address_exists(hash),
{:contract, :not_found} <-
{:contract, Chain.check_decompiled_contract_exists(params["address_hash"], params["decompiler_version"])} do
case Chain.create_decompiled_smart_contract(params) do
{:ok, decompiled_smart_contract} ->
send_resp(conn, :created, Jason.encode!(decompiled_smart_contract))
@ -29,7 +30,7 @@ defmodule BlockScoutWeb.API.V1.DecompiledSmartContractController do
:not_found ->
send_resp(conn, :unprocessable_entity, encode(%{error: "address is not found"}))
:contract_exists ->
{:contract, :ok} ->
send_resp(
conn,
:unprocessable_entity,
@ -41,13 +42,6 @@ defmodule BlockScoutWeb.API.V1.DecompiledSmartContractController do
end
end
defp smart_contract_exists?(address_hash) do
case Chain.hash_to_address(address_hash) do
{:ok, _address} -> :ok
_ -> :not_found
end
end
defp validate_address_hash(address_hash) do
case Address.cast(address_hash) do
{:ok, hash} -> {:ok, hash}
@ -55,13 +49,6 @@ defmodule BlockScoutWeb.API.V1.DecompiledSmartContractController do
end
end
defp decompiled_contract_exists?(address_hash, decompiler_version) do
case Chain.decompiled_code(address_hash, decompiler_version) do
{:ok, _} -> :contract_exists
_ -> :ok
end
end
defp auth_token(conn) do
case get_req_header(conn, "auth_token") do
[token] -> token

@ -4,19 +4,22 @@ defmodule BlockScoutWeb.API.V1.HealthController do
alias Explorer.Chain
def health(conn, _) do
with {:ok, number, timestamp} <- Chain.last_block_status() do
send_resp(conn, :ok, result(number, timestamp))
with {:ok, number, timestamp} <- Chain.last_db_block_status(),
{:ok, cache_number, cache_timestamp} <- Chain.last_cache_block_status() do
send_resp(conn, :ok, result(number, timestamp, cache_number, cache_timestamp))
else
status -> send_resp(conn, :internal_server_error, error(status))
end
end
def result(number, timestamp) do
def result(number, timestamp, cache_number, cache_timestamp) do
%{
"healthy" => true,
"data" => %{
"latest_block_number" => to_string(number),
"latest_block_inserted_at" => to_string(timestamp)
"latest_block_inserted_at" => to_string(timestamp),
"cache_latest_block_number" => to_string(cache_number),
"cache_latest_block_inserted_at" => to_string(cache_timestamp)
}
}
|> Jason.encode!()

@ -7,8 +7,8 @@ defmodule BlockScoutWeb.API.V1.VerifiedSmartContractController do
def create(conn, params) do
with {:ok, hash} <- validate_address_hash(params["address_hash"]),
:ok <- smart_contract_exists?(hash),
:ok <- verified_smart_contract_exists?(hash) do
:ok <- Chain.check_address_exists(hash),
{:contract, :not_found} <- {:contract, Chain.check_verified_smart_contract_exists(hash)} do
external_libraries = fetch_external_libraries(params)
case Publisher.publish(hash, params, external_libraries) do
@ -31,7 +31,7 @@ defmodule BlockScoutWeb.API.V1.VerifiedSmartContractController do
:not_found ->
send_resp(conn, :unprocessable_entity, encode(%{error: "address is not found"}))
:contract_exists ->
{:contract, :ok} ->
send_resp(
conn,
:unprocessable_entity,
@ -40,13 +40,6 @@ defmodule BlockScoutWeb.API.V1.VerifiedSmartContractController do
end
end
defp smart_contract_exists?(address_hash) do
case Chain.hash_to_address(address_hash) do
{:ok, _address} -> :ok
_ -> :not_found
end
end
defp validate_address_hash(address_hash) do
case Address.cast(address_hash) do
{:ok, hash} -> {:ok, hash}
@ -54,14 +47,6 @@ defmodule BlockScoutWeb.API.V1.VerifiedSmartContractController do
end
end
defp verified_smart_contract_exists?(address_hash) do
if Chain.address_hash_to_smart_contract(address_hash) do
:contract_exists
else
:ok
end
end
defp encode(data) do
Jason.encode!(data)
end

@ -1,8 +1,8 @@
defmodule BlockScoutWeb.APIDocsController do
use BlockScoutWeb, :controller
alias BlockScoutWeb.API.RPC.EthController
alias BlockScoutWeb.Etherscan
alias Explorer.EthRPC
def index(conn, _params) do
conn
@ -12,7 +12,7 @@ defmodule BlockScoutWeb.APIDocsController do
def eth_rpc(conn, _params) do
conn
|> assign(:documentation, EthController.methods())
|> assign(:documentation, EthRPC.methods())
|> render("eth_rpc.html")
end
end

@ -11,57 +11,57 @@ defmodule BlockScoutWeb.BlockTransactionController do
alias Phoenix.View
def index(conn, %{"block_hash_or_number" => formatted_block_hash_or_number, "type" => "JSON"} = params) do
with {:ok, block} <-
param_block_hash_or_number_to_block(formatted_block_hash_or_number, []) do
full_options =
Keyword.merge(
[
necessity_by_association: %{
:block => :optional,
[created_contract_address: :names] => :optional,
[from_address: :names] => :required,
[to_address: :names] => :optional
}
],
paging_options(params)
)
transactions_plus_one = Chain.block_to_transactions(block, full_options)
{transactions, next_page} = split_list_by_page(transactions_plus_one)
next_page_path =
case next_page_params(next_page, transactions, params) do
nil ->
nil
case param_block_hash_or_number_to_block(formatted_block_hash_or_number, []) do
{:ok, block} ->
full_options =
Keyword.merge(
[
necessity_by_association: %{
:block => :optional,
[created_contract_address: :names] => :optional,
[from_address: :names] => :required,
[to_address: :names] => :optional
}
],
paging_options(params)
)
next_page_params ->
block_transaction_path(
conn,
:index,
block,
Map.delete(next_page_params, "type")
transactions_plus_one = Chain.block_to_transactions(block.hash, full_options)
{transactions, next_page} = split_list_by_page(transactions_plus_one)
next_page_path =
case next_page_params(next_page, transactions, params) do
nil ->
nil
next_page_params ->
block_transaction_path(
conn,
:index,
block,
Map.delete(next_page_params, "type")
)
end
items =
transactions
|> Enum.map(fn transaction ->
View.render_to_string(
TransactionView,
"_tile.html",
transaction: transaction
)
end
items =
transactions
|> Enum.map(fn transaction ->
View.render_to_string(
TransactionView,
"_tile.html",
transaction: transaction
)
end)
json(
conn,
%{
items: items,
next_page_path: next_page_path
}
)
else
end)
json(
conn,
%{
items: items,
next_page_path: next_page_path
}
)
{:error, {:invalid, :hash}} ->
not_found(conn)
@ -80,25 +80,25 @@ defmodule BlockScoutWeb.BlockTransactionController do
end
def index(conn, %{"block_hash_or_number" => formatted_block_hash_or_number}) do
with {:ok, block} <-
param_block_hash_or_number_to_block(formatted_block_hash_or_number,
necessity_by_association: %{
[miner: :names] => :required,
:uncles => :optional,
:nephews => :optional,
:rewards => :optional
}
) do
block_transaction_count = Chain.block_to_transaction_count(block)
render(
conn,
"index.html",
block: block,
block_transaction_count: block_transaction_count,
current_path: current_path(conn)
)
else
case param_block_hash_or_number_to_block(formatted_block_hash_or_number,
necessity_by_association: %{
[miner: :names] => :required,
:uncles => :optional,
:nephews => :optional,
:rewards => :optional
}
) do
{:ok, block} ->
block_transaction_count = Chain.block_to_transaction_count(block.hash)
render(
conn,
"index.html",
block: block,
block_transaction_count: block_transaction_count,
current_path: current_path(conn)
)
{:error, {:invalid, :hash}} ->
not_found(conn)
@ -117,19 +117,23 @@ defmodule BlockScoutWeb.BlockTransactionController do
end
defp param_block_hash_or_number_to_block("0x" <> _ = param, options) do
with {:ok, hash} <- string_to_block_hash(param) do
hash_to_block(hash, options)
else
:error -> {:error, {:invalid, :hash}}
case string_to_block_hash(param) do
{:ok, hash} ->
hash_to_block(hash, options)
:error ->
{:error, {:invalid, :hash}}
end
end
defp param_block_hash_or_number_to_block(number_string, options)
when is_binary(number_string) do
with {:ok, number} <- BlockScoutWeb.Chain.param_to_block_number(number_string) do
number_to_block(number, options)
else
{:error, :invalid} -> {:error, {:invalid, :number}}
case BlockScoutWeb.Chain.param_to_block_number(number_string) do
{:ok, number} ->
number_to_block(number, options)
{:error, :invalid} ->
{:error, {:invalid, :number}}
end
end

@ -5,7 +5,7 @@ defmodule BlockScoutWeb.Chain.MarketHistoryChartController do
alias Explorer.ExchangeRates.Token
def show(conn, _params) do
with true <- ajax?(conn) do
if ajax?(conn) do
exchange_rate = Market.get_exchange_rate(Explorer.coin()) || Token.null()
recent_market_history = Market.fetch_recent_history()
@ -24,7 +24,7 @@ defmodule BlockScoutWeb.Chain.MarketHistoryChartController do
supply_data: available_supply(Chain.supply_for_days(), exchange_rate)
})
else
_ -> unprocessable_entity(conn)
unprocessable_entity(conn)
end
end

@ -2,7 +2,7 @@ defmodule BlockScoutWeb.ChainController do
use BlockScoutWeb, :controller
alias BlockScoutWeb.ChainView
alias Explorer.{Chain, PagingOptions, Repo}
alias Explorer.{Chain, PagingOptions}
alias Explorer.Chain.{Address, Block, Transaction}
alias Explorer.Chain.Supply.RSK
alias Explorer.Counters.AverageBlockTime
@ -52,6 +52,8 @@ defmodule BlockScoutWeb.ChainController do
end
end
def search(conn, _), do: not_found(conn)
def token_autocomplete(conn, %{"q" => term}) when is_binary(term) do
if term == "" do
json(conn, "{}")
@ -72,9 +74,15 @@ defmodule BlockScoutWeb.ChainController do
def chain_blocks(conn, _params) do
if ajax?(conn) do
blocks =
[paging_options: %PagingOptions{page_size: 4}]
[
paging_options: %PagingOptions{page_size: 4},
necessity_by_association: %{
[miner: :names] => :optional,
:transactions => :optional,
:rewards => :optional
}
]
|> Chain.list_blocks()
|> Repo.preload([[miner: :names], :transactions, :rewards])
|> Enum.map(fn block ->
%{
chain_block_html:

@ -6,7 +6,7 @@ defmodule BlockScoutWeb.RecentTransactionsController do
alias Phoenix.View
def index(conn, _params) do
with true <- ajax?(conn) do
if ajax?(conn) do
recent_transactions =
Chain.recent_collated_transactions(
necessity_by_association: %{
@ -29,7 +29,7 @@ defmodule BlockScoutWeb.RecentTransactionsController do
json(conn, %{transactions: transactions})
else
_ -> unprocessable_entity(conn)
unprocessable_entity(conn)
end
end
end

@ -30,15 +30,18 @@ defmodule BlockScoutWeb.SmartContractController do
end
end
def index(conn, _), do: not_found(conn)
def show(conn, params) do
with true <- ajax?(conn),
{:ok, address_hash} <- Chain.string_to_address_hash(params["id"]),
{:ok, _address} <- Chain.find_contract_address(address_hash),
outputs =
Reader.query_function(
address_hash,
%{name: params["function_name"], args: params["args"]}
) do
:ok <- Chain.check_contract_address_exists(address_hash) do
outputs =
Reader.query_function(
address_hash,
%{name: params["function_name"], args: params["args"]}
)
conn
|> put_status(200)
|> put_layout(false)
@ -51,7 +54,7 @@ defmodule BlockScoutWeb.SmartContractController do
:error ->
unprocessable_entity(conn)
{:error, :not_found} ->
:not_found ->
not_found(conn)
_ ->

@ -62,15 +62,15 @@ defmodule BlockScoutWeb.TransactionController do
def show(conn, %{"id" => id}) do
with {:ok, transaction_hash} <- Chain.string_to_transaction_hash(id),
{:ok, %Chain.Transaction{} = transaction} <- Chain.hash_to_transaction(transaction_hash) do
if Chain.transaction_has_token_transfers?(transaction.hash) do
:ok <- Chain.check_transaction_exists(transaction_hash) do
if Chain.transaction_has_token_transfers?(transaction_hash) do
redirect(conn, to: transaction_token_transfer_path(conn, :index, id))
else
redirect(conn, to: transaction_internal_transaction_path(conn, :index, id))
end
else
:error -> conn |> put_status(422) |> render("invalid.html", transaction_hash: id)
{:error, :not_found} -> conn |> put_status(404) |> render("not_found.html", transaction_hash: id)
:not_found -> conn |> put_status(404) |> render("not_found.html", transaction_hash: id)
end
end
end

@ -10,7 +10,7 @@ defmodule BlockScoutWeb.TransactionInternalTransactionController do
def index(conn, %{"transaction_id" => hash_string, "type" => "JSON"} = params) do
with {:ok, hash} <- Chain.string_to_transaction_hash(hash_string),
{:ok, transaction} <- Chain.hash_to_transaction(hash) do
:ok <- Chain.check_transaction_exists(hash) do
full_options =
Keyword.merge(
[
@ -24,7 +24,7 @@ defmodule BlockScoutWeb.TransactionInternalTransactionController do
paging_options(params)
)
internal_transactions_plus_one = Chain.transaction_to_internal_transactions(transaction, full_options)
internal_transactions_plus_one = Chain.transaction_to_internal_transactions(hash, full_options)
{internal_transactions, next_page} = split_list_by_page(internal_transactions_plus_one)
@ -37,7 +37,7 @@ defmodule BlockScoutWeb.TransactionInternalTransactionController do
transaction_internal_transaction_path(
conn,
:index,
transaction,
hash,
Map.delete(next_page_params, "type")
)
end
@ -66,7 +66,7 @@ defmodule BlockScoutWeb.TransactionInternalTransactionController do
|> put_view(TransactionView)
|> render("invalid.html", transaction_hash: hash_string)
{:error, :not_found} ->
:not_found ->
conn
|> put_status(404)
|> put_view(TransactionView)

@ -11,7 +11,9 @@ defmodule BlockScoutWeb.TransactionLogController do
def index(conn, %{"transaction_id" => transaction_hash_string, "type" => "JSON"} = params) do
with {:ok, transaction_hash} <- Chain.string_to_transaction_hash(transaction_hash_string),
{:ok, transaction} <-
Chain.hash_to_transaction(transaction_hash) do
Chain.hash_to_transaction(transaction_hash,
necessity_by_association: %{[to_address: :smart_contract] => :optional}
) do
full_options =
Keyword.merge(
[
@ -22,7 +24,7 @@ defmodule BlockScoutWeb.TransactionLogController do
paging_options(params)
)
logs_plus_one = Chain.transaction_to_logs(transaction, full_options)
logs_plus_one = Chain.transaction_to_logs(transaction_hash, full_options)
{logs, next_page} = split_list_by_page(logs_plus_one)

@ -19,7 +19,7 @@ defmodule BlockScoutWeb.TransactionRawTraceController do
:token_transfers => :optional
}
) do
internal_transactions = Chain.transaction_to_internal_transactions(transaction)
internal_transactions = Chain.transaction_to_internal_transactions(hash)
render(
conn,

@ -10,8 +10,7 @@ defmodule BlockScoutWeb.TransactionTokenTransferController do
def index(conn, %{"transaction_id" => hash_string, "type" => "JSON"} = params) do
with {:ok, hash} <- Chain.string_to_transaction_hash(hash_string),
{:ok, transaction} <-
Chain.hash_to_transaction(hash) do
:ok <- Chain.check_transaction_exists(hash) do
full_options =
Keyword.merge(
[
@ -24,7 +23,7 @@ defmodule BlockScoutWeb.TransactionTokenTransferController do
paging_options(params)
)
token_transfers_plus_one = Chain.transaction_to_token_transfers(transaction, full_options)
token_transfers_plus_one = Chain.transaction_to_token_transfers(hash, full_options)
{token_transfers, next_page} = split_list_by_page(token_transfers_plus_one)
@ -34,7 +33,7 @@ defmodule BlockScoutWeb.TransactionTokenTransferController do
nil
next_page_params ->
transaction_token_transfer_path(conn, :index, transaction, Map.delete(next_page_params, "type"))
transaction_token_transfer_path(conn, :index, hash, Map.delete(next_page_params, "type"))
end
items =
@ -62,7 +61,7 @@ defmodule BlockScoutWeb.TransactionTokenTransferController do
|> put_view(TransactionView)
|> render("invalid.html", transaction_hash: hash_string)
{:error, :not_found} ->
:not_found ->
conn
|> put_status(404)
|> put_view(TransactionView)

@ -13,8 +13,8 @@ defmodule BlockScoutWeb.CSPHeader do
"content-security-policy" => "\
connect-src 'self' #{websocket_endpoints(conn)}; \
default-src 'self';\
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://nico-amsterdam.github.io;\
style-src 'self' 'unsafe-inline' 'unsafe-eval' https://fonts.googleapis.com https://nico-amsterdam.github.io;\
script-src 'self' 'unsafe-inline' 'unsafe-eval';\
style-src 'self' 'unsafe-inline' 'unsafe-eval' https://fonts.googleapis.com;\
img-src 'self' 'unsafe-inline' 'unsafe-eval' data:;\
font-src 'self' 'unsafe-inline' 'unsafe-eval' https://fonts.gstatic.com data:;\
"

@ -126,7 +126,7 @@ defmodule BlockScoutWeb.Notifier do
"""
def broadcast_blocks_indexed_ratio(ratio, finished?) do
Endpoint.broadcast("blocks:indexing", "index_status", %{
ratio: ratio,
ratio: Decimal.to_string(ratio),
finished: finished?
})
end

@ -261,6 +261,6 @@ defmodule BlockScoutWeb.Router do
get("/api_docs", APIDocsController, :index)
get("/eth_rpc_api_docs", APIDocsController, :eth_rpc)
get("/:page", PageNotFoundController, :index)
get("/*path", PageNotFoundController, :index)
end
end

@ -137,7 +137,7 @@ defmodule BlockScoutWeb.Schema.Types do
field(:r, :decimal)
field(:s, :decimal)
field(:status, :status)
field(:v, :integer)
field(:v, :decimal)
field(:value, :wei)
field(:from_address_hash, :address_hash)
field(:to_address_hash, :address_hash)

@ -21,7 +21,7 @@
<td class="stakes-td color-lighten">
<!-- percentage of coins from total supply -->
<%= if @total_supply do %>
(<%= balance_percentage(@address, @total_supply) %>)
<%= balance_percentage(@address, @total_supply) %>
<% end %>
</td>
<td class="stakes-td">

@ -38,7 +38,7 @@
</tr>
</thead>
<tbody data-items data-selector="top-addresses-list">
<%= render BlockScoutWeb.CommonComponentsView, "_table-loader.html" %>
</tbody>
</table>
</div>

@ -79,18 +79,20 @@
<%= gettext("Incoming Transactions") %>
<% else %>
<span data-selector="transaction-count">
<%= Cldr.Number.to_string!(@transaction_count, format: "#,###") %>
<%= BlockScoutWeb.Cldr.Number.to_string!(@transaction_count, format: "#,###") %>
</span>
<%= gettext("Transactions Sent") %>
<% end %>
</span>
<span class="address-detail-item">
<%= gettext("Last Balance Update: Block #") %><span data-selector="fetched-coin-balance-block-number"><%= @address.fetched_coin_balance_block_number %></span>
</span>
<%= if @address.fetched_coin_balance_block_number do %>
<span class="address-detail-item">
<%= gettext("Last Balance Update: Block #") %><span data-selector="fetched-coin-balance-block-number"><%= @address.fetched_coin_balance_block_number %></span>
</span>
<% end %>
<%= if validator?(@validation_count) do %>
<span class="address-detail-item">
<span data-selector="validation-count">
<%= Cldr.Number.to_string!(@validation_count, format: "#,###") %>
<%= BlockScoutWeb.Cldr.Number.to_string!(@validation_count, format: "#,###") %>
</span>
<%= gettext("Blocks Validated") %>
</span>

@ -40,7 +40,9 @@
</div>
</div>
<div data-selector="coin-balances-list" data-items></div>
<div data-selector="coin-balances-list" data-items>
<%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %>
</div>
<%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %>

@ -46,14 +46,18 @@
<dd class="col-sm-8 col-md-10"><%= @address.smart_contract.evm_version %></dd>
</dl>
<% end %>
<%= if @address.smart_contract.constructor_arguments do %>
<dl class="row">
<dt class="col-sm-4 col-md-2 text-muted"><%= gettext "Constructor arguments" %></dt>
<dd class="col-sm-8 col-md-10"><%= @address.smart_contract.constructor_arguments %></dd>
</dl>
<% end %>
</div>
<hr/>
<%= if @address.smart_contract.constructor_arguments do %>
<section>
<div class="d-flex justify-content-between align-items-baseline">
<h3><%= gettext "Constructor Arguments" %></h3>
</div>
<div class="tile tile-muted mb-4">
<pre class="pre-wrap pre-scrollable"><code class="nohighlight"><%= raw(format_constructor_arguments(@address.smart_contract)) %></code>
</pre>
</div>
</section>
<% end %>
<section>
<div class="d-flex justify-content-between align-items-baseline">
<h3><%= gettext "Contract source code" %></h3>
@ -116,7 +120,7 @@
<h3><%= gettext "External libraries" %></h3>
</div>
<div class="tile tile-muted mb-4">
<pre class="pre-wrap pre-scrollable"><code class="nohighlight"><%= format_external_libraries(@address.smart_contract.external_libraries) %></code>
<pre class="pre-wrap pre-scrollable"><code class="nohighlight"><%= raw(format_external_libraries(@address.smart_contract.external_libraries)) %></code>
</pre>
</div>
</section>

@ -66,7 +66,9 @@
</div>
</div>
<div data-items></div>
<div data-items>
<%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %>
</div>
<%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %>

@ -11,6 +11,80 @@
) %>
</h3>
</dd>
<%= case decode(@log, @log.transaction) do %>
<% {:error, :contract_not_verified} -> %>
<dt class="col-md-2"><%= gettext "Decoded" %></dt>
<dd class="col-md-10">
<div class="alert alert-info">
<%= gettext "To see decoded input data, the contract must be verified." %>
<%= case @log.transaction do %>
<% %{to_address: %{hash: hash}} -> %>
<%= gettext "Verify the contract " %><a href="<%= address_verify_contract_path(@conn, :new, hash)%>"><%= gettext "here" %></a>
<% _ -> %>
<%= nil %>
<% end %>
</div>
<% {:error, :could_not_decode} -> %>
<dt class="col-md-2"><%= gettext "Decoded" %></dt>
<dd class="col-md-10">
<div class="alert alert-danger">
<%= gettext "Failed to decode log data." %>
</div>
<% {:ok, method_id, text, mapping} -> %>
<dt class="col-md-2"><%= gettext "Decoded" %></dt>
<dd class="col-md-10">
<table summary="Transaction Info" class="table thead-light table-bordered transaction-input-table">
<tr>
<td>Method Id</td>
<td colspan="3"><code>0x<%= method_id %></code></td>
</tr>
<tr>
<td>Call</td>
<td colspan="3"><code><%= text %></code></td>
</tr>
</table>
<div class="table-responsive text-center">
<table style="color: black;" summary="<%= gettext "Log Data" %>" class="table thead-light table-bordered">
<tr>
<th scope="col"></th>
<th scope="col"><%= gettext "Name" %></th>
<th scope="col"><%= gettext "Type" %></th>
<th scope="col"><%= gettext "Indexed?" %></th>
<th scope="col"><%= gettext "Data" %></th>
<tr>
<%= for {name, type, indexed?, value} <- mapping do %>
<tr>
<th scope="row">
<%= case BlockScoutWeb.ABIEncodedValueView.copy_text(type, value) do %>
<% :error -> %>
<%= nil %>
<% copy_text -> %>
<span
aria-label='<%= gettext "Copy Value" %>'
class="btn-copy-ico"
data-clipboard-text="<%= copy_text %>"
data-placement="top"
data-toggle="tooltip"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32.5 32.5" width="32" height="32">
<path fill-rule="evenodd" d="M23.5 20.5a1 1 0 0 1-1-1v-9h-9a1 1 0 0 1 0-2h10a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1zm-3-7v10a1 1 0 0 1-1 1h-10a1 1 0 0 1-1-1v-10a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1zm-2 1h-8v8h8v-8z"/>
</svg>
</span>
<% end %>
</th>
<td><%= name %></td>
<td><%= type %></td>
<td><%= indexed? %></td>
<td>
<pre class="transaction-input-text tile"><code><%= BlockScoutWeb.ABIEncodedValueView.value_html(type, value) %></code></pre>
</td>
</tr>
<% end %>
</table>
</div>
<% _ -> %>
<%= nil %>
<% end %>
<dt class="col-md-2"><%= gettext "Topics" %></dt>
<dd class="col-md-10">
<div class="raw-transaction-log-topics">

@ -27,7 +27,9 @@
</div>
</div>
<div data-items></div>
<div data-items>
<%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %>
</div>
<%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %>

@ -19,7 +19,9 @@
</div>
</div>
<div data-items></div>
<div data-items>
<%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %>
</div>
<div class="transaction-bottom-panel">
<div csv-download class="download-all-transactions">

@ -21,7 +21,9 @@
</span>
</button>
<div data-items></div>
<div data-items>
<%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %>
</div>
<%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %>

@ -65,7 +65,9 @@
</div>
</div>
<div data-items></div>
<div data-items>
<%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %>
</div>
<div class="transaction-bottom-panel">
<div class="download-all-transactions">

@ -22,7 +22,9 @@
<%= gettext "Something went wrong, click to reload." %>
</span>
</button>
<div data-items data-selector="validations-list"></div>
<div data-items data-selector="validations-list">
<%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %>
</div>
<%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %>

@ -11,7 +11,9 @@
<%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %>
<div data-items></div>
<div data-items>
<%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %>
</div>
<div data-empty-response-message style="display: none;">
<span><%= gettext "There are no blocks." %></span>
</div>

@ -57,7 +57,7 @@
<dl class="row">
<dt class="col-sm-3 text-muted"><%= gettext "Difficulty" %></dt>
<dd class="col-sm-9">
<span><%= @block.difficulty |> Cldr.Number.to_string! %></span>
<span><%= @block.difficulty |> BlockScoutWeb.Cldr.Number.to_string! %></span>
</dd>
</dl>
@ -65,7 +65,7 @@
<!-- Total Difficulty -->
<dl class="row">
<dt class="col-sm-3 text-muted"><%= gettext "Total Difficulty" %></dt>
<dd class="col-sm-9"><span><%= @block.total_difficulty |> Cldr.Number.to_string! %></span></dd>
<dd class="col-sm-9"><span><%= @block.total_difficulty |> BlockScoutWeb.Cldr.Number.to_string! %></span></dd>
</dl>
<!-- Nonce -->
@ -97,14 +97,14 @@
<dl class="row">
<dt class="col-sm-3 text-muted"><%= gettext "Gas Used" %></dt>
<dd class="col-sm-9">
<span><%= @block.gas_used |> Cldr.Number.to_string! %></span>
<span class="text-muted">(<%= (Decimal.to_integer(@block.gas_used) / Decimal.to_integer(@block.gas_limit)) |> Cldr.Number.to_string!(format: "#.#%") %>)</span>
<span><%= @block.gas_used |> BlockScoutWeb.Cldr.Number.to_string! %></span>
<span class="text-muted">(<%= (Decimal.to_integer(@block.gas_used) / Decimal.to_integer(@block.gas_limit)) |> BlockScoutWeb.Cldr.Number.to_string!(format: "#.#%") %>)</span>
</dt>
</dl>
<dl class="row mb-0">
<dt class="col-sm-3 text-muted"><%= gettext "Gas Limit" %></dt>
<dd class="col-sm-9">
<span><%= Cldr.Number.to_string!(@block.gas_limit) %></span>
<span><%= BlockScoutWeb.Cldr.Number.to_string!(@block.gas_limit) %></span>
</dd>
</dl>
<% end %>
@ -146,10 +146,10 @@
<h2 class="card-title balance-card-title"><%= gettext "Gas Used" %></h2>
<div class="text-right">
<h3 class="address-balance-text">
<%= @block.gas_used |> Cldr.Number.to_string! %>
<span class="text-muted">(<%= (Decimal.to_integer(@block.gas_used) / Decimal.to_integer(@block.gas_limit)) |> Cldr.Number.to_string!(format: "#.#%") %>)</span>
<%= @block.gas_used |> BlockScoutWeb.Cldr.Number.to_string! %>
<span class="text-muted">(<%= (Decimal.to_integer(@block.gas_used) / Decimal.to_integer(@block.gas_limit)) |> BlockScoutWeb.Cldr.Number.to_string!(format: "#.#%") %>)</span>
</h3>
<p class="address-current-balance"><%= @block.gas_limit |> Cldr.Number.to_string! %><%= gettext "Gas Limit" %></p>
<p class="address-current-balance"><%= @block.gas_limit |> BlockScoutWeb.Cldr.Number.to_string! %><%= gettext "Gas Limit" %></p>
</div>
<% end %>
</div>

@ -29,7 +29,9 @@
</div>
</div>
<div data-items></div>
<div data-items>
<%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %>
</div>
<%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %>

@ -5,7 +5,7 @@
<div class="dashboard-banner-network-graph">
<!-- Graph -->
<div class="dashboard-banner-chart">
<div data-chart-loading-message class="tile tile-muted text-center mt-5">
<div hidden data-chart-loading-message class="tile tile-muted text-center mt-5">
<span class="loading-spinner-small mr-2">
<span class="loading-spinner-block-1"></span>
<span class="loading-spinner-block-2"></span>
@ -56,7 +56,7 @@
<%= gettext "Total transactions" %>
</span>
<span class="dashboard-banner-network-stats-value" data-selector="transaction-count">
<%= Cldr.Number.to_string!(@transaction_estimated_count, format: "#,###") %>
<%= BlockScoutWeb.Cldr.Number.to_string!(@transaction_estimated_count, format: "#,###") %>
</span>
</div>
<div class="dashboard-banner-network-stats-item dashboard-banner-network-stats-item-3">
@ -64,7 +64,7 @@
<%= gettext "Total blocks" %>
</span>
<span class="dashboard-banner-network-stats-value" data-selector="block-count">
<%= Cldr.Number.to_string!(@block_count, format: "#,###") %>
<%= BlockScoutWeb.Cldr.Number.to_string!(@block_count, format: "#,###") %>
</span>
</div>
<div class="dashboard-banner-network-stats-item dashboard-banner-network-stats-item-4">
@ -72,7 +72,7 @@
<%= gettext "Wallet addresses" %>
</span>
<span class="dashboard-banner-network-stats-value" data-selector="address-count">
<%= Cldr.Number.to_string!(@address_count, format: "#,###") %>
<%= BlockScoutWeb.Cldr.Number.to_string!(@address_count, format: "#,###") %>
</span>
</div>
</div>
@ -91,8 +91,8 @@
<%= gettext "Something went wrong, click to reload." %>
</span>
</button>
<div data-selector="loading-message" class="tile tile-muted text-center mt-3 w-100">
<span class="loading-spinner-small mr-2">
<div hidden data-selector="loading-message" class="tile tile-muted text-center mt-3 w-100" >
<span class="loading-spinner-small mr-2">
<span class="loading-spinner-block-1"></span>
<span class="loading-spinner-block-2"></span>
</span>
@ -117,12 +117,8 @@
<%= gettext "Something went wrong, click to retry." %>
</span>
</button>
<div data-selector="loading-message" class="tile tile-muted text-center mt-3">
<span class="loading-spinner-small mr-2">
<span class="loading-spinner-block-1"></span>
<span class="loading-spinner-block-2"></span>
</span>
<%= gettext("Loading...") %>
<div hidden data-selector="loading-message">
<%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %>
</div>
</span>
</div>

@ -14,15 +14,14 @@
<!-- Pagination -->
<ul class="pagination">
<!-- First -->
<%= if assigns[:first_page_path] do %>
<li class="page-item">
<a
<%= if !assigns[:first_page_path] do %>disabled<% end %>
class="page-link"
href='<%= "#{assigns[:first_page_path]}" %>'
>First</a>
</li>
<% end %>
<li class="page-item">
<a
disabled
class="page-link"
href
data-first-page-button
>First</a>
</li>
<!-- Previous -->
<li class="page-item">
<a

@ -0,0 +1,85 @@
<tr class="table-content-pseudo">
<td class="stakes-td">
<span class="table-content-loader"></span>
</td>
<td class="stakes-td">
<span class="table-content-loader"></span>
</td>
<td class="stakes-td">
<span class="table-content-loader"></span>
</td>
<td class="stakes-td">
<span class="table-content-loader"></span>
</td>
<td class="stakes-td">
<span class="table-content-loader"></span>
</td>
</tr>
<tr class="table-content-pseudo">
<td class="stakes-td">
<span class="table-content-loader"></span>
</td>
<td class="stakes-td">
<span class="table-content-loader"></span>
</td>
<td class="stakes-td">
<span class="table-content-loader"></span>
</td>
<td class="stakes-td">
<span class="table-content-loader"></span>
</td>
<td class="stakes-td">
<span class="table-content-loader"></span>
</td>
</tr>
<tr class="table-content-pseudo">
<td class="stakes-td">
<span class="table-content-loader"></span>
</td>
<td class="stakes-td">
<span class="table-content-loader"></span>
</td>
<td class="stakes-td">
<span class="table-content-loader"></span>
</td>
<td class="stakes-td">
<span class="table-content-loader"></span>
</td>
<td class="stakes-td">
<span class="table-content-loader"></span>
</td>
</tr>
<tr class="table-content-pseudo">
<td class="stakes-td">
<span class="table-content-loader"></span>
</td>
<td class="stakes-td">
<span class="table-content-loader"></span>
</td>
<td class="stakes-td">
<span class="table-content-loader"></span>
</td>
<td class="stakes-td">
<span class="table-content-loader"></span>
</td>
<td class="stakes-td">
<span class="table-content-loader"></span>
</td>
</tr>
<tr class="table-content-pseudo">
<td class="stakes-td">
<span class="table-content-loader"></span>
</td>
<td class="stakes-td">
<span class="table-content-loader"></span>
</td>
<td class="stakes-td">
<span class="table-content-loader"></span>
</td>
<td class="stakes-td">
<span class="table-content-loader"></span>
</td>
<td class="stakes-td">
<span class="table-content-loader"></span>
</td>
</tr>

@ -0,0 +1,72 @@
<div data-loading-message data-selector="loading-message" class="tile tile-type-loading">
<div class="row tile-body">
<div class="tile-transaction-type-block col-md-2 d-flex flex-row flex-md-column">
<span class="tile-label">
<span class="tile-loader tile-label-loader"></span>
</span>
<span class="tile-status-label ml-2 ml-md-0">
<span class="tile-loader tile-label-loader"></span>
</span>
</div>
<div class="col-md-7 col-lg-8 d-flex flex-column pr-2 pr-sm-2 pr-md-0">
<span class="tile-loader tile-address-loader"></span>
<span class="tile-loader tile-address-loader"></span>
</div>
<div class="col-md-3 col-lg-2 d-flex flex-row flex-md-column flex-nowrap justify-content-center text-md-right mt-3 mt-md-0 tile-bottom">
<span class="mr-2 mr-md-0 order-1">
<span class="tile-loader tile-label-loader"></span>
</span>
<span class="mr-2 mr-md-0 order-2">
<span class="tile-loader tile-label-loader"></span>
</span>
</div>
</div>
</div>
<div data-loading-message data-selector="loading-message" class="tile tile-type-loading">
<div class="row tile-body">
<div class="tile-transaction-type-block col-md-2 d-flex flex-row flex-md-column">
<span class="tile-label">
<span class="tile-loader tile-label-loader"></span>
</span>
<span class="tile-status-label ml-2 ml-md-0">
<span class="tile-loader tile-label-loader"></span>
</span>
</div>
<div class="col-md-7 col-lg-8 d-flex flex-column pr-2 pr-sm-2 pr-md-0">
<span class="tile-loader tile-address-loader"></span>
<span class="tile-loader tile-address-loader"></span>
</div>
<div class="col-md-3 col-lg-2 d-flex flex-row flex-md-column flex-nowrap justify-content-center text-md-right mt-3 mt-md-0 tile-bottom">
<span class="mr-2 mr-md-0 order-1">
<span class="tile-loader tile-label-loader"></span>
</span>
<span class="mr-2 mr-md-0 order-2">
<span class="tile-loader tile-label-loader"></span>
</span>
</div>
</div>
</div>
<div data-loading-message data-selector="loading-message" class="tile tile-type-loading">
<div class="row tile-body">
<div class="tile-transaction-type-block col-md-2 d-flex flex-row flex-md-column">
<span class="tile-label">
<span class="tile-loader tile-label-loader"></span>
</span>
<span class="tile-status-label ml-2 ml-md-0">
<span class="tile-loader tile-label-loader"></span>
</span>
</div>
<div class="col-md-7 col-lg-8 d-flex flex-column pr-2 pr-sm-2 pr-md-0">
<span class="tile-loader tile-address-loader"></span>
<span class="tile-loader tile-address-loader"></span>
</div>
<div class="col-md-3 col-lg-2 d-flex flex-row flex-md-column flex-nowrap justify-content-center text-md-right mt-3 mt-md-0 tile-bottom">
<span class="mr-2 mr-md-0 order-1">
<span class="tile-loader tile-label-loader"></span>
</span>
<span class="mr-2 mr-md-0 order-2">
<span class="tile-loader tile-label-loader"></span>
</span>
</div>
</div>
</div>

@ -1,8 +1,22 @@
<nav class="navbar navbar-dark navbar-expand-lg navbar-primary" data-selector="navbar">
<link rel="preload" href="/css/awesomplete.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/awesomplete.css"></noscript>
<script src="/js/awesomplete.min.js"></script>
<script src="/js/awesomplete-util.min.js"></script>
<nav class="navbar navbar-dark navbar-expand-lg navbar-primary" data-selector="navbar" id="top-navbar">
<script>
if (localStorage.getItem("current-color-mode") === "dark") {
document.getElementById("top-navbar").style.backgroundColor = "#282945";
}
</script>
<div class="container-fluid navbar-container">
<%= link to: chain_path(@conn, :show), class: "navbar-brand", "data-test": "header_logo" do %>
<img class="navbar-logo" src="<%= logo() %>" alt="<%= subnetwork_title() %>" />
<img class="navbar-logo" id="navbar-logo" src="<%= logo() %>" alt="<%= subnetwork_title() %>" />
<% end %>
<script>
if (localStorage.getItem("current-color-mode") === "dark") {
document.getElementById("navbar-logo").style.filter = "brightness(0) invert(1)";
}
</script>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="<%= gettext("Toggle navigation") %>">
<span class="navbar-toggler-icon"></span>
</button>
@ -76,7 +90,7 @@
) %>
<%= link(
gettext("Eth RPC"),
class: "dropdown-item #{tab_status("api_docs", @conn.request_path)}",
class: "dropdown-item #{tab_status("eth_rpc_api_docs", @conn.request_path)}",
to: api_docs_path(@conn, :eth_rpc)
) %>
</div>
@ -90,8 +104,14 @@
</a>
</li>
</ul>
<!-- Dark mode changer -->
<button class="dark-mode-changer" id="dark-mode-changer">
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="16">
<path fill="#9B62FF" fill-rule="evenodd" d="M14.88 11.578a.544.544 0 0 0-.599-.166 5.7 5.7 0 0 1-1.924.321c-3.259 0-5.91-2.632-5.91-5.866 0-1.947.968-3.759 2.59-4.849a.534.534 0 0 0-.225-.97A5.289 5.289 0 0 0 8.059 0C3.615 0 0 3.588 0 8s3.615 8 8.059 8c2.82 0 5.386-1.423 6.862-3.806a.533.533 0 0 0-.041-.616z"/>
</svg>
</button>
<!-- Search navbar -->
<div class="search-form d-lg-flex d-inline-block">
<div class="search-form d-lg-flex d-inline-block" style="background-color: #22223a">
<%= form_for @conn, chain_path(@conn, :search), [class: "form-inline my-2 my-lg-0", method: :get, enforce_utf8: false], fn f -> %>
<div class="input-group" title='<%= gettext("Search by address, token symbol name, transaction hash, or block number") %>'>
<%= awesomplete(f, :q,
@ -117,9 +137,22 @@
</div>
</div>
<button class="btn btn-outline-success my-2 my-sm-0 sr-only hidden" type="submit"><%= gettext "Search" %></button>
<script>
if (localStorage.getItem("current-color-mode") === "dark") {
document.getElementById("q").style.backgroundColor = "#22223a";
document.getElementById("q").style.borderColor = "#22223a";
}
</script>
<% end %>
</div>
</div>
</div>
</nav>
<%= render BlockScoutWeb.LayoutView, "_network_selector.html" %>
<%= render BlockScoutWeb.LayoutView, "_network_selector.html" %>
<script>
if (localStorage.getItem("current-color-mode") === "dark") {
var modeChanger = document.getElementById("dark-mode-changer");
modeChanger.className += " " + "dark-mode-changer--dark";
document.body.className += " " + "dark-theme-applied";
}
</script>

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

Loading…
Cancel
Save