608 lines
16 KiB
JavaScript
608 lines
16 KiB
JavaScript
/* eslint-disable no-console */
|
|
import BN from 'bignumber.js'
|
|
import { hexToNumber, numberToHex } from 'web3-utils'
|
|
import { SnackbarProgrammatic as Snackbar, DialogProgrammatic as Dialog } from 'buefy'
|
|
|
|
import { PROVIDERS } from '@/constants'
|
|
import networkConfig from '@/networkConfig'
|
|
import { walletConnectConnector } from '@/services'
|
|
|
|
const { toChecksumAddress } = require('web3-utils')
|
|
|
|
const state = () => {
|
|
return {
|
|
netId: 1,
|
|
walletName: '',
|
|
ethBalance: '0',
|
|
ethAccount: null,
|
|
providerConfig: {},
|
|
providerName: null,
|
|
isInitialized: false,
|
|
isReconnecting: false,
|
|
mismatchNetwork: false
|
|
}
|
|
}
|
|
|
|
const getters = {
|
|
isWalletConnect(state) {
|
|
return state.providerConfig.name === 'WalletConnect'
|
|
},
|
|
isPartialSupport(state) {
|
|
return state.providerConfig.isPartialSupport
|
|
},
|
|
hasEthAccount(state) {
|
|
return state.ethAccount !== null
|
|
},
|
|
mismatchNetwork(state) {
|
|
return state.mismatchNetwork
|
|
},
|
|
netId(state) {
|
|
return state.netId
|
|
},
|
|
networkName(state) {
|
|
return networkConfig[`netId${state.netId}`].networkName
|
|
},
|
|
currency(state) {
|
|
return networkConfig[`netId${state.netId}`].currencyName
|
|
},
|
|
nativeCurrency(state) {
|
|
return networkConfig[`netId${state.netId}`].nativeCurrency
|
|
},
|
|
networkConfig(state) {
|
|
const conf = networkConfig[`netId${state.netId}`]
|
|
return conf || networkConfig.netId1
|
|
},
|
|
getEthereumProvider: (state, getters) => (netId) => {
|
|
switch (state.providerName) {
|
|
case 'walletConnect':
|
|
return walletConnectConnector(netId || getters.netId)
|
|
case 'metamask':
|
|
case 'trustwallet':
|
|
case 'imtoken':
|
|
case 'alphawallet':
|
|
case 'generic':
|
|
default:
|
|
if (window.ethereum) {
|
|
return window.ethereum
|
|
} else {
|
|
throw new Error(this.app.i18n.t('networkDoesNotHaveEthereumProperty'))
|
|
}
|
|
}
|
|
},
|
|
isLoggedIn: (state, getters) => {
|
|
return !!state.providerName && getters.hasEthAccount
|
|
}
|
|
}
|
|
|
|
const mutations = {
|
|
IDENTIFY(state, ethAccount) {
|
|
state.ethAccount = ethAccount
|
|
},
|
|
SET_NET_ID(state, netId) {
|
|
netId = parseInt(netId, 10)
|
|
window.localStorage.setItem('netId', netId)
|
|
|
|
state.netId = netId
|
|
},
|
|
SET_RECONNECTING(state, bool) {
|
|
state.isReconnecting = bool
|
|
},
|
|
SET_MISMATCH_NETWORK(state, payload) {
|
|
state.mismatchNetwork = payload
|
|
},
|
|
SAVE_BALANCE(state, ethBalance) {
|
|
state.ethBalance = ethBalance
|
|
},
|
|
SET_WALLET_NAME(state, walletName) {
|
|
state.walletName = walletName
|
|
},
|
|
SET_PROVIDER_NAME(state, providerName) {
|
|
state.providerName = providerName
|
|
state.providerConfig = PROVIDERS[providerName]
|
|
window.localStorage.setItem('provider', providerName)
|
|
},
|
|
CLEAR_PROVIDER(state) {
|
|
state.providerName = null
|
|
state.providerConfig = {}
|
|
},
|
|
SET_INITIALIZED(state, initialized) {
|
|
state.isInitialized = initialized
|
|
}
|
|
}
|
|
|
|
const actions = {
|
|
async initialize({ dispatch, commit, getters, rootState, rootGetters }, payload) {
|
|
await dispatch('askPermission', payload)
|
|
|
|
dispatch('governance/gov/checkActiveProposals', {}, { root: true })
|
|
},
|
|
onSetInitializeData({ commit, dispatch, state }, isMismatch) {
|
|
if (isMismatch) {
|
|
commit('IDENTIFY', null)
|
|
commit('SET_INITIALIZED', false)
|
|
} else {
|
|
const providerName = window.localStorage.getItem('provider')
|
|
|
|
if (providerName && !state.isInitialized) {
|
|
dispatch('initialize', { providerName })
|
|
}
|
|
}
|
|
|
|
commit('SET_MISMATCH_NETWORK', isMismatch)
|
|
},
|
|
async checkMismatchNetwork({ dispatch, commit, state, getters }, netId) {
|
|
if (getters.isWalletConnect) {
|
|
const { id } = this.$provider.config
|
|
|
|
const isMismatch = Number(netId) !== Number(id)
|
|
|
|
await dispatch('onSetInitializeData', isMismatch)
|
|
return
|
|
}
|
|
if (!window.ethereum) {
|
|
return
|
|
}
|
|
const chainId = await window.ethereum.request({ method: 'eth_chainId' })
|
|
const isMismatch = Number(netId) !== hexToNumber(chainId)
|
|
|
|
await dispatch('onSetInitializeData', isMismatch)
|
|
},
|
|
async sendTransaction(
|
|
{ dispatch, state, rootGetters },
|
|
{ method, params, watcherParams, isAwait = true, isSaving = true, eipDisable = false }
|
|
) {
|
|
try {
|
|
const { ethAccount, netId } = state
|
|
const gasParams = rootGetters['gasPrices/getGasParams']
|
|
|
|
const callParams = {
|
|
method,
|
|
params: [
|
|
{
|
|
value: '0x00',
|
|
from: ethAccount,
|
|
...params,
|
|
...gasParams
|
|
}
|
|
]
|
|
}
|
|
|
|
dispatch('loading/showConfirmLoader', {}, { root: true })
|
|
|
|
const txHash = await this.$provider.sendRequest(callParams)
|
|
|
|
dispatch(
|
|
'loading/changeText',
|
|
{ message: this.app.i18n.t('waitUntilTransactionIsMined') },
|
|
{ root: true }
|
|
)
|
|
|
|
const activeWatcher = () =>
|
|
dispatch(
|
|
'txHashKeeper/runTxWatcherWithNotifications',
|
|
{
|
|
...watcherParams,
|
|
txHash,
|
|
isSaving,
|
|
netId
|
|
},
|
|
{ root: true }
|
|
)
|
|
|
|
if (isAwait) {
|
|
await activeWatcher()
|
|
} else {
|
|
activeWatcher()
|
|
}
|
|
|
|
dispatch('loading/disable', {}, { root: true })
|
|
|
|
return txHash
|
|
} catch (err) {
|
|
if (err.message.includes('EIP-1559')) {
|
|
return await dispatch('sendTransaction', {
|
|
method,
|
|
params,
|
|
watcherParams,
|
|
isAwait,
|
|
isSaving,
|
|
eipDisable: true
|
|
})
|
|
} else {
|
|
throw new Error(this.app.i18n.t('rejectedRequest', { description: state.walletName }))
|
|
}
|
|
} finally {
|
|
dispatch('loading/disable', {}, { root: true })
|
|
}
|
|
},
|
|
async getEncryptionPublicKey({ state }) {
|
|
try {
|
|
const { ethAccount } = state
|
|
|
|
const callParams = {
|
|
method: 'eth_getEncryptionPublicKey',
|
|
params: [ethAccount]
|
|
}
|
|
|
|
const key = await this.$provider.sendRequest(callParams)
|
|
return key
|
|
} catch (err) {
|
|
let errorMessage = 'decryptFailed'
|
|
|
|
if (err.message.includes('Trezor')) {
|
|
errorMessage = 'trezorNotSupported'
|
|
} else if (err.message.includes('Ledger')) {
|
|
errorMessage = 'ledgerNotSupported'
|
|
}
|
|
|
|
const isRejected = err.message.includes(
|
|
'MetaMask EncryptionPublicKey: User denied message EncryptionPublicKey.'
|
|
)
|
|
|
|
if (isRejected) {
|
|
throw new Error(this.app.i18n.t('rejectedRequest', { description: state.walletName }))
|
|
}
|
|
|
|
throw new Error(this.app.i18n.t(errorMessage))
|
|
}
|
|
},
|
|
async ethDecrypt({ state }, hexData) {
|
|
try {
|
|
const { ethAccount } = state
|
|
|
|
const callParams = {
|
|
method: 'eth_decrypt',
|
|
params: [hexData, ethAccount]
|
|
}
|
|
|
|
const encryptedData = await this.$provider.sendRequest(callParams)
|
|
return encryptedData
|
|
} catch (err) {
|
|
throw new Error(`Method ethDecrypt has error: ${err.message}`)
|
|
}
|
|
},
|
|
async onAccountsChanged({ dispatch, commit }, { newAccount }) {
|
|
if (newAccount) {
|
|
const account = toChecksumAddress(newAccount)
|
|
commit('IDENTIFY', account)
|
|
await dispatch('updateAccountBalance')
|
|
} else {
|
|
await dispatch('onLogOut')
|
|
}
|
|
},
|
|
onLogOut({ commit, getters, dispatch }) {
|
|
if (getters.isWalletConnect) {
|
|
const mobileProvider = this.$provider.provider
|
|
|
|
if (typeof mobileProvider.close === 'function') {
|
|
mobileProvider.close()
|
|
}
|
|
}
|
|
commit('IDENTIFY', null)
|
|
dispatch('clearProvider')
|
|
commit('SET_INITIALIZED', false)
|
|
},
|
|
async mobileWalletReconnect({ state, dispatch, commit, rootState }, { netId }) {
|
|
try {
|
|
commit('SET_RECONNECTING', true)
|
|
const { providerName } = state
|
|
const { enabled } = rootState.loading
|
|
|
|
await dispatch('onLogOut')
|
|
await dispatch('initialize', { providerName, chosenNetId: netId })
|
|
|
|
if (enabled) {
|
|
await dispatch('loading/disable', {}, { root: true })
|
|
}
|
|
} catch ({ message }) {
|
|
throw new Error(`Mobile wallet reconnect error: ${message}`)
|
|
} finally {
|
|
commit('SET_RECONNECTING', false)
|
|
}
|
|
},
|
|
async networkChangeHandler({ state, getters, commit, dispatch }, params) {
|
|
try {
|
|
if (getters.isWalletConnect) {
|
|
dispatch('loading/disable', {}, { root: true })
|
|
|
|
const networkName = networkConfig[`netId${params.netId}`].networkName
|
|
|
|
const { result } = await Dialog.confirm({
|
|
title: this.app.i18n.t('changeNetwork'),
|
|
message: this.app.i18n.t('mobileWallet.reconnect.message', { networkName }),
|
|
cancelText: this.app.i18n.t('cancelButton'),
|
|
confirmText: this.app.i18n.t('mobileWallet.reconnect.action')
|
|
})
|
|
|
|
if (result) {
|
|
await dispatch('mobileWalletReconnect', params)
|
|
this.$provider._onNetworkChanged({ id: params.netId })
|
|
}
|
|
} else {
|
|
if (state.isInitialized) {
|
|
await dispatch('switchNetwork', params)
|
|
}
|
|
await dispatch('onNetworkChanged', params)
|
|
}
|
|
} catch (err) {
|
|
console.error('networkChangeHandler', err.message)
|
|
}
|
|
},
|
|
async onNetworkChanged({ state, getters, commit, dispatch }, { netId }) {
|
|
dispatch('checkMismatchNetwork', netId)
|
|
|
|
if (netId !== 'loading' && Number(state.netId) !== Number(netId)) {
|
|
try {
|
|
if (!networkConfig[`netId${netId}`]) {
|
|
dispatch('clearProvider')
|
|
|
|
Snackbar.open({
|
|
message: this.app.i18n.t('currentNetworkIsNotSupported'),
|
|
type: 'is-primary',
|
|
position: 'is-top',
|
|
actionText: 'OK',
|
|
indefinite: true
|
|
})
|
|
throw new Error(this.app.i18n.t('currentNetworkIsNotSupported'))
|
|
}
|
|
|
|
commit('SET_NET_ID', netId)
|
|
await dispatch('application/setNativeCurrency', { netId }, { root: true })
|
|
|
|
// TODO what if all rpc failed
|
|
await dispatch('settings/checkCurrentRpc', {}, { root: true })
|
|
|
|
dispatch('application/updateSelectEvents', {}, { root: true })
|
|
|
|
if (getters.isLoggedIn) {
|
|
await dispatch('updateAccountBalance')
|
|
}
|
|
} catch (e) {
|
|
throw new Error(e.message)
|
|
}
|
|
}
|
|
},
|
|
async updateAccountBalance({ state, commit }, account = '') {
|
|
try {
|
|
const address = account || state.ethAccount
|
|
if (!address) {
|
|
return 0
|
|
}
|
|
|
|
const balance = await this.$provider.getBalance({ address })
|
|
commit('SAVE_BALANCE', balance)
|
|
return balance
|
|
} catch (err) {
|
|
console.error(`updateAccountBalance has error ${err.message}`)
|
|
}
|
|
},
|
|
clearProvider({ commit, state }) {
|
|
if (state.providerConfig.storageName) {
|
|
window.localStorage.removeItem(state.providerConfig.storageName)
|
|
}
|
|
|
|
commit('CLEAR_PROVIDER')
|
|
|
|
window.localStorage.removeItem('provider')
|
|
window.localStorage.removeItem('network')
|
|
},
|
|
async askPermission(
|
|
{ commit, dispatch, getters, rootGetters, state, rootState },
|
|
{ providerName, chosenNetId }
|
|
) {
|
|
commit('SET_PROVIDER_NAME', providerName)
|
|
|
|
const { name, listener } = state.providerConfig
|
|
|
|
commit('SET_WALLET_NAME', name)
|
|
|
|
try {
|
|
const provider = await getters.getEthereumProvider(chosenNetId)
|
|
|
|
if (providerName === 'walletConnect') {
|
|
await dispatch(listener, { provider })
|
|
}
|
|
const address = await this.$provider.initProvider(provider, {})
|
|
|
|
if (!address) {
|
|
throw new Error('lockedMetamask')
|
|
}
|
|
|
|
commit('IDENTIFY', address)
|
|
|
|
const netId = await dispatch('checkNetworkVersion')
|
|
|
|
await dispatch('onNetworkChanged', { netId })
|
|
commit('SET_INITIALIZED', true)
|
|
|
|
const { url } = rootGetters['settings/currentRpc']
|
|
this.$provider.initWeb3(url)
|
|
|
|
await dispatch('updateAccountBalance', address)
|
|
|
|
if (getters.isWalletConnect) {
|
|
if (provider.wc.peerMeta) {
|
|
commit('SET_WALLET_NAME', provider.wc.peerMeta.name)
|
|
}
|
|
}
|
|
|
|
this.$provider.on({
|
|
method: 'chainChanged',
|
|
callback: () => {
|
|
dispatch('onNetworkChanged', { netId })
|
|
}
|
|
})
|
|
|
|
this.$provider.on({
|
|
method: 'accountsChanged',
|
|
callback: ([newAccount]) => {
|
|
dispatch('onAccountsChanged', { newAccount })
|
|
}
|
|
})
|
|
|
|
return { netId, ethAccount: address }
|
|
} catch (err) {
|
|
if (providerName === 'walletConnect') {
|
|
const mobileProvider = this.$provider.provider
|
|
|
|
if (typeof mobileProvider.disconnect === 'function') {
|
|
mobileProvider.disconnect()
|
|
}
|
|
await dispatch('onLogOut')
|
|
}
|
|
throw new Error(`method askPermission has error: ${err.message}`)
|
|
}
|
|
},
|
|
walletConnectSocketListener({ state, commit, dispatch, getters, rootState }, { provider }) {
|
|
const { enabled } = rootState.loading
|
|
|
|
try {
|
|
provider.wc.on('disconnect', (error, payload) => {
|
|
if (state.isReconnecting) {
|
|
console.warn('Provider reconnect payload', { payload, error, isReconnecting: state.isReconnecting })
|
|
|
|
if (enabled) {
|
|
dispatch('loading/disable', {}, { root: true })
|
|
}
|
|
commit('SET_RECONNECTING', false)
|
|
return
|
|
}
|
|
|
|
const prevConnection = localStorage.getItem('walletconnectTimeStamp')
|
|
|
|
const isPrevConnection = new BN(Date.now()).minus(prevConnection).isGreaterThanOrEqualTo(5000)
|
|
|
|
if (isPrevConnection) {
|
|
console.warn('Provider disconnect payload', {
|
|
payload,
|
|
error,
|
|
isReconnecting: state.isReconnecting
|
|
})
|
|
|
|
dispatch('onLogOut')
|
|
}
|
|
|
|
if (enabled) {
|
|
dispatch('loading/disable', {}, { root: true })
|
|
}
|
|
})
|
|
} catch (err) {
|
|
console.error('WalletConnect listeners error: ', err)
|
|
}
|
|
},
|
|
async switchNetwork({ dispatch }, { netId }) {
|
|
try {
|
|
await this.$provider.sendRequest({
|
|
method: 'wallet_switchEthereumChain',
|
|
params: [{ chainId: numberToHex(netId) }]
|
|
})
|
|
} catch (err) {
|
|
// This error indicates that the chain has not been added to MetaMask.
|
|
if (err.message.includes('wallet_addEthereumChain')) {
|
|
return dispatch('addNetwork', { netId })
|
|
}
|
|
|
|
throw new Error(err.message)
|
|
}
|
|
},
|
|
async addNetwork(_, { netId }) {
|
|
const METAMASK_LIST = {
|
|
56: {
|
|
chainId: '0x38',
|
|
chainName: 'Binance Smart Chain Mainnet',
|
|
rpcUrls: ['https://bscrpc.com'],
|
|
nativeCurrency: {
|
|
name: 'Binance Chain Native Token',
|
|
symbol: 'BNB',
|
|
decimals: 18
|
|
},
|
|
blockExplorerUrls: ['https://bscscan.com']
|
|
},
|
|
10: {
|
|
chainId: '0xa',
|
|
chainName: 'Optimism',
|
|
rpcUrls: ['https://mainnet.optimism.io/'],
|
|
nativeCurrency: {
|
|
name: 'Ether',
|
|
symbol: 'ETH',
|
|
decimals: 18
|
|
},
|
|
blockExplorerUrls: ['https://optimistic.etherscan.io']
|
|
},
|
|
100: {
|
|
chainId: '0x64',
|
|
chainName: 'Gnosis',
|
|
rpcUrls: ['https://development.tornadocash.community/rpc/v1'],
|
|
nativeCurrency: {
|
|
name: 'xDAI',
|
|
symbol: 'xDAI',
|
|
decimals: 18
|
|
},
|
|
blockExplorerUrls: ['https://blockscout.com/xdai/mainnet']
|
|
},
|
|
137: {
|
|
chainId: '0x89',
|
|
chainName: 'Polygon Mainnet',
|
|
rpcUrls: ['https://polygon-rpc.com'],
|
|
nativeCurrency: {
|
|
name: 'MATIC',
|
|
symbol: 'MATIC',
|
|
decimals: 18
|
|
},
|
|
blockExplorerUrls: ['https://polygonscan.com']
|
|
},
|
|
42161: {
|
|
chainId: '0xA4B1',
|
|
chainName: 'Arbitrum One',
|
|
rpcUrls: ['https://arb1.arbitrum.io/rpc'],
|
|
nativeCurrency: {
|
|
name: 'Ether',
|
|
symbol: 'ETH',
|
|
decimals: 18
|
|
},
|
|
blockExplorerUrls: ['https://arbiscan.io']
|
|
},
|
|
43114: {
|
|
chainId: '0xA86A',
|
|
chainName: 'Avalanche C-Chain',
|
|
rpcUrls: ['https://api.avax.network/ext/bc/C/rpc'],
|
|
nativeCurrency: {
|
|
name: 'Avalanche',
|
|
symbol: 'AVAX',
|
|
decimals: 18
|
|
},
|
|
blockExplorerUrls: ['https://snowtrace.io']
|
|
}
|
|
}
|
|
|
|
if (METAMASK_LIST[netId]) {
|
|
await this.$provider.sendRequest({
|
|
method: 'wallet_addEthereumChain',
|
|
params: [METAMASK_LIST[netId]]
|
|
})
|
|
}
|
|
},
|
|
async checkNetworkVersion() {
|
|
try {
|
|
const id = Number(
|
|
await this.$provider.sendRequest({
|
|
method: 'eth_chainId',
|
|
params: []
|
|
})
|
|
)
|
|
|
|
return id
|
|
} catch (err) {
|
|
throw new Error(err.message)
|
|
}
|
|
}
|
|
}
|
|
|
|
export default {
|
|
namespaced: true,
|
|
state,
|
|
getters,
|
|
mutations,
|
|
actions
|
|
}
|