metamask.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. /* eslint-disable no-console */
  2. import BN from 'bignumber.js'
  3. import { hexToNumber, numberToHex } from 'web3-utils'
  4. import { SnackbarProgrammatic as Snackbar, DialogProgrammatic as Dialog } from 'buefy'
  5. import { PROVIDERS } from '@/constants'
  6. import networkConfig from '@/networkConfig'
  7. import { walletConnectConnector } from '@/services'
  8. import SanctionsListAbi from '@/abis/SanctionsList.abi'
  9. const { toChecksumAddress } = require('web3-utils')
  10. const state = () => {
  11. return {
  12. netId: 1,
  13. walletName: '',
  14. ethBalance: '0',
  15. ethAccount: null,
  16. providerConfig: {},
  17. providerName: null,
  18. isInitialized: false,
  19. isReconnecting: false,
  20. mismatchNetwork: false
  21. }
  22. }
  23. const getters = {
  24. isWalletConnect(state) {
  25. return state.providerConfig.name === 'WalletConnect'
  26. },
  27. isPartialSupport(state) {
  28. return state.providerConfig.isPartialSupport
  29. },
  30. hasEthAccount(state) {
  31. return state.ethAccount !== null
  32. },
  33. mismatchNetwork(state) {
  34. return state.mismatchNetwork
  35. },
  36. netId(state) {
  37. return state.netId
  38. },
  39. networkName(state) {
  40. return networkConfig[`netId${state.netId}`].networkName
  41. },
  42. currency(state) {
  43. return networkConfig[`netId${state.netId}`].currencyName
  44. },
  45. nativeCurrency(state) {
  46. return networkConfig[`netId${state.netId}`].nativeCurrency
  47. },
  48. networkConfig(state) {
  49. const conf = networkConfig[`netId${state.netId}`]
  50. return conf || networkConfig.netId1
  51. },
  52. getEthereumProvider: (state, getters) => (netId) => {
  53. switch (state.providerName) {
  54. case 'walletConnect':
  55. return walletConnectConnector(netId || getters.netId)
  56. case 'metamask':
  57. case 'trustwallet':
  58. case 'imtoken':
  59. case 'alphawallet':
  60. case 'generic':
  61. default:
  62. if (window.ethereum) {
  63. return window.ethereum
  64. } else {
  65. throw new Error(this.app.i18n.t('networkDoesNotHaveEthereumProperty'))
  66. }
  67. }
  68. },
  69. isLoggedIn: (state, getters) => {
  70. return !!state.providerName && getters.hasEthAccount
  71. }
  72. }
  73. const mutations = {
  74. IDENTIFY(state, ethAccount) {
  75. state.ethAccount = ethAccount
  76. },
  77. SET_NET_ID(state, netId) {
  78. netId = parseInt(netId, 10)
  79. window.localStorage.setItem('netId', netId)
  80. state.netId = netId
  81. },
  82. SET_RECONNECTING(state, bool) {
  83. state.isReconnecting = bool
  84. },
  85. SET_MISMATCH_NETWORK(state, payload) {
  86. state.mismatchNetwork = payload
  87. },
  88. SAVE_BALANCE(state, ethBalance) {
  89. state.ethBalance = ethBalance
  90. },
  91. SET_WALLET_NAME(state, walletName) {
  92. state.walletName = walletName
  93. },
  94. SET_PROVIDER_NAME(state, providerName) {
  95. state.providerName = providerName
  96. state.providerConfig = PROVIDERS[providerName]
  97. window.localStorage.setItem('provider', providerName)
  98. },
  99. CLEAR_PROVIDER(state) {
  100. state.providerName = null
  101. state.providerConfig = {}
  102. },
  103. SET_INITIALIZED(state, initialized) {
  104. state.isInitialized = initialized
  105. }
  106. }
  107. const actions = {
  108. async initialize({ dispatch, commit, getters, rootState, rootGetters }, payload) {
  109. await dispatch('askPermission', payload)
  110. dispatch('governance/gov/checkActiveProposals', {}, { root: true })
  111. },
  112. onSetInitializeData({ commit, dispatch, state }, isMismatch) {
  113. if (isMismatch) {
  114. commit('IDENTIFY', null)
  115. commit('SET_INITIALIZED', false)
  116. } else {
  117. const providerName = window.localStorage.getItem('provider')
  118. if (providerName && !state.isInitialized) {
  119. dispatch('initialize', { providerName })
  120. }
  121. }
  122. commit('SET_MISMATCH_NETWORK', isMismatch)
  123. },
  124. async checkMismatchNetwork({ dispatch, commit, state, getters }, netId) {
  125. if (getters.isWalletConnect) {
  126. const { id } = this.$provider.config
  127. const isMismatch = Number(netId) !== Number(id)
  128. await dispatch('onSetInitializeData', isMismatch)
  129. return
  130. }
  131. if (!window.ethereum) {
  132. return
  133. }
  134. const chainId = await window.ethereum.request({ method: 'eth_chainId' })
  135. const isMismatch = Number(netId) !== hexToNumber(chainId)
  136. await dispatch('onSetInitializeData', isMismatch)
  137. },
  138. async sendTransaction(
  139. { dispatch, state, rootGetters },
  140. { method, params, watcherParams, isAwait = true, isSaving = true, eipDisable = false }
  141. ) {
  142. try {
  143. const { ethAccount, netId } = state
  144. const gasParams = rootGetters['gasPrices/getGasParams']
  145. const callParams = {
  146. method,
  147. params: [
  148. {
  149. value: '0x00',
  150. from: ethAccount,
  151. ...params,
  152. ...gasParams
  153. }
  154. ]
  155. }
  156. dispatch('loading/showConfirmLoader', {}, { root: true })
  157. const txHash = await this.$provider.sendRequest(callParams)
  158. dispatch(
  159. 'loading/changeText',
  160. { message: this.app.i18n.t('waitUntilTransactionIsMined') },
  161. { root: true }
  162. )
  163. const activeWatcher = () =>
  164. dispatch(
  165. 'txHashKeeper/runTxWatcherWithNotifications',
  166. {
  167. ...watcherParams,
  168. txHash,
  169. isSaving,
  170. netId
  171. },
  172. { root: true }
  173. )
  174. if (isAwait) {
  175. await activeWatcher()
  176. } else {
  177. activeWatcher()
  178. }
  179. dispatch('loading/disable', {}, { root: true })
  180. return txHash
  181. } catch (err) {
  182. if (err.message.includes('EIP-1559')) {
  183. return await dispatch('sendTransaction', {
  184. method,
  185. params,
  186. watcherParams,
  187. isAwait,
  188. isSaving,
  189. eipDisable: true
  190. })
  191. } else {
  192. throw new Error(this.app.i18n.t('rejectedRequest', { description: state.walletName }))
  193. }
  194. } finally {
  195. dispatch('loading/disable', {}, { root: true })
  196. }
  197. },
  198. async getEncryptionPublicKey({ state }) {
  199. try {
  200. const { ethAccount } = state
  201. const callParams = {
  202. method: 'eth_getEncryptionPublicKey',
  203. params: [ethAccount]
  204. }
  205. const key = await this.$provider.sendRequest(callParams)
  206. return key
  207. } catch (err) {
  208. let errorMessage = 'decryptFailed'
  209. if (err.message.includes('Trezor')) {
  210. errorMessage = 'trezorNotSupported'
  211. } else if (err.message.includes('Ledger')) {
  212. errorMessage = 'ledgerNotSupported'
  213. }
  214. const isRejected = err.message.includes(
  215. 'MetaMask EncryptionPublicKey: User denied message EncryptionPublicKey.'
  216. )
  217. if (isRejected) {
  218. throw new Error(this.app.i18n.t('rejectedRequest', { description: state.walletName }))
  219. }
  220. throw new Error(this.app.i18n.t(errorMessage))
  221. }
  222. },
  223. async ethDecrypt({ state }, hexData) {
  224. try {
  225. const { ethAccount } = state
  226. const callParams = {
  227. method: 'eth_decrypt',
  228. params: [hexData, ethAccount]
  229. }
  230. const encryptedData = await this.$provider.sendRequest(callParams)
  231. return encryptedData
  232. } catch (err) {
  233. throw new Error(`Method ethDecrypt has error: ${err.message}`)
  234. }
  235. },
  236. async onAccountsChanged({ dispatch, commit }, { newAccount }) {
  237. if (newAccount) {
  238. const account = toChecksumAddress(newAccount)
  239. commit('IDENTIFY', account)
  240. await dispatch('updateAccountBalance')
  241. } else {
  242. await dispatch('onLogOut')
  243. }
  244. },
  245. onLogOut({ commit, getters, dispatch }) {
  246. if (getters.isWalletConnect) {
  247. const mobileProvider = this.$provider.provider
  248. if (typeof mobileProvider.close === 'function') {
  249. mobileProvider.close()
  250. }
  251. }
  252. commit('IDENTIFY', null)
  253. dispatch('clearProvider')
  254. commit('SET_INITIALIZED', false)
  255. },
  256. async mobileWalletReconnect({ state, dispatch, commit, rootState }, { netId }) {
  257. try {
  258. commit('SET_RECONNECTING', true)
  259. const { providerName } = state
  260. const { enabled } = rootState.loading
  261. await dispatch('onLogOut')
  262. await dispatch('initialize', { providerName, chosenNetId: netId })
  263. if (enabled) {
  264. await dispatch('loading/disable', {}, { root: true })
  265. }
  266. } catch ({ message }) {
  267. throw new Error(`Mobile wallet reconnect error: ${message}`)
  268. } finally {
  269. commit('SET_RECONNECTING', false)
  270. }
  271. },
  272. async networkChangeHandler({ state, getters, commit, dispatch }, params) {
  273. try {
  274. if (getters.isWalletConnect) {
  275. dispatch('loading/disable', {}, { root: true })
  276. const networkName = networkConfig[`netId${params.netId}`].networkName
  277. const { result } = await Dialog.confirm({
  278. title: this.app.i18n.t('changeNetwork'),
  279. message: this.app.i18n.t('mobileWallet.reconnect.message', { networkName }),
  280. cancelText: this.app.i18n.t('cancelButton'),
  281. confirmText: this.app.i18n.t('mobileWallet.reconnect.action')
  282. })
  283. if (result) {
  284. await dispatch('mobileWalletReconnect', params)
  285. this.$provider._onNetworkChanged({ id: params.netId })
  286. }
  287. } else {
  288. if (state.isInitialized) {
  289. await dispatch('switchNetwork', params)
  290. }
  291. await dispatch('onNetworkChanged', params)
  292. }
  293. } catch (err) {
  294. console.error('networkChangeHandler', err.message)
  295. }
  296. },
  297. async checkIsSanctioned({ rootGetters }, { address }) {
  298. const ethProvider = rootGetters['relayer/ethProvider']
  299. const contract = new ethProvider.eth.Contract(
  300. SanctionsListAbi,
  301. '0x40C57923924B5c5c5455c48D93317139ADDaC8fb'
  302. )
  303. const isSanctioned = await contract.methods.isSanctioned(address).call()
  304. if (isSanctioned) {
  305. window.onbeforeunload = null
  306. window.location = 'https://twitter.com/TornadoCash/status/1514904975037669386'
  307. }
  308. },
  309. async onNetworkChanged({ state, getters, commit, dispatch }, { netId }) {
  310. dispatch('checkMismatchNetwork', netId)
  311. if (netId !== 'loading' && Number(state.netId) !== Number(netId)) {
  312. try {
  313. if (!networkConfig[`netId${netId}`]) {
  314. dispatch('clearProvider')
  315. Snackbar.open({
  316. message: this.app.i18n.t('currentNetworkIsNotSupported'),
  317. type: 'is-primary',
  318. position: 'is-top',
  319. actionText: 'OK',
  320. indefinite: true
  321. })
  322. throw new Error(this.app.i18n.t('currentNetworkIsNotSupported'))
  323. }
  324. commit('SET_NET_ID', netId)
  325. await dispatch('application/setNativeCurrency', { netId }, { root: true })
  326. // TODO what if all rpc failed
  327. await dispatch('settings/checkCurrentRpc', {}, { root: true })
  328. dispatch('application/updateSelectEvents', {}, { root: true })
  329. if (getters.isLoggedIn) {
  330. await dispatch('updateAccountBalance')
  331. }
  332. } catch (e) {
  333. throw new Error(e.message)
  334. }
  335. }
  336. },
  337. async updateAccountBalance({ state, commit }, account = '') {
  338. try {
  339. const address = account || state.ethAccount
  340. if (!address) {
  341. return 0
  342. }
  343. const balance = await this.$provider.getBalance({ address })
  344. commit('SAVE_BALANCE', balance)
  345. return balance
  346. } catch (err) {
  347. console.error(`updateAccountBalance has error ${err.message}`)
  348. }
  349. },
  350. clearProvider({ commit, state }) {
  351. if (state.providerConfig.storageName) {
  352. window.localStorage.removeItem(state.providerConfig.storageName)
  353. }
  354. commit('CLEAR_PROVIDER')
  355. window.localStorage.removeItem('provider')
  356. window.localStorage.removeItem('network')
  357. },
  358. async askPermission(
  359. { commit, dispatch, getters, rootGetters, state, rootState },
  360. { providerName, chosenNetId }
  361. ) {
  362. commit('SET_PROVIDER_NAME', providerName)
  363. const { name, listener } = state.providerConfig
  364. commit('SET_WALLET_NAME', name)
  365. try {
  366. const provider = await getters.getEthereumProvider(chosenNetId)
  367. if (providerName === 'walletConnect') {
  368. await dispatch(listener, { provider })
  369. }
  370. const address = await this.$provider.initProvider(provider, {})
  371. if (!address) {
  372. throw new Error('lockedMetamask')
  373. }
  374. await dispatch('checkIsSanctioned', { address })
  375. commit('IDENTIFY', address)
  376. const netId = await dispatch('checkNetworkVersion')
  377. await dispatch('onNetworkChanged', { netId })
  378. commit('SET_INITIALIZED', true)
  379. const { url } = rootGetters['settings/currentRpc']
  380. this.$provider.initWeb3(url)
  381. await dispatch('updateAccountBalance', address)
  382. if (getters.isWalletConnect) {
  383. if (provider.wc.peerMeta) {
  384. commit('SET_WALLET_NAME', provider.wc.peerMeta.name)
  385. }
  386. }
  387. this.$provider.on({
  388. method: 'chainChanged',
  389. callback: () => {
  390. dispatch('onNetworkChanged', { netId })
  391. }
  392. })
  393. this.$provider.on({
  394. method: 'accountsChanged',
  395. callback: ([newAccount]) => {
  396. dispatch('onAccountsChanged', { newAccount })
  397. }
  398. })
  399. return { netId, ethAccount: address }
  400. } catch (err) {
  401. if (providerName === 'walletConnect') {
  402. const mobileProvider = this.$provider.provider
  403. if (typeof mobileProvider.disconnect === 'function') {
  404. mobileProvider.disconnect()
  405. }
  406. await dispatch('onLogOut')
  407. }
  408. throw new Error(`method askPermission has error: ${err.message}`)
  409. }
  410. },
  411. walletConnectSocketListener({ state, commit, dispatch, getters, rootState }, { provider }) {
  412. const { enabled } = rootState.loading
  413. try {
  414. provider.wc.on('disconnect', (error, payload) => {
  415. if (state.isReconnecting) {
  416. console.warn('Provider reconnect payload', { payload, error, isReconnecting: state.isReconnecting })
  417. if (enabled) {
  418. dispatch('loading/disable', {}, { root: true })
  419. }
  420. commit('SET_RECONNECTING', false)
  421. return
  422. }
  423. const prevConnection = localStorage.getItem('walletconnectTimeStamp')
  424. const isPrevConnection = new BN(Date.now()).minus(prevConnection).isGreaterThanOrEqualTo(5000)
  425. if (isPrevConnection) {
  426. console.warn('Provider disconnect payload', {
  427. payload,
  428. error,
  429. isReconnecting: state.isReconnecting
  430. })
  431. dispatch('onLogOut')
  432. }
  433. if (enabled) {
  434. dispatch('loading/disable', {}, { root: true })
  435. }
  436. })
  437. } catch (err) {
  438. console.error('WalletConnect listeners error: ', err)
  439. }
  440. },
  441. async switchNetwork({ dispatch }, { netId }) {
  442. try {
  443. await this.$provider.sendRequest({
  444. method: 'wallet_switchEthereumChain',
  445. params: [{ chainId: numberToHex(netId) }]
  446. })
  447. } catch (err) {
  448. // This error indicates that the chain has not been added to MetaMask.
  449. if (err.message.includes('wallet_addEthereumChain')) {
  450. return dispatch('addNetwork', { netId })
  451. }
  452. throw new Error(err.message)
  453. }
  454. },
  455. async addNetwork(_, { netId }) {
  456. const METAMASK_LIST = {
  457. 56: {
  458. chainId: '0x38',
  459. chainName: 'Binance Smart Chain Mainnet',
  460. rpcUrls: ['https://bsc-dataseed1.ninicoin.io'],
  461. nativeCurrency: {
  462. name: 'Binance Chain Native Token',
  463. symbol: 'BNB',
  464. decimals: 18
  465. },
  466. blockExplorerUrls: ['https://bscscan.com']
  467. },
  468. 10: {
  469. chainId: '0xa',
  470. chainName: 'Optimism',
  471. rpcUrls: ['https://mainnet.optimism.io/'],
  472. nativeCurrency: {
  473. name: 'Ether',
  474. symbol: 'ETH',
  475. decimals: 18
  476. },
  477. blockExplorerUrls: ['https://optimistic.etherscan.io']
  478. },
  479. 100: {
  480. chainId: '0x64',
  481. chainName: 'Gnosis',
  482. rpcUrls: ['https://rpc.gnosischain.com'],
  483. nativeCurrency: {
  484. name: 'xDAI',
  485. symbol: 'xDAI',
  486. decimals: 18
  487. },
  488. blockExplorerUrls: ['https://blockscout.com/xdai/mainnet']
  489. },
  490. 137: {
  491. chainId: '0x89',
  492. chainName: 'Polygon Mainnet',
  493. rpcUrls: ['https://rpc-mainnet.maticvigil.com'],
  494. nativeCurrency: {
  495. name: 'MATIC',
  496. symbol: 'MATIC',
  497. decimals: 18
  498. },
  499. blockExplorerUrls: ['https://polygonscan.com']
  500. },
  501. 42161: {
  502. chainId: '0xA4B1',
  503. chainName: 'Arbitrum One',
  504. rpcUrls: ['https://arb1.arbitrum.io/rpc'],
  505. nativeCurrency: {
  506. name: 'Ether',
  507. symbol: 'ETH',
  508. decimals: 18
  509. },
  510. blockExplorerUrls: ['https://arbiscan.io']
  511. },
  512. 43114: {
  513. chainId: '0xA86A',
  514. chainName: 'Avalanche C-Chain',
  515. rpcUrls: ['https://api.avax.network/ext/bc/C/rpc'],
  516. nativeCurrency: {
  517. name: 'Avalanche',
  518. symbol: 'AVAX',
  519. decimals: 18
  520. },
  521. blockExplorerUrls: ['https://snowtrace.io']
  522. }
  523. }
  524. if (METAMASK_LIST[netId]) {
  525. await this.$provider.sendRequest({
  526. method: 'wallet_addEthereumChain',
  527. params: [METAMASK_LIST[netId]]
  528. })
  529. }
  530. },
  531. async checkNetworkVersion() {
  532. try {
  533. const id = Number(
  534. await this.$provider.sendRequest({
  535. method: 'eth_chainId',
  536. params: []
  537. })
  538. )
  539. return id
  540. } catch (err) {
  541. throw new Error(err.message)
  542. }
  543. }
  544. }
  545. export default {
  546. namespaced: true,
  547. state,
  548. getters,
  549. mutations,
  550. actions
  551. }