From cbb79679e05ebde5133af3e73c180d0308f202b0 Mon Sep 17 00:00:00 2001 From: Eduard Sachava Date: Wed, 16 Oct 2019 15:16:33 +0300 Subject: [PATCH] Implement logging in using MetaMask extension (#2408) We check Web3 every 0.1s to check for currently selected account. This is ugly and MetaMask developers admit that, but it's the only way they can recommend. Login button always redirects to metamask.io website, but if the MetaMask is detected, it's onclick is overridden to launch MetaMask's Privacy Mode unlock dialog. When an account is detected, it is sent to websocket and used for rendering top panel during further staking information updates. Putting account into session is not implemented, as it _might_ cause privacy issues. In the meanwhile, we'll try to think of more clever way for preventing login button blinking during page load. Note: npm cache is invalidated in CI to work around issue of websocket package breaking npm install due to .git directory being present. Co-Authored-By: Anatoly Nikiforov Co-Authored-By: Kirill Andreev --- .circleci/config.yml | 12 +- .../assets/css/components/stakes/_stakes.scss | 9 +- .../block_scout_web/assets/js/pages/stakes.js | 50 ++- apps/block_scout_web/assets/package-lock.json | 411 +++++++++++++++++- .../channels/stakes_channel.ex | 7 +- .../controllers/stakes_controller.ex | 18 +- .../_stakes_stats_item_account.html.eex | 26 ++ .../templates/stakes/_stakes_top.html.eex | 2 +- .../block_scout_web/views/currency_helpers.ex | 9 +- apps/explorer/lib/explorer/chain.ex | 54 ++- .../chain/address/current_token_balance.ex | 12 + 11 files changed, 572 insertions(+), 38 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_stats_item_account.html.eex diff --git a/.circleci/config.yml b/.circleci/config.yml index c7596f7d60..d3ee183bb3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -41,9 +41,9 @@ jobs: - restore_cache: keys: - - v7-npm-install-{{ .Branch }}-{{ checksum "apps/block_scout_web/assets/package-lock.json" }} - - v7-npm-install-{{ .Branch }} - - v7-npm-install + - v8-npm-install-{{ .Branch }}-{{ checksum "apps/block_scout_web/assets/package-lock.json" }} + - v8-npm-install-{{ .Branch }} + - v8-npm-install - run: command: npm install @@ -68,13 +68,13 @@ jobs: working_directory: "apps/block_scout_web/assets" - save_cache: - key: v7-npm-install-{{ .Branch }}-{{ checksum "apps/block_scout_web/assets/package-lock.json" }} + key: v8-npm-install-{{ .Branch }}-{{ checksum "apps/block_scout_web/assets/package-lock.json" }} paths: "apps/block_scout_web/assets/node_modules" - save_cache: - key: v7-npm-install-{{ .Branch }} + key: v8-npm-install-{{ .Branch }} paths: "apps/block_scout_web/assets/node_modules" - save_cache: - key: v7-npm-install + key: v8-npm-install paths: "apps/block_scout_web/assets/node_modules" - run: mix compile diff --git a/apps/block_scout_web/assets/css/components/stakes/_stakes.scss b/apps/block_scout_web/assets/css/components/stakes/_stakes.scss index 97f1323116..6a06bb6fc1 100644 --- a/apps/block_scout_web/assets/css/components/stakes/_stakes.scss +++ b/apps/block_scout_web/assets/css/components/stakes/_stakes.scss @@ -67,9 +67,12 @@ $stakes-stats-item-border-color: #fff !default; } .stakes-top-stats-login { - color: $secondary; - cursor: pointer; - margin-right: 8px; + &, &:hover { + color: $secondary; + text-decoration: none; + cursor: pointer; + margin-right: 8px; + } } .stakes-address-container { diff --git a/apps/block_scout_web/assets/js/pages/stakes.js b/apps/block_scout_web/assets/js/pages/stakes.js index e2579c5edf..7e71f0066d 100644 --- a/apps/block_scout_web/assets/js/pages/stakes.js +++ b/apps/block_scout_web/assets/js/pages/stakes.js @@ -5,9 +5,12 @@ import _ from 'lodash' import { subscribeChannel } from '../socket' import { connectElements } from '../lib/redux_helpers.js' import { createAsyncLoadStore } from '../lib/async_listing_load' +import Web3 from 'web3' export const initialState = { - channel: null + channel: null, + web3: null, + account: null } export function reducer (state = initialState, action) { @@ -19,6 +22,12 @@ export function reducer (state = initialState, action) { case 'CHANNEL_CONNECTED': { return Object.assign({}, state, { channel: action.channel }) } + case 'WEB3_DETECTED': { + return Object.assign({}, state, { web3: action.web3 }) + } + case 'ACCOUNT_UPDATED': { + return Object.assign({}, state, { account: action.account }) + } default: return state } @@ -35,8 +44,47 @@ if ($stakesPage.length) { const channel = subscribeChannel('stakes:staking_update') channel.on('staking_update', msg => onStakingUpdate(msg, store)) store.dispatch({ type: 'CHANNEL_CONNECTED', channel }) + + initializeWeb3(store) } function onStakingUpdate (msg, store) { $('[data-selector="stakes-top"]').html(msg.top_html) + + if (store.getState().web3) { + $('[data-selector="login-button"]').on('click', loginByMetamask) + } +} + +function initializeWeb3 (store) { + if (window.ethereum) { + const web3 = new Web3(window.ethereum) + store.dispatch({ type: 'WEB3_DETECTED', web3 }) + + setInterval(async function () { + const accounts = await web3.eth.getAccounts() + const account = accounts[0] ? accounts[0].toLowerCase() : null + + if (account !== store.getState().account) { + setAccount(account, store) + } + }, 100) + } +} + +function setAccount (account, store) { + store.dispatch({ type: 'ACCOUNT_UPDATED', account }) + store.getState().channel.push('set_account', account) +} + +async function loginByMetamask (event) { + event.stopPropagation() + event.preventDefault() + + try { + await window.ethereum.enable() + } catch (e) { + console.log(e) + console.error('User denied account access') + } } diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 191181211b..8a7b06dfe1 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -13403,6 +13403,29 @@ "xhr": "^2.3.3" } }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "servify": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/servify/-/servify-0.1.12.tgz", + "integrity": "sha512-/xE6GvsKKqyo1BAY+KxOWXcLpPsUUyji7Qg3bVD7hh1eRze5bR1uYiuDA/k3Gof1s9BTzQZEJK8sNcNGFIzeWw==", + "requires": { + "body-parser": "^1.16.0", + "cors": "^2.8.1", + "express": "^4.14.0", + "request": "^2.79.0", + "xhr": "^2.3.3" + } + }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -13451,6 +13474,21 @@ "safe-buffer": "^5.0.1" } }, + "sha3": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/sha3/-/sha3-1.2.3.tgz", + "integrity": "sha512-sOWDZi8cDBRkLfWOw18wvJyNblXDHzwMGnRWut8zNNeIeLnmMRO17bjpLc7OzMuj1ASUgx2IyohzUCAl+Kx5vA==", + "requires": { + "nan": "2.13.2" + }, + "dependencies": { + "nan": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", + "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==" + } + } + }, "shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -14543,8 +14581,7 @@ "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, "through2": { "version": "2.0.5", @@ -14602,6 +14639,11 @@ "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", "dev": true }, + "to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" + }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -14724,6 +14766,11 @@ "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" }, + "type": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/type/-/type-1.0.3.tgz", + "integrity": "sha512-51IMtNfVcee8+9GJvj0spSuFcZHe9vSib6Xtgsny1Km9ugyz2mbS08I3rsUIRYgJohFRFU1160sgRodYz378Hg==" + }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -15467,6 +15514,286 @@ } } }, + "web3": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/web3/-/web3-1.2.1.tgz", + "integrity": "sha512-nNMzeCK0agb5i/oTWNdQ1aGtwYfXzHottFP2Dz0oGIzavPMGSKyVlr8ibVb1yK5sJBjrWVnTdGaOC2zKDFuFRw==", + "requires": { + "web3-bzz": "1.2.1", + "web3-core": "1.2.1", + "web3-eth": "1.2.1", + "web3-eth-personal": "1.2.1", + "web3-net": "1.2.1", + "web3-shh": "1.2.1", + "web3-utils": "1.2.1" + } + }, + "web3-bzz": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.2.1.tgz", + "integrity": "sha512-LdOO44TuYbGIPfL4ilkuS89GQovxUpmLz6C1UC7VYVVRILeZS740FVB3j9V4P4FHUk1RenaDfKhcntqgVCHtjw==", + "requires": { + "got": "9.6.0", + "swarm-js": "0.1.39", + "underscore": "1.9.1" + } + }, + "web3-core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.2.1.tgz", + "integrity": "sha512-5ODwIqgl8oIg/0+Ai4jsLxkKFWJYE0uLuE1yUKHNVCL4zL6n3rFjRMpKPokd6id6nJCNgeA64KdWQ4XfpnjdMg==", + "requires": { + "web3-core-helpers": "1.2.1", + "web3-core-method": "1.2.1", + "web3-core-requestmanager": "1.2.1", + "web3-utils": "1.2.1" + } + }, + "web3-core-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.2.1.tgz", + "integrity": "sha512-Gx3sTEajD5r96bJgfuW377PZVFmXIH4TdqDhgGwd2lZQCcMi+DA4TgxJNJGxn0R3aUVzyyE76j4LBrh412mXrw==", + "requires": { + "underscore": "1.9.1", + "web3-eth-iban": "1.2.1", + "web3-utils": "1.2.1" + } + }, + "web3-core-method": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.2.1.tgz", + "integrity": "sha512-Ghg2WS23qi6Xj8Od3VCzaImLHseEA7/usvnOItluiIc5cKs00WYWsNy2YRStzU9a2+z8lwQywPYp0nTzR/QXdQ==", + "requires": { + "underscore": "1.9.1", + "web3-core-helpers": "1.2.1", + "web3-core-promievent": "1.2.1", + "web3-core-subscriptions": "1.2.1", + "web3-utils": "1.2.1" + } + }, + "web3-core-promievent": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.2.1.tgz", + "integrity": "sha512-IVUqgpIKoeOYblwpex4Hye6npM0aMR+kU49VP06secPeN0rHMyhGF0ZGveWBrGvf8WDPI7jhqPBFIC6Jf3Q3zw==", + "requires": { + "any-promise": "1.3.0", + "eventemitter3": "3.1.2" + } + }, + "web3-core-requestmanager": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.2.1.tgz", + "integrity": "sha512-xfknTC69RfYmLKC+83Jz73IC3/sS2ZLhGtX33D4Q5nQ8yc39ElyAolxr9sJQS8kihOcM6u4J+8gyGMqsLcpIBg==", + "requires": { + "underscore": "1.9.1", + "web3-core-helpers": "1.2.1", + "web3-providers-http": "1.2.1", + "web3-providers-ipc": "1.2.1", + "web3-providers-ws": "1.2.1" + } + }, + "web3-core-subscriptions": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.2.1.tgz", + "integrity": "sha512-nmOwe3NsB8V8UFsY1r+sW6KjdOS68h8nuh7NzlWxBQT/19QSUGiERRTaZXWu5BYvo1EoZRMxCKyCQpSSXLc08g==", + "requires": { + "eventemitter3": "3.1.2", + "underscore": "1.9.1", + "web3-core-helpers": "1.2.1" + } + }, + "web3-eth": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.2.1.tgz", + "integrity": "sha512-/2xly4Yry5FW1i+uygPjhfvgUP/MS/Dk+PDqmzp5M88tS86A+j8BzKc23GrlA8sgGs0645cpZK/999LpEF5UdA==", + "requires": { + "underscore": "1.9.1", + "web3-core": "1.2.1", + "web3-core-helpers": "1.2.1", + "web3-core-method": "1.2.1", + "web3-core-subscriptions": "1.2.1", + "web3-eth-abi": "1.2.1", + "web3-eth-accounts": "1.2.1", + "web3-eth-contract": "1.2.1", + "web3-eth-ens": "1.2.1", + "web3-eth-iban": "1.2.1", + "web3-eth-personal": "1.2.1", + "web3-net": "1.2.1", + "web3-utils": "1.2.1" + } + }, + "web3-eth-abi": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.2.1.tgz", + "integrity": "sha512-jI/KhU2a/DQPZXHjo2GW0myEljzfiKOn+h1qxK1+Y9OQfTcBMxrQJyH5AP89O6l6NZ1QvNdq99ThAxBFoy5L+g==", + "requires": { + "ethers": "4.0.0-beta.3", + "underscore": "1.9.1", + "web3-utils": "1.2.1" + } + }, + "web3-eth-accounts": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.2.1.tgz", + "integrity": "sha512-26I4qq42STQ8IeKUyur3MdQ1NzrzCqPsmzqpux0j6X/XBD7EjZ+Cs0lhGNkSKH5dI3V8CJasnQ5T1mNKeWB7nQ==", + "requires": { + "any-promise": "1.3.0", + "crypto-browserify": "3.12.0", + "eth-lib": "0.2.7", + "scryptsy": "2.1.0", + "semver": "6.2.0", + "underscore": "1.9.1", + "uuid": "3.3.2", + "web3-core": "1.2.1", + "web3-core-helpers": "1.2.1", + "web3-core-method": "1.2.1", + "web3-utils": "1.2.1" + }, + "dependencies": { + "eth-lib": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/eth-lib/-/eth-lib-0.2.7.tgz", + "integrity": "sha1-L5Pxex4jrsN1nNSj/iDBKGo/wco=", + "requires": { + "bn.js": "^4.11.6", + "elliptic": "^6.4.0", + "xhr-request-promise": "^0.1.2" + } + }, + "semver": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.2.0.tgz", + "integrity": "sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A==" + } + } + }, + "web3-eth-contract": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.2.1.tgz", + "integrity": "sha512-kYFESbQ3boC9bl2rYVghj7O8UKMiuKaiMkxvRH5cEDHil8V7MGEGZNH0slSdoyeftZVlaWSMqkRP/chfnKND0g==", + "requires": { + "underscore": "1.9.1", + "web3-core": "1.2.1", + "web3-core-helpers": "1.2.1", + "web3-core-method": "1.2.1", + "web3-core-promievent": "1.2.1", + "web3-core-subscriptions": "1.2.1", + "web3-eth-abi": "1.2.1", + "web3-utils": "1.2.1" + } + }, + "web3-eth-ens": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.2.1.tgz", + "integrity": "sha512-lhP1kFhqZr2nnbu3CGIFFrAnNxk2veXpOXBY48Tub37RtobDyHijHgrj+xTh+mFiPokyrapVjpFsbGa+Xzye4Q==", + "requires": { + "eth-ens-namehash": "2.0.8", + "underscore": "1.9.1", + "web3-core": "1.2.1", + "web3-core-helpers": "1.2.1", + "web3-core-promievent": "1.2.1", + "web3-eth-abi": "1.2.1", + "web3-eth-contract": "1.2.1", + "web3-utils": "1.2.1" + } + }, + "web3-eth-iban": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.2.1.tgz", + "integrity": "sha512-9gkr4QPl1jCU+wkgmZ8EwODVO3ovVj6d6JKMos52ggdT2YCmlfvFVF6wlGLwi0VvNa/p+0BjJzaqxnnG/JewjQ==", + "requires": { + "bn.js": "4.11.8", + "web3-utils": "1.2.1" + } + }, + "web3-eth-personal": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.2.1.tgz", + "integrity": "sha512-RNDVSiaSoY4aIp8+Hc7z+X72H7lMb3fmAChuSBADoEc7DsJrY/d0R5qQDK9g9t2BO8oxgLrLNyBP/9ub2Hc6Bg==", + "requires": { + "web3-core": "1.2.1", + "web3-core-helpers": "1.2.1", + "web3-core-method": "1.2.1", + "web3-net": "1.2.1", + "web3-utils": "1.2.1" + } + }, + "web3-net": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.2.1.tgz", + "integrity": "sha512-Yt1Bs7WgnLESPe0rri/ZoPWzSy55ovioaP35w1KZydrNtQ5Yq4WcrAdhBzcOW7vAkIwrsLQsvA+hrOCy7mNauw==", + "requires": { + "web3-core": "1.2.1", + "web3-core-method": "1.2.1", + "web3-utils": "1.2.1" + } + }, + "web3-providers-http": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.2.1.tgz", + "integrity": "sha512-BDtVUVolT9b3CAzeGVA/np1hhn7RPUZ6YYGB/sYky+GjeO311Yoq8SRDUSezU92x8yImSC2B+SMReGhd1zL+bQ==", + "requires": { + "web3-core-helpers": "1.2.1", + "xhr2-cookies": "1.1.0" + } + }, + "web3-providers-ipc": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.2.1.tgz", + "integrity": "sha512-oPEuOCwxVx8L4CPD0TUdnlOUZwGBSRKScCz/Ws2YHdr9Ium+whm+0NLmOZjkjQp5wovQbyBzNa6zJz1noFRvFA==", + "requires": { + "oboe": "2.1.4", + "underscore": "1.9.1", + "web3-core-helpers": "1.2.1" + } + }, + "web3-providers-ws": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.2.1.tgz", + "integrity": "sha512-oqsQXzu+ejJACVHy864WwIyw+oB21nw/pI65/sD95Zi98+/HQzFfNcIFneF1NC4bVF3VNX4YHTNq2I2o97LAiA==", + "requires": { + "underscore": "1.9.1", + "web3-core-helpers": "1.2.1", + "websocket": "github:web3-js/WebSocket-Node#polyfill/globalThis" + } + }, + "web3-shh": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.2.1.tgz", + "integrity": "sha512-/3Cl04nza5kuFn25bV3FJWa0s3Vafr5BlT933h26xovQ6HIIz61LmvNQlvX1AhFL+SNJOTcQmK1SM59vcyC8bA==", + "requires": { + "web3-core": "1.2.1", + "web3-core-method": "1.2.1", + "web3-core-subscriptions": "1.2.1", + "web3-net": "1.2.1" + } + }, + "web3-utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.2.1.tgz", + "integrity": "sha512-Mrcn3l58L+yCKz3zBryM6JZpNruWuT0OCbag8w+reeNROSGVlXzUQkU+gtAwc9JCZ7tKUyg67+2YUGqUjVcyBA==", + "requires": { + "bn.js": "4.11.8", + "eth-lib": "0.2.7", + "ethjs-unit": "0.1.6", + "number-to-bn": "1.7.0", + "randomhex": "0.1.5", + "underscore": "1.9.1", + "utf8": "3.0.0" + }, + "dependencies": { + "eth-lib": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/eth-lib/-/eth-lib-0.2.7.tgz", + "integrity": "sha1-L5Pxex4jrsN1nNSj/iDBKGo/wco=", + "requires": { + "bn.js": "^4.11.6", + "elliptic": "^6.4.0", + "xhr-request-promise": "^0.1.2" + } + } + } + }, "webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", @@ -15936,6 +16263,24 @@ } } }, + "websocket": { + "version": "github:web3-js/WebSocket-Node#905deb4812572b344f5801f8c9ce8bb02799d82e", + "from": "github:web3-js/WebSocket-Node#polyfill/globalThis", + "requires": { + "debug": "^2.2.0", + "es5-ext": "^0.10.50", + "nan": "^2.14.0", + "typedarray-to-buffer": "^3.1.5", + "yaeti": "^0.0.6" + }, + "dependencies": { + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" + } + } + }, "whatwg-encoding": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", @@ -16160,6 +16505,59 @@ "cookiejar": "^2.1.1" } }, + "xhr": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.5.0.tgz", + "integrity": "sha512-4nlO/14t3BNUZRXIXfXe+3N6w3s1KoxcJUUURctd64BLRe67E4gRwp4PjywtDY72fXpZ1y6Ch0VZQRY/gMPzzQ==", + "requires": { + "global": "~4.3.0", + "is-function": "^1.0.1", + "parse-headers": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "xhr-request": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xhr-request/-/xhr-request-1.1.0.tgz", + "integrity": "sha512-Y7qzEaR3FDtL3fP30k9wO/e+FBnBByZeybKOhASsGP30NIkRAAkKD/sCnLvgEfAIEC1rcmK7YG8f4oEnIrrWzA==", + "requires": { + "buffer-to-arraybuffer": "^0.0.5", + "object-assign": "^4.1.1", + "query-string": "^5.0.1", + "simple-get": "^2.7.0", + "timed-out": "^4.0.1", + "url-set-query": "^1.0.0", + "xhr": "^2.0.4" + }, + "dependencies": { + "query-string": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", + "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", + "requires": { + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + } + } + }, + "xhr-request-promise": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/xhr-request-promise/-/xhr-request-promise-0.1.2.tgz", + "integrity": "sha1-NDxE0e53JrhkgGloLQ+EDIO0Jh0=", + "requires": { + "xhr-request": "^1.0.1" + } + }, + "xhr2-cookies": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xhr2-cookies/-/xhr2-cookies-1.1.0.tgz", + "integrity": "sha1-fXdEnQmZGX8VXLc7I99yUF7YnUg=", + "requires": { + "cookiejar": "^2.1.1" + } + }, "xml-name-validator": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", @@ -16272,6 +16670,15 @@ "camelcase": "^5.0.0", "decamelize": "^1.2.0" } + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } } } } diff --git a/apps/block_scout_web/lib/block_scout_web/channels/stakes_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/stakes_channel.ex index 29c1b6ecb0..e656cc7415 100644 --- a/apps/block_scout_web/lib/block_scout_web/channels/stakes_channel.ex +++ b/apps/block_scout_web/lib/block_scout_web/channels/stakes_channel.ex @@ -12,9 +12,14 @@ defmodule BlockScoutWeb.StakesChannel do {:ok, %{}, socket} end + def handle_in("set_account", account, socket) do + socket = assign(socket, :account, account) + handle_out("staking_update", nil, socket) + end + def handle_out("staking_update", _data, socket) do push(socket, "staking_update", %{ - top_html: StakesController.render_top() + top_html: StakesController.render_top(socket) }) {:noreply, socket} diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/stakes_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/stakes_controller.ex index 3d77acfa36..868b20f3bf 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/stakes_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/stakes_controller.ex @@ -15,16 +15,28 @@ defmodule BlockScoutWeb.StakesController do render_template(assigns.filter, conn, params) end - def render_top do + def render_top(conn) do epoch_number = ContractState.get(:epoch_number, 0) epoch_end_block = ContractState.get(:epoch_end_block, 0) block_number = BlockNumber.get_max() + token = ContractState.get(:token, %Token{}) + + account = + if account_address = conn.assigns[:account] do + %{ + address: account_address, + balance: Chain.fetch_last_token_balance(account_address, token.contract_address_hash), + staked: Chain.get_total_staked(account_address), + pool: Chain.staking_pool(account_address) + } + end View.render_to_string(StakesView, "_stakes_top.html", epoch_number: epoch_number, epoch_end_in: epoch_end_block - block_number, block_number: block_number, - logged_in: false + account: account, + token: token ) end @@ -83,7 +95,7 @@ defmodule BlockScoutWeb.StakesController do defp render_template(filter, conn, _) do render(conn, "index.html", - top: render_top(), + top: render_top(conn), pools_type: filter, current_path: current_path(conn), average_block_time: AverageBlockTime.average_block_time() diff --git a/apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_stats_item_account.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_stats_item_account.html.eex new file mode 100644 index 0000000000..c363f2bbdd --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_stats_item_account.html.eex @@ -0,0 +1,26 @@ +
+ + <%= if @account do %> +
> + <%= binary_part(@account.address, 0, 13) %>... +
+
+ + + +
+ <% else %> + with MetaMask + <% end %> +
+ + <%= gettext "Balance" %>: + <%= format_according_to_decimals(@account[:balance], @token.decimals) %> <%= @token.symbol %> + + <%= gettext "Staked" %>: + <%= format_according_to_decimals(@account[:staked], @token.decimals) %> <%= @token.symbol %> + + +
\ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_top.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_top.html.eex index 806c3694a0..992904f271 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_top.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_top.html.eex @@ -4,10 +4,10 @@ <%= render BlockScoutWeb.StakesView, "_stakes_stats_item.html", title: gettext("Epoch number"), value: @epoch_number %> <%= render BlockScoutWeb.StakesView, "_stakes_stats_item.html", title: gettext("Block number"), value: @block_number %> <%= render BlockScoutWeb.StakesView, "_stakes_stats_item.html", title: gettext("Next epoch in"), value: ngettext("%{blocks} block", "%{blocks} blocks", @epoch_end_in, blocks: @epoch_end_in) %> + <%= render BlockScoutWeb.StakesView, "_stakes_stats_item_account.html", account: @account, token: @token %>
-<%= render BlockScoutWeb.StakesView, "_stakes_modal_become_candidate.html" %> \ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/views/currency_helpers.ex b/apps/block_scout_web/lib/block_scout_web/views/currency_helpers.ex index 2aad5b013c..2bb4117951 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/currency_helpers.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/currency_helpers.ex @@ -25,6 +25,9 @@ defmodule BlockScoutWeb.CurrencyHelpers do ## Examples + iex> format_according_to_decimals(nil, Decimal.new(5)) + "-" + iex> format_according_to_decimals(Decimal.new(20500000), Decimal.new(5)) "205" @@ -40,7 +43,11 @@ defmodule BlockScoutWeb.CurrencyHelpers do iex> format_according_to_decimals(205000, Decimal.new(2)) "2,050" """ - @spec format_according_to_decimals(non_neg_integer(), nil) :: String.t() + @spec format_according_to_decimals(non_neg_integer() | nil, nil) :: String.t() + def format_according_to_decimals(nil, _) do + "-" + end + def format_according_to_decimals(value, nil) do format_according_to_decimals(value, Decimal.new(0)) end diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 427dd70d34..9f50e91072 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -53,6 +53,7 @@ defmodule Explorer.Chain do PendingBlockOperation, SmartContract, StakingPool, + StakingPoolsDelegator, Token, Token.Instance, TokenTransfer, @@ -4349,6 +4350,13 @@ defmodule Explorer.Chain do end end + @spec fetch_last_token_balance(Hash.Address.t(), Hash.Address.t()) :: Decimal.t() + def fetch_last_token_balance(address_hash, token_contract_address_hash) do + address_hash + |> CurrentTokenBalance.last_token_balance(token_contract_address_hash) + |> Repo.one() || Decimal.new(0) + end + @spec address_to_coin_balances(Hash.Address.t(), [paging_options]) :: [] def address_to_coin_balances(address_hash, options) do paging_options = Keyword.get(options, :paging_options, @default_paging_options) @@ -4725,6 +4733,7 @@ defmodule Explorer.Chain do base_query = StakingPool + |> where(is_deleted: false) |> staking_pool_filter(filter) |> limit(^page_size) |> order_by(desc: :staked_ratio, asc: :staking_address_hash) @@ -4747,39 +4756,44 @@ defmodule Explorer.Chain do @spec staking_pools_count(filter :: :validator | :active | :inactive) :: integer def staking_pools_count(filter) do StakingPool + |> where(is_deleted: false) |> staking_pool_filter(filter) |> Repo.aggregate(:count, :staking_address_hash) end defp staking_pool_filter(query, :validator) do - where( - query, - [pool], - pool.is_active == true and - pool.is_deleted == false and - pool.is_validator == true - ) + where(query, is_validator: true) end defp staking_pool_filter(query, :active) do - where( - query, - [pool], - pool.is_active == true and - pool.is_deleted == false - ) + where(query, is_active: true) end defp staking_pool_filter(query, :inactive) do - where( - query, - [pool], - pool.is_active == false and - pool.is_deleted == false - ) + where(query, is_active: false) + end + + def staking_pool(staking_address_hash) do + Repo.get_by(StakingPool, staking_address_hash: staking_address_hash) end - defp staking_pool_filter(query, _), do: query + def get_total_staked(address_hash) do + staked_query = + from( + delegator in StakingPoolsDelegator, + where: delegator.delegator_address_hash == ^address_hash and delegator.is_active, + select: sum(delegator.stake_amount) + ) + + self_staked_query = + from( + pool in StakingPool, + where: pool.staking_address_hash == ^address_hash and pool.is_active, + select: sum(pool.self_staked_amount) + ) + + Decimal.add(Repo.one(staked_query) || Decimal.new(0), Repo.one(self_staked_query) || Decimal.new(0)) + end defp with_decompiled_code_flag(query, _hash, false), do: query diff --git a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex index 1471c9fbec..650ad0076c 100644 --- a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex @@ -106,6 +106,18 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do ) end + @doc """ + Builds an `t:Ecto.Query.t/0` to fetch the current balance of the given address for the given token. + """ + def last_token_balance(address_hash, token_contract_address_hash) do + from( + tb in __MODULE__, + where: tb.token_contract_address_hash == ^token_contract_address_hash, + where: tb.address_hash == ^address_hash, + select: tb.value + ) + end + @doc """ Builds an `t:Ecto.Query.t/0` to fetch addresses that hold the token.