Merge pull request #4931 from blockscout/vb-web3modal

Web3 modal with Wallet Connect for Write contract page and Staking Dapp
pull/4964/head
Victor Baranov 3 years ago committed by GitHub
commit b5bae303c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 1
      apps/block_scout_web/assets/css/components/_address-overview.scss
  3. 2
      apps/block_scout_web/assets/css/components/_card.scss
  4. 4
      apps/block_scout_web/assets/js/lib/async_listing_load.js
  5. 4
      apps/block_scout_web/assets/js/lib/autocomplete.js
  6. 6
      apps/block_scout_web/assets/js/lib/datepicker.js
  7. 118
      apps/block_scout_web/assets/js/lib/smart_contract/common_helpers.js
  8. 177
      apps/block_scout_web/assets/js/lib/smart_contract/connect.js
  9. 63
      apps/block_scout_web/assets/js/lib/smart_contract/functions.js
  10. 116
      apps/block_scout_web/assets/js/lib/smart_contract/interact.js
  11. 186
      apps/block_scout_web/assets/js/lib/smart_contract/write.js
  12. 6
      apps/block_scout_web/assets/js/lib/token_icon.js
  13. 2
      apps/block_scout_web/assets/js/lib/try_api.js
  14. 2
      apps/block_scout_web/assets/js/pages/layout.js
  15. 119
      apps/block_scout_web/assets/js/pages/stakes.js
  16. 1
      apps/block_scout_web/assets/js/pages/write_contract.js
  17. 3424
      apps/block_scout_web/assets/package-lock.json
  18. 2
      apps/block_scout_web/assets/package.json
  19. 5
      apps/block_scout_web/assets/webpack.config.js
  20. 2
      apps/block_scout_web/lib/block_scout_web/csp_header.ex
  21. 1
      apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/index.html.eex
  22. 1
      apps/block_scout_web/lib/block_scout_web/templates/address_write_proxy/index.html.eex
  23. 15
      apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_connect_container.html.eex
  24. 16
      apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex
  25. 4
      apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_pending_contract_write.html.eex
  26. 5
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_stats_item_account.html.eex
  27. 8
      apps/block_scout_web/priv/gettext/default.pot
  28. 8
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  29. 3
      docker/Makefile

@ -1,6 +1,7 @@
## Current
### Features
- [#4931](https://github.com/blockscout/blockscout/pull/4931) - Web3 modal with Wallet Connect for Write contract page and Staking Dapp
### Fixes
- [#4888](https://github.com/blockscout/blockscout/pull/4888) - Fix fetch_top_tokens method: add nulls last for token holders desc order

@ -4,6 +4,7 @@
font-weight: bold;
line-height: 1.2;
text-align: left;
word-break: break-all;
}
.overview-title-buttons {

@ -285,7 +285,6 @@ $card-tab-icon-color-active: #fff !default;
.connect-container {
display: flex;
height: 36px;
line-height: 36px;
}
@ -293,7 +292,6 @@ $card-tab-icon-color-active: #fff !default;
padding: 6px 8px !important;
height: 31px !important;
font-size: 11px;
margin-left: 20px;
}
.contract-plus-btn-container {

@ -42,7 +42,7 @@ import '../app'
*
*/
var enableFirstLoading = true
let enableFirstLoading = true
export const asyncInitialState = {
/* it will consider any query param in the current URI as paging */
@ -234,7 +234,7 @@ export const elements = {
$el.show()
$el.attr('disabled', false)
var url
let url
if (blockParam !== null) {
url = firstPageHref + '?block_type=' + blockParam
} else {

@ -54,7 +54,7 @@ const searchEngine = (query, record) => {
(record.block_hash && record.block_hash.toLowerCase().includes(query.toLowerCase()))
)
) {
var searchResult = '<div>'
let searchResult = '<div>'
searchResult += `<div>${record.address_hash || record.tx_hash || record.block_hash}</div>`
if (record.type === 'label') {
@ -76,7 +76,7 @@ const searchEngine = (query, record) => {
searchResult += '</div>'
}
searchResult += '</div>'
var re = new RegExp(query, 'ig')
const re = new RegExp(query, 'ig')
searchResult = searchResult.replace(re, '<mark class=\'autoComplete_highlight\'>$&</mark>')
return searchResult
}

@ -66,9 +66,9 @@ function onSelect (date, paramToReplace) {
const formattedDate = moment(date).format(DATE_FORMAT)
if (date) {
var csvExportPath = $button.data('link')
const csvExportPath = $button.data('link')
var updatedCsvExportUrl = replaceUrlParam(csvExportPath, paramToReplace, formattedDate)
const updatedCsvExportUrl = replaceUrlParam(csvExportPath, paramToReplace, formattedDate)
$button.data('link', updatedCsvExportUrl)
}
}
@ -77,7 +77,7 @@ function replaceUrlParam (url, paramName, paramValue) {
if (paramValue == null) {
paramValue = ''
}
var pattern = new RegExp('\\b(' + paramName + '=).*?(&|#|$)')
const pattern = new RegExp('\\b(' + paramName + '=).*?(&|#|$)')
if (url.search(pattern) >= 0) {
return url.replace(pattern, '$1' + paramValue + '$2')
}

@ -1,6 +1,12 @@
import Web3 from 'web3'
import $ from 'jquery'
import { props } from 'eth-net-props'
export const connectSelector = '[connect-wallet]'
export const disconnectSelector = '[disconnect-wallet]'
const connectToSelector = '[connect-to]'
const connectedToSelector = '[connected-to]'
export function getContractABI ($form) {
const implementationAbi = $form.data('implementation-abi')
const parentAbi = $form.data('contract-abi')
@ -37,7 +43,7 @@ export function prepareMethodArgs ($functionInputs, inputs) {
const sanitizedInputValueElements = inputValueElements.map(elementValue => {
const elementInputType = inputType.split('[')[0]
var sanitizedElementValue = replaceDoubleQuotes(elementValue, elementInputType)
let sanitizedElementValue = replaceDoubleQuotes(elementValue, elementInputType)
sanitizedElementValue = replaceSpaces(sanitizedElementValue, elementInputType)
if (isBoolInputType(elementInputType)) {
@ -72,10 +78,10 @@ export const formatError = (error) => {
export const formatTitleAndError = (error) => {
let { message } = error
var title = message && message.split('Error: ').length > 1 ? message.split('Error: ')[1] : message
let title = message && message.split('Error: ').length > 1 ? message.split('Error: ')[1] : message
title = title && title.split('{').length > 1 ? title.split('{')[0].replace(':', '') : title
var txHash = ''
var errorMap = ''
let txHash = ''
let errorMap = ''
try {
errorMap = message && message.indexOf('{') >= 0 ? JSON.parse(message && message.slice(message.indexOf('{'))) : ''
message = errorMap.error || ''
@ -86,7 +92,42 @@ export const formatTitleAndError = (error) => {
return { title: title, message: message, txHash: txHash }
}
export const getCurrentAccount = () => {
export const getCurrentAccountPromise = (provider) => {
return new Promise((resolve, reject) => {
if (provider && provider.wc) {
getCurrentAccountFromWCPromise(provider)
.then(account => resolve(account))
.catch(err => {
reject(err)
})
} else {
getCurrentAccountFromMMPromise()
.then(account => resolve(account))
.catch(err => {
reject(err)
})
}
})
}
export const getCurrentAccountFromWCPromise = (provider) => {
return new Promise((resolve, reject) => {
// Get a Web3 instance for the wallet
const web3 = new Web3(provider)
// Get list of accounts of the connected wallet
web3.eth.getAccounts()
.then(accounts => {
// MetaMask does not give you all accounts, only the selected account
resolve(accounts[0])
})
.catch(err => {
reject(err)
})
})
}
export const getCurrentAccountFromMMPromise = () => {
return new Promise((resolve, reject) => {
window.ethereum.request({ method: 'eth_accounts' })
.then(accounts => {
@ -99,6 +140,73 @@ export const getCurrentAccount = () => {
})
}
function hideConnectedToContainer () {
document.querySelector(connectedToSelector) && document.querySelector(connectedToSelector).classList.add('hidden')
}
function showConnectedToContainer () {
document.querySelector(connectedToSelector) && document.querySelector(connectedToSelector).classList.remove('hidden')
}
function hideConnectContainer () {
document.querySelector(connectSelector) && document.querySelector(connectSelector).classList.add('hidden')
}
function showConnectContainer () {
document.querySelector(connectSelector) && document.querySelector(connectSelector).classList.remove('hidden')
}
function hideConnectToContainer () {
document.querySelector(connectToSelector) && document.querySelector(connectToSelector).classList.add('hidden')
}
function showConnectToContainer () {
document.querySelector(connectToSelector) && document.querySelector(connectToSelector).classList.remove('hidden')
}
export function showHideDisconnectButton () {
// Show disconnect button only in case of Wallet Connect
if (window.web3 && window.web3.currentProvider && window.web3.currentProvider.wc) {
document.querySelector(disconnectSelector) && document.querySelector(disconnectSelector).classList.remove('hidden')
} else {
document.querySelector(disconnectSelector) && document.querySelector(disconnectSelector).classList.add('hidden')
}
}
export function showConnectedToElements (account) {
hideConnectToContainer()
showConnectContainer()
showConnectedToContainer()
showHideDisconnectButton()
setConnectToAddress(account)
}
export function showConnectElements () {
showConnectToContainer()
showConnectContainer()
hideConnectedToContainer()
}
export function hideConnectButton () {
showConnectToContainer()
hideConnectContainer()
hideConnectedToContainer()
}
function setConnectToAddress (account) {
if (document.querySelector('[connected-to-address]')) {
document.querySelector('[connected-to-address]').innerHTML = `<a href='/address/${account}'>${trimmedAddressHash(account)}</a>`
}
}
function trimmedAddressHash (account) {
if ($(window).width() < 544) {
return `${account.slice(0, 7)}${account.slice(-6)}`
} else {
return account
}
}
function convertToBool (value) {
const boolVal = (value === 'true' || value === '1' || value === 1)

@ -0,0 +1,177 @@
import Web3 from 'web3'
import Web3Modal from 'web3modal'
import WalletConnectProvider from '@walletconnect/web3-provider'
import { compareChainIDs, formatError, showConnectElements, showConnectedToElements } from './common_helpers'
import { openWarningModal } from '../modals'
const instanceChainId = process.env.CHAIN_ID ? parseInt(`${process.env.CHAIN_ID}`, 10) : 100
const walletConnectOptions = { rpc: {}, chainId: instanceChainId }
walletConnectOptions.rpc[instanceChainId] = process.env.JSON_RPC ? process.env.JSON_RPC : 'https://dai.poa.network'
// Chosen wallet provider given by the dialog window
let provider
// Web3modal instance
let web3Modal
/**
* Setup the orchestra
*/
export async function web3ModalInit (connectToWallet, ...args) {
return new Promise((resolve) => {
// Tell Web3modal what providers we have available.
// Built-in web browser provider (only one can exist as a time)
// like MetaMask, Brave or Opera is added automatically by Web3modal
const providerOptions = {
walletconnect: {
package: WalletConnectProvider,
options: walletConnectOptions
}
}
web3Modal = new Web3Modal({
cacheProvider: true,
providerOptions,
disableInjectedProvider: false
})
if (web3Modal.cachedProvider) {
connectToWallet(...args)
}
resolve(web3Modal)
})
}
export const walletEnabled = () => {
return new Promise((resolve) => {
if (window.web3 && window.web3.currentProvider && window.web3.currentProvider.wc) {
resolve(true)
} else {
if (window.ethereum) {
window.web3 = new Web3(window.ethereum)
window.ethereum._metamask.isUnlocked()
.then(isUnlocked => {
if (isUnlocked && window.ethereum.isNiftyWallet) { // Nifty Wallet
window.web3 = new Web3(window.web3.currentProvider)
resolve(true)
} else if (isUnlocked === false && window.ethereum.isNiftyWallet) { // Nifty Wallet
window.ethereum.enable()
resolve(false)
} else {
if (window.ethereum.isNiftyWallet) {
window.ethereum.enable()
window.web3 = new Web3(window.web3.currentProvider)
resolve(true)
} else {
return window.ethereum.request({ method: 'eth_requestAccounts' })
.then((_res) => {
window.web3 = new Web3(window.web3.currentProvider)
resolve(true)
})
.catch(_error => {
resolve(false)
})
}
}
})
.catch(_error => {
resolve(false)
})
} else if (window.web3) {
window.web3 = new Web3(window.web3.currentProvider)
resolve(true)
} else {
resolve(false)
}
}
})
}
export async function disconnect () {
if (provider && provider.close) {
await provider.close()
}
provider = null
window.web3 = null
// If the cached provider is not cleared,
// WalletConnect will default to the existing session
// and does not allow to re-scan the QR code with a new wallet.
// Depending on your use case you may want or want not his behavir.
await web3Modal.clearCachedProvider()
}
/**
* Disconnect wallet button pressed.
*/
export async function disconnectWallet () {
await disconnect()
showConnectElements()
}
export const connectToProvider = () => {
return new Promise((resolve, reject) => {
try {
web3Modal
.connect()
.then((connectedProvider) => {
provider = connectedProvider
window.web3 = new Web3(provider)
resolve(provider)
})
} catch (e) {
reject(e)
}
})
}
export const connectToWallet = async () => {
await connectToProvider()
// Subscribe to accounts change
provider.on('accountsChanged', async (accs) => {
const newAccount = accs && accs.length > 0 ? accs[0].toLowerCase() : null
if (!newAccount) {
await disconnectWallet()
}
fetchAccountData(showConnectedToElements, [])
})
// Subscribe to chainId change
provider.on('chainChanged', (chainId) => {
compareChainIDs(instanceChainId, chainId)
.then(() => fetchAccountData(showConnectedToElements, []))
.catch(error => {
openWarningModal('Unauthorized', formatError(error))
})
})
provider.on('disconnect', async () => {
await disconnectWallet()
})
await fetchAccountData(showConnectedToElements, [])
}
export async function fetchAccountData (setAccount, args) {
// Get a Web3 instance for the wallet
if (provider) {
window.web3 = new Web3(provider)
}
// Get list of accounts of the connected wallet
const accounts = window.web3 && await window.web3.eth.getAccounts()
// MetaMask does not give you all accounts, only the selected account
if (accounts && accounts.length > 0) {
const selectedAccount = accounts[0]
setAccount(selectedAccount, ...args)
}
}

@ -1,6 +1,7 @@
import $ from 'jquery'
import { getContractABI, getMethodInputs, prepareMethodArgs } from './common_helpers'
import { walletEnabled, connectToWallet, shouldHideConnectButton, callMethod, queryMethod } from './write'
import { connectSelector, disconnectSelector, getContractABI, getMethodInputs, prepareMethodArgs } from './common_helpers'
import { queryMethod, callMethod } from './interact'
import { walletEnabled, connectToWallet, disconnectWallet, web3ModalInit } from './connect.js'
import '../../pages/address'
const loadFunctions = (element) => {
@ -16,37 +17,9 @@ const loadFunctions = (element) => {
response => $element.html(response)
)
.done(function () {
const $connect = $('[connect-metamask]')
const $connectTo = $('[connect-to]')
const $connectedTo = $('[connected-to]')
const $reconnect = $('[re-connect-metamask]')
window.ethereum && window.ethereum.on('accountsChanged', function (accounts) {
if (accounts.length === 0) {
showConnectElements($connect, $connectTo, $connectedTo)
} else {
showConnectedToElements($connect, $connectTo, $connectedTo, accounts[0])
}
})
shouldHideConnectButton()
.then(({ shouldHide, account }) => {
if (shouldHide && account) {
showConnectedToElements($connect, $connectTo, $connectedTo, account)
} else if (shouldHide) {
hideConnectButton($connect, $connectTo, $connectedTo)
} else {
showConnectElements($connect, $connectTo, $connectedTo)
}
})
$connect.on('click', () => {
connectToWallet()
})
$reconnect.on('click', () => {
connectToWallet()
})
document.querySelector(connectSelector) && document.querySelector(connectSelector).addEventListener('click', connectToWallet)
document.querySelector(disconnectSelector) && document.querySelector(disconnectSelector).addEventListener('click', disconnectWallet)
web3ModalInit(connectToWallet)
$('[data-function]').each((_, element) => {
readWriteFunction(element)
@ -75,30 +48,6 @@ const loadFunctions = (element) => {
})
}
function showConnectedToElements ($connect, $connectTo, $connectedTo, account) {
$connectTo.addClass('hidden')
$connect.removeClass('hidden')
$connectedTo.removeClass('hidden')
setConnectToAddress(account)
}
function setConnectToAddress (account) {
const $connectedToAddress = $('[connected-to-address]')
$connectedToAddress.html(`<a href='/address/${account}'>${account}</a>`)
}
function showConnectElements ($connect, $connectTo, $connectedTo) {
$connectTo.removeClass('hidden')
$connect.removeClass('hidden')
$connectedTo.addClass('hidden')
}
function hideConnectButton ($connect, $connectTo, $connectedTo) {
$connectTo.removeClass('hidden')
$connect.addClass('hidden')
$connectedTo.addClass('hidden')
}
const readWriteFunction = (element) => {
const $element = $(element)
const $form = $element.find('[data-function-form]')

@ -0,0 +1,116 @@
import $ from 'jquery'
import { openErrorModal, openWarningModal, openSuccessModal, openModalWithMessage } from '../modals'
import { compareChainIDs, formatError, formatTitleAndError, getContractABI, getCurrentAccountPromise, getMethodInputs, prepareMethodArgs } from './common_helpers'
export const queryMethod = (isWalletEnabled, url, $methodId, args, type, functionName, $responseContainer) => {
let data = {
function_name: functionName,
method_id: $methodId.val(),
type: type,
args
}
if (isWalletEnabled) {
getCurrentAccountPromise(window.web3 && window.web3.currentProvider)
.then((currentAccount) => {
data = {
function_name: functionName,
method_id: $methodId.val(),
type: type,
from: currentAccount,
args
}
$.get(url, data, response => $responseContainer.html(response))
}
)
} else {
$.get(url, data, response => $responseContainer.html(response))
}
}
export const callMethod = (isWalletEnabled, $functionInputs, explorerChainId, $form, functionName, $element) => {
if (!isWalletEnabled) {
const warningMsg = 'Wallet is not connected.'
return openWarningModal('Unauthorized', warningMsg)
}
const contractAbi = getContractABI($form)
const inputs = getMethodInputs(contractAbi, functionName)
const $functionInputsExceptTxValue = $functionInputs.filter(':not([tx-value])')
const args = prepareMethodArgs($functionInputsExceptTxValue, inputs)
const txValue = getTxValue($functionInputs)
const contractAddress = $form.data('contract-address')
window.web3.eth.getChainId()
.then((walletChainId) => {
compareChainIDs(explorerChainId, walletChainId)
.then(() => getCurrentAccountPromise(window.web3.currentProvider))
.catch(error => {
openWarningModal('Unauthorized', formatError(error))
})
.then((currentAccount) => {
if (functionName) {
const TargetContract = new window.web3.eth.Contract(contractAbi, contractAddress)
const sendParams = { from: currentAccount, value: txValue || 0 }
const methodToCall = TargetContract.methods[functionName](...args).send(sendParams)
methodToCall
.on('error', function (error) {
const titleAndError = formatTitleAndError(error)
const message = titleAndError.message + (titleAndError.txHash ? `<br><a href="/tx/${titleAndError.txHash}">More info</a>` : '')
openErrorModal(titleAndError.title.length ? titleAndError.title : `Error in sending transaction for method "${functionName}"`, message, false)
})
.on('transactionHash', function (txHash) {
onTransactionHash(txHash, $element, functionName)
})
} else {
const txParams = {
from: currentAccount,
to: contractAddress,
value: txValue || 0
}
window.ethereum.request({
method: 'eth_sendTransaction',
params: [txParams]
})
.then(function (txHash) {
onTransactionHash(txHash, $element, functionName)
})
.catch(function (error) {
openErrorModal('Error in sending transaction for fallback method', formatError(error), false)
})
}
})
.catch(error => {
openWarningModal('Unauthorized', formatError(error))
})
})
}
function onTransactionHash (txHash, $element, functionName) {
openModalWithMessage($element.find('#pending-contract-write'), true, txHash)
const getTxReceipt = (txHash) => {
window.ethereum.request({
method: 'eth_getTransactionReceipt',
params: [txHash]
})
.then(txReceipt => {
if (txReceipt) {
const successMsg = `Successfully sent <a href="/tx/${txHash}">transaction</a> for method "${functionName}"`
openSuccessModal('Success', successMsg)
clearInterval(txReceiptPollingIntervalId)
}
})
}
const txReceiptPollingIntervalId = setInterval(() => { getTxReceipt(txHash) }, 5 * 1000)
}
function getTxValue ($functionInputs) {
const WEI_MULTIPLIER = 10 ** 18
const $txValue = $functionInputs.filter('[tx-value]:first')
const txValue = $txValue && $txValue.val() && parseFloat($txValue.val()) * WEI_MULTIPLIER
let txValueStr = txValue && txValue.toString(16)
if (!txValueStr) {
txValueStr = '0'
}
return '0x' + txValueStr
}

@ -1,186 +0,0 @@
import Web3 from 'web3'
import $ from 'jquery'
import { openErrorModal, openWarningModal, openSuccessModal, openModalWithMessage } from '../modals'
import { compareChainIDs, formatError, formatTitleAndError, getContractABI, getCurrentAccount, getMethodInputs, prepareMethodArgs } from './common_helpers'
export const walletEnabled = () => {
return new Promise((resolve) => {
if (window.ethereum) {
window.web3 = new Web3(window.ethereum)
window.ethereum._metamask.isUnlocked()
.then(isUnlocked => {
if (isUnlocked && window.ethereum.isNiftyWallet) { // Nifty Wallet
window.web3 = new Web3(window.web3.currentProvider)
resolve(true)
} else if (isUnlocked === false && window.ethereum.isNiftyWallet) { // Nifty Wallet
window.ethereum.enable()
resolve(false)
} else {
if (window.ethereum.isNiftyWallet) {
window.ethereum.enable()
window.web3 = new Web3(window.web3.currentProvider)
resolve(true)
} else {
return window.ethereum.request({ method: 'eth_requestAccounts' })
.then((_res) => {
window.web3 = new Web3(window.web3.currentProvider)
resolve(true)
})
.catch(_error => {
resolve(false)
})
}
}
})
.catch(_error => {
resolve(false)
})
} else if (window.web3) {
window.web3 = new Web3(window.web3.currentProvider)
resolve(true)
} else {
resolve(false)
}
})
}
export const connectToWallet = () => {
if (window.ethereum) {
if (window.ethereum.isNiftyWallet) {
window.ethereum.enable()
} else {
window.ethereum.request({ method: 'eth_requestAccounts' })
}
}
}
export const shouldHideConnectButton = () => {
return new Promise((resolve) => {
if (window.ethereum) {
window.web3 = new Web3(window.ethereum)
if (window.ethereum.isNiftyWallet) {
resolve({ shouldHide: true, account: window.ethereum.selectedAddress })
} else if (window.ethereum.isMetaMask) {
window.ethereum.request({ method: 'eth_accounts' })
.then(accounts => {
accounts.length > 0 ? resolve({ shouldHide: true, account: accounts[0] }) : resolve({ shouldHide: false })
})
.catch(_error => {
resolve({ shouldHide: false })
})
} else {
resolve({ shouldHide: true, account: window.ethereum.selectedAddress })
}
} else {
resolve({ shouldHide: false })
}
})
}
export function callMethod (isWalletEnabled, $functionInputs, explorerChainId, $form, functionName, $element) {
if (!isWalletEnabled) {
const warningMsg = 'You haven\'t approved the reading of account list from your MetaMask or MetaMask/Nifty wallet is locked or is not installed.'
return openWarningModal('Unauthorized', warningMsg)
}
const contractAbi = getContractABI($form)
const inputs = getMethodInputs(contractAbi, functionName)
const $functionInputsExceptTxValue = $functionInputs.filter(':not([tx-value])')
const args = prepareMethodArgs($functionInputsExceptTxValue, inputs)
const txValue = getTxValue($functionInputs)
const contractAddress = $form.data('contract-address')
const { chainId: walletChainIdHex } = window.ethereum
compareChainIDs(explorerChainId, walletChainIdHex)
.then(() => getCurrentAccount())
.then(currentAccount => {
if (functionName) {
const TargetContract = new window.web3.eth.Contract(contractAbi, contractAddress)
const sendParams = { from: currentAccount, value: txValue || 0 }
const methodToCall = TargetContract.methods[functionName](...args).send(sendParams)
methodToCall
.on('error', function (error) {
var titleAndError = formatTitleAndError(error)
var message = titleAndError.message + (titleAndError.txHash ? `<br><a href="/tx/${titleAndError.txHash}">More info</a>` : '')
openErrorModal(titleAndError.title.length ? titleAndError.title : `Error in sending transaction for method "${functionName}"`, message, false)
})
.on('transactionHash', function (txHash) {
onTransactionHash(txHash, $element, functionName)
})
} else {
const txParams = {
from: currentAccount,
to: contractAddress,
value: txValue || 0
}
window.ethereum.request({
method: 'eth_sendTransaction',
params: [txParams]
})
.then(function (txHash) {
onTransactionHash(txHash, $element, functionName)
})
.catch(function (error) {
openErrorModal('Error in sending transaction for fallback method', formatError(error), false)
})
}
})
.catch(error => {
openWarningModal('Unauthorized', formatError(error))
})
}
export function queryMethod (isWalletEnabled, url, $methodId, args, type, functionName, $responseContainer) {
var data = {
function_name: functionName,
method_id: $methodId.val(),
type: type,
args
}
if (isWalletEnabled) {
getCurrentAccount()
.then((currentAccount) => {
data = {
function_name: functionName,
method_id: $methodId.val(),
type: type,
from: currentAccount,
args
}
$.get(url, data, response => $responseContainer.html(response))
}
)
} else {
$.get(url, data, response => $responseContainer.html(response))
}
}
function onTransactionHash (txHash, $element, functionName) {
openModalWithMessage($element.find('#pending-contract-write'), true, txHash)
const getTxReceipt = (txHash) => {
window.ethereum.request({
method: 'eth_getTransactionReceipt',
params: [txHash]
})
.then(txReceipt => {
if (txReceipt) {
const successMsg = `Successfully sent <a href="/tx/${txHash}">transaction</a> for method "${functionName}"`
openSuccessModal('Success', successMsg)
clearInterval(txReceiptPollingIntervalId)
}
})
}
const txReceiptPollingIntervalId = setInterval(() => { getTxReceipt(txHash) }, 5 * 1000)
}
function getTxValue ($functionInputs) {
const WEI_MULTIPLIER = 10 ** 18
const $txValue = $functionInputs.filter('[tx-value]:first')
const txValue = $txValue && $txValue.val() && parseFloat($txValue.val()) * WEI_MULTIPLIER
var txValueStr = txValue && txValue.toString(16)
if (!txValueStr) {
txValueStr = '0'
}
return '0x' + txValueStr
}

@ -1,5 +1,5 @@
function getTokenIconUrl (chainID, addressHash) {
var chainName = null
let chainName = null
switch (chainID) {
case '1':
chainName = 'ethereum'
@ -23,7 +23,7 @@ function getTokenIconUrl (chainID, addressHash) {
function appendTokenIcon ($tokenIconContainer, chainID, addressHash, foreignChainID, foreignAddressHash, displayTokenIcons, size) {
const iconSize = size || 20
var tokenIconURL = null
let tokenIconURL = null
if (foreignChainID) {
tokenIconURL = getTokenIconUrl(foreignChainID.toString(), foreignAddressHash)
} else if (chainID) {
@ -34,7 +34,7 @@ function appendTokenIcon ($tokenIconContainer, chainID, addressHash, foreignChai
.then(checkTokenIconLink => {
if (checkTokenIconLink) {
if ($tokenIconContainer) {
var img = new Image(iconSize, iconSize)
const img = new Image(iconSize, iconSize)
img.src = tokenIconURL
$tokenIconContainer.append(img)
}

@ -57,7 +57,7 @@ function handleSuccess (query, xhr, clickedButton) {
}
function escapeHtml (text) {
var map = {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',

@ -22,7 +22,7 @@ $(document).on('keyup', function (event) {
$('.main-search-autocomplete').on('keyup', function (event) {
if (event.key === 'Enter') {
var selected = false
let selected = false
$('li[id^="autoComplete_result_"]').each(function () {
if ($(this).attr('aria-selected')) {
selected = true

@ -5,6 +5,8 @@ import _ from 'lodash'
import { subscribeChannel } from '../socket'
import { connectElements } from '../lib/redux_helpers.js'
import { createAsyncLoadStore, refreshPage } from '../lib/async_listing_load'
import { showHideDisconnectButton } from '../lib/smart_contract/common_helpers'
import { connectToProvider, disconnect, fetchAccountData, web3ModalInit } from '../lib/smart_contract/connect'
import Queue from '../lib/queue'
import Web3 from 'web3'
import { openPoolInfoModal } from './stakes/validator_info'
@ -22,6 +24,8 @@ import constants from './stakes/constants'
const stakesPageSelector = '[data-page="stakes"]'
let provider = null
if (localStorage.getItem('stakes-alert-read') === 'true') {
$('.js-stakes-welcome-alert').hide()
} else {
@ -164,6 +168,16 @@ const $stakesPage = $(stakesPageSelector)
const $stakesTop = $('[data-selector="stakes-top"]')
const $refreshInformer = $('.refresh-informer', $stakesPage)
const observer = new MutationObserver(function (mutationsList) {
mutationsList.forEach(function (mutation) {
mutation.addedNodes.forEach(function (addedNode) {
if (addedNode.className === 'stakes-top') {
showHideDisconnectButton()
}
})
})
})
if ($stakesPage.length) {
const store = createAsyncLoadStore(reducer, initialState, 'dataset.identifierPool')
connectElements({ store, elements })
@ -333,7 +347,17 @@ if ($stakesPage.length) {
.on('change', '[pool-filter-banned]', () => updateFilters(store, 'banned'))
.on('change', '[pool-filter-my]', () => updateFilters(store, 'my'))
initialize(store)
web3ModalInit(connectToWallet, store)
$stakesTop.on('click', '[data-selector="login-button"]', async (_event) => {
login(store)
})
$stakesTop.on('click', '[disconnect-wallet]', async (_event) => {
disconnectWalletFromStakingDapp(store)
})
observer.observe(document.querySelector('[data-selector="stakes-top"]'), { subtree: false, childList: true })
}
function accountChanged (account, state) {
@ -352,16 +376,21 @@ async function getAccounts () {
}
async function getNetId (web3) {
let netId = window.ethereum.chainId
if (!netId) {
netId = await window.ethereum.request({ method: 'eth_chainId' })
}
if (!netId) {
console.error(`Cannot get chainId. ${constants.METAMASK_VERSION_WARNING}`)
if (window.web3 && window.web3.currentProvider && window.web3.currentProvider.wc) {
return window.web3.currentProvider.chainId
} else {
netId = web3.utils.isHex(netId) ? web3.utils.hexToNumber(netId) : netId
let netId = window.ethereum.chainId
if (!netId) {
netId = await window.ethereum.request({ method: 'eth_chainId' })
}
if (!netId) {
const msg = `Cannot get chainId. ${constants.METAMASK_VERSION_WARNING}`
console.error(msg)
} else {
netId = web3.utils.isHex(netId) ? web3.utils.hexToNumber(netId) : netId
}
return netId
}
return netId
}
function hideCurrentModal () {
@ -369,33 +398,48 @@ function hideCurrentModal () {
if ($modal) $modal.modal('hide')
}
function initialize (store) {
if (window.ethereum) {
const web3 = new Web3(window.ethereum)
if (window.ethereum.autoRefreshOnNetworkChange) {
window.ethereum.autoRefreshOnNetworkChange = false
}
store.dispatch({ type: 'WEB3_DETECTED', web3 })
async function disconnectWalletFromStakingDapp (store) {
await disconnect()
initNetworkAndAccount(store, web3)
provider = null
window.ethereum.on('chainChanged', async (chainId) => {
const newNetId = web3.utils.isHex(chainId) ? web3.utils.hexToNumber(chainId) : chainId
setNetwork(newNetId, store, true)
})
if (accountChanged(null, store.getState())) {
await setAccount(null, store)
}
}
window.ethereum.on('accountsChanged', async (accs) => {
const newAccount = accs && accs.length > 0 ? accs[0].toLowerCase() : null
if (accountChanged(newAccount, store.getState())) {
await setAccount(newAccount, store)
}
})
async function connectToWallet (store) {
provider = await connectToProvider()
$stakesTop.on('click', '[data-selector="login-button"]', loginByMetamask)
} else {
// We do the first load immediately if the latest version of MetaMask is not installed
refreshPageWrapper(store)
provider.on('chainChanged', async (chainId) => {
const newNetId = web3.utils.isHex(chainId) ? web3.utils.hexToNumber(chainId) : chainId
setNetwork(newNetId, store, true)
})
provider.on('accountsChanged', async (accs) => {
const newAccount = accs && accs.length > 0 ? accs[0].toLowerCase() : null
if (!newAccount) {
await disconnectWalletFromStakingDapp(store)
}
if (accountChanged(newAccount, store.getState())) {
await setAccount(newAccount, store)
}
})
provider.on('disconnect', async () => {
await disconnectWalletFromStakingDapp(store)
})
const web3 = new Web3(provider)
if (provider.autoRefreshOnNetworkChange) {
provider.autoRefreshOnNetworkChange = false
}
store.dispatch({ type: 'WEB3_DETECTED', web3 })
initNetworkAndAccount(store, web3)
await fetchAccountData(setAccount, [store])
}
async function initNetworkAndAccount (store, web3) {
@ -418,18 +462,10 @@ async function initNetworkAndAccount (store, web3) {
}
}
async function loginByMetamask () {
async function login (store) {
event.stopPropagation()
event.preventDefault()
try {
await window.ethereum.request({ method: 'eth_requestAccounts' })
} catch (e) {
console.log(e)
if (e.code !== 4001) {
console.error(`eth_requestAccounts failed. ${constants.METAMASK_VERSION_WARNING}`)
openErrorModal(`Request account access', 'Cannot request access to your account in MetaMask. ${constants.METAMASK_VERSION_WARNING}`)
}
}
connectToWallet(store)
}
async function refreshPageWrapper (store) {
@ -475,6 +511,7 @@ function setAccount (account, store) {
store.dispatch({ type: 'ACCOUNT_UPDATED', account })
if (!account) {
resetFilterMy(store)
resolve(true)
}
const errorMsg = 'Cannot properly set account due to connection loss. Please, reload the page.'

@ -1 +0,0 @@
import '../lib/smart_contract/write'

File diff suppressed because it is too large Load Diff

@ -21,6 +21,7 @@
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.3",
"@tarekraafat/autocomplete.js": "^10.2.6",
"@walletconnect/web3-provider": "^1.6.6",
"assert": "^2.0.0",
"bignumber.js": "^9.0.0",
"bootstrap": "^4.6.0",
@ -55,6 +56,7 @@
"url": "^0.11.0",
"util": "^0.12.3",
"web3": "^1.3.5",
"web3modal": "^1.9.4",
"xss": "^1.0.9"
},
"devDependencies": {

@ -84,7 +84,6 @@ const appJs =
'admin-tasks': './js/pages/admin/tasks.js',
'read-token-contract': './js/pages/read_token_contract.js',
'smart-contract-helpers': './js/lib/smart_contract/index.js',
'write-contract': './js/pages/write_contract.js',
'token-transfers-toggle': './js/lib/token_transfers_toggle.js',
'try-api': './js/lib/try_api.js',
'try-eth-api': './js/lib/try_eth_api.js',
@ -175,7 +174,9 @@ const appJs =
),
new ContextReplacementPlugin(/moment[\/\\]locale$/, /en/),
new webpack.DefinePlugin({
'process.env.SOCKET_ROOT': JSON.stringify(process.env.SOCKET_ROOT)
'process.env.SOCKET_ROOT': JSON.stringify(process.env.SOCKET_ROOT),
'process.env.CHAIN_ID': JSON.stringify(process.env.CHAIN_ID),
'process.env.JSON_RPC': JSON.stringify(process.env.JSON_RPC)
}),
new webpack.ProvidePlugin({
process: 'process/browser',

@ -11,7 +11,7 @@ defmodule BlockScoutWeb.CSPHeader do
def call(conn, _opts) do
Controller.put_secure_browser_headers(conn, %{
"content-security-policy" => "\
connect-src 'self' #{websocket_endpoints(conn)} https://request-global.czilladx.com/ https://raw.githubusercontent.com/trustwallet/assets/;\
connect-src 'self' #{websocket_endpoints(conn)} wss://*.bridge.walletconnect.org/ https://request-global.czilladx.com/ https://raw.githubusercontent.com/trustwallet/assets/ https://registry.walletconnect.org/data/wallets.json https://*.poa.network;\
default-src 'self';\
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://coinzillatag.com https://www.google.com https://www.gstatic.com;\
style-src 'self' 'unsafe-inline' 'unsafe-eval' https://fonts.googleapis.com;\

@ -14,5 +14,4 @@
</div>
</div>
<script defer data-cfasync="false" src="<%= static_path(@conn, "/js/smart-contract-helpers.js") %>"></script>
<script defer data-cfasync="false" src="<%= static_path(@conn, "/js/write-contract.js") %>"></script>
</section>

@ -14,5 +14,4 @@
</div>
</div>
<script defer data-cfasync="false" src="<%= static_path(@conn, "/js/smart-contract-helpers.js") %>"></script>
<script defer data-cfasync="false" src="<%= static_path(@conn, "/js/write-contract.js") %>"></script>
</section>

@ -3,11 +3,14 @@
<%= render BlockScoutWeb.IconsView, "_inactive_icon.html" %>
</span>
<h2 style="margin-top: -2px;">Disconnected</h2>
<button connect-metamask class="button btn-line ml-4">Connect to Metamask</button>
<button connect-wallet class="button btn-line ml-4">Connect wallet</button>
</div>
<div connected-to class="connect-container hidden">
<span style="margin-top: -2px;" class="mr-2">
<%= render BlockScoutWeb.IconsView, "_active_icon.html" %>
</span>
<h2 style="margin-top: -2px;">Connected to</h2><h3 connected-to-address class="ml-2"></h3>
<div connected-to class="connect-container row hidden">
<div class="d-flex ml-3">
<span style="margin-top: -2px;" class="mr-2">
<%= render BlockScoutWeb.IconsView, "_active_icon.html" %>
</span>
<h2 style="margin-top: -2px; min-width: 104px;" class="mr-2">Connected to</h2><h3 connected-to-address></h3>
</div>
<button disconnect-wallet class="button btn-line ml-3">Disconnect wallet</button>
</div>

@ -57,13 +57,13 @@
<%= if queryable?(function["inputs"]) do %>
<%= for input <- function["inputs"] do %>
<div class="form-group pr-3">
<div class="form-group pr-3 d-flex" style="margin: 0.5rem 0 !important;">
<%= if int?(input["type"]) do %>
<input type="number" name="function_input" class="form-control form-control-sm address-input-sm mt-2"
<input type="number" name="function_input" class="form-control form-control-sm address-input-sm"
placeholder='<%= input["name"] %>(<%= input["type"] %>)'
style="width: <%= (String.length(input["name"]) + String.length(input["type"]) + 2) * 10 %>px;"/>
<span data-dropdown-toggle="" data-toggle="dropdown">
<span class="button btn-line button-xs contract-plus-btn-container ml-1 mt-2">
<span class="button btn-line button-xs contract-plus-btn-container ml-1">
<i class="fa fa-plus contract-plus-btn"></i>
</span>
<div class="dropdown-menu exponention-dropdown">
@ -75,7 +75,7 @@
</span>
<% else %>
<input type="text" name="function_input" class="form-control form-control-sm address-input-sm mt-2"
<input type="text" name="function_input" class="form-control form-control-sm address-input-sm"
placeholder='<%= input["name"] %>(<%= input["type"] %>)'
size="<%= String.length(input["name"]) + String.length(input["type"]) + 2 %>" />
<% end %>
@ -84,13 +84,15 @@
<% end %>
<%= if Helper.payable?(function) do %>
<div class="form-group pr-3">
<div class="form-group pr-3 d-flex">
<input type="number" name="function_input" tx-value
data-toggle="tooltip" title='Amount in native token <<%= gettext("ETH")%>>' class="form-control form-control-sm address-input-sm mt-2" placeholder='value(<%= gettext("ETH")%>)' min="0" step="1e-18" />
data-toggle="tooltip" title='Amount in native token <<%= gettext("ETH")%>>' class="form-control form-control-sm address-input-sm" placeholder='value(<%= gettext("ETH")%>)' min="0" step="1e-18" />
</div>
<% end %>
<input type="submit" value='<%= if @action == "write", do: gettext("Write"), else: gettext("Query")%>' class="button btn-line button-xs py-0 mt-2 write-contract-btn" />
<div>
<input type="submit" value='<%= if @action == "write", do: gettext("Write"), else: gettext("Query")%>' class="button btn-line button-xs py-0 write-contract-btn" />
</div>
</form>
<%= if outputs?(function["outputs"]) do %>

@ -1,8 +1,8 @@
<div id="pending-contract-write" class="modal modal-fullwidth-xs fade" tabindex="-1" role="dialog" aria-hidden="true">
<div id="pending-contract-write" class="modal modal-fullwidth-xs fade" tabindex="-1" style="right: 0; width: auto; margin: 1rem;" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-stake" role="document">
<div class="modal-content">
<%= render BlockScoutWeb.CommonComponentsView, "_modal_close_button.html" %>
<div class="modal-stake-two-cols">
<div>
<div class="modal-header">
<div class="modal-header-group">
<%= render BlockScoutWeb.CommonComponentsView, "_loading_spinner.html" %>

@ -12,7 +12,7 @@
</svg>
</div>
<% else %>
<a href="https://metamask.io" target="_blank" data-selector="login-button" class="stakes-top-stats-login">Login</a> with MetaMask
<a target="_blank" data-selector="login-button" class="stakes-top-stats-login">Login</a>
<% end %>
</span>
<span class="stakes-top-stats-label">
@ -51,4 +51,7 @@
</span>
<% end %>
</span>
<%= if @account do %>
<button disconnect-wallet class="button btn-full-primary mt-2 mr-4 hidden">Disconnect wallet</button>
<% end %>
</div>

@ -1010,7 +1010,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_token/overview.html.eex:1
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:89 lib/block_scout_web/templates/smart_contract/_functions.html.eex:89
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:126
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:128
msgid "ETH"
msgstr ""
@ -1853,7 +1853,7 @@ msgid "QR Code"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:93
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:94
msgid "Query"
msgstr ""
@ -2981,7 +2981,7 @@ msgid "Vyper contract"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:125
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:127
msgid "WEI"
msgstr ""
@ -3031,7 +3031,7 @@ msgid "Working Stake Amount"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:93
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:94
msgid "Write"
msgstr ""

@ -1010,7 +1010,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_token/overview.html.eex:1
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:89 lib/block_scout_web/templates/smart_contract/_functions.html.eex:89
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:126
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:128
msgid "ETH"
msgstr ""
@ -1853,7 +1853,7 @@ msgid "QR Code"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:93
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:94
msgid "Query"
msgstr ""
@ -2981,7 +2981,7 @@ msgid "Vyper contract"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:125
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:127
msgid "WEI"
msgstr ""
@ -3031,7 +3031,7 @@ msgid "Working Stake Amount"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:93
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:94
msgid "Write"
msgstr ""

@ -320,6 +320,9 @@ endif
ifdef CHAIN_ID
BLOCKSCOUT_CONTAINER_PARAMS += -e 'CHAIN_ID=$(CHAIN_ID)'
endif
ifdef JSON_RPC
BLOCKSCOUT_CONTAINER_PARAMS += -e 'JSON_RPC=$(JSON_RPC)'
endif
ifdef MAX_SIZE_UNLESS_HIDE_ARRAY
BLOCKSCOUT_CONTAINER_PARAMS += -e 'MAX_SIZE_UNLESS_HIDE_ARRAY=$(MAX_SIZE_UNLESS_HIDE_ARRAY)'
endif

Loading…
Cancel
Save