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 <anatolyniky@gmail.com>
Co-Authored-By: Kirill Andreev <hindmost.one@gmail.com>
staking
Eduard Sachava 5 years ago committed by Victor Baranov
parent c64f77fe94
commit cbb79679e0
  1. 12
      .circleci/config.yml
  2. 3
      apps/block_scout_web/assets/css/components/stakes/_stakes.scss
  3. 50
      apps/block_scout_web/assets/js/pages/stakes.js
  4. 411
      apps/block_scout_web/assets/package-lock.json
  5. 7
      apps/block_scout_web/lib/block_scout_web/channels/stakes_channel.ex
  6. 18
      apps/block_scout_web/lib/block_scout_web/controllers/stakes_controller.ex
  7. 26
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_stats_item_account.html.eex
  8. 2
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_top.html.eex
  9. 9
      apps/block_scout_web/lib/block_scout_web/views/currency_helpers.ex
  10. 54
      apps/explorer/lib/explorer/chain.ex
  11. 12
      apps/explorer/lib/explorer/chain/address/current_token_balance.ex

@ -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

@ -67,9 +67,12 @@ $stakes-stats-item-border-color: #fff !default;
}
.stakes-top-stats-login {
&, &:hover {
color: $secondary;
text-decoration: none;
cursor: pointer;
margin-right: 8px;
}
}
.stakes-address-container {

@ -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')
}
}

@ -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"
}
}
}
}

@ -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}

@ -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()

@ -0,0 +1,26 @@
<div class="stakes-top-stats-item stakes-top-stats-item-address"
data-user-address="<%= if @account, do: @account.address %>"
>
<span class="stakes-top-stats-value">
<%= if @account do %>
<div data-placement="top" data-toggle="tooltip" title=<%= @account.address %>>
<%= binary_part(@account.address, 0, 13) %>...
</div>
<div class="copy-icon" data-clipboard-text="<%= @account.address %>">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
<path fill-rule="evenodd" d="M13 10a1 1 0 0 1-1-1V2H5a1 1 0 0 1 0-2h8a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1zm-3-5v8a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1zM8 6H2v6h6V6z"/>
</svg>
</div>
<% else %>
<a href="https://metamask.io" target="_blank" data-selector="login-button" class="stakes-top-stats-login">Login</a> with MetaMask
<% end %>
</span>
<span class="stakes-top-stats-label">
<span class="stakes-top-stats-label-item"><%= gettext "Balance" %>:
<%= format_according_to_decimals(@account[:balance], @token.decimals) %> <%= @token.symbol %>
</span>
<span class="stakes-top-stats-label-item"><%= gettext "Staked" %>:
<%= format_according_to_decimals(@account[:staked], @token.decimals) %> <%= @token.symbol %>
</span>
</span>
</div>

@ -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 %>
<!-- Buttons -->
<div class="stakes-top-buttons">
</div>
</div>
</div>
</div>
<%= render BlockScoutWeb.StakesView, "_stakes_modal_become_candidate.html" %>

@ -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

@ -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

@ -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.

Loading…
Cancel
Save