From 543463a3eda7c0be5fc3604b712b7cdb5e3b64d7 Mon Sep 17 00:00:00 2001 From: Brett <27568879+BrettCleary@users.noreply.github.com> Date: Fri, 15 Nov 2024 15:21:54 -0800 Subject: [PATCH] [Fix] ext lock connect (#1149) * fix mm ext unlock after webview launch * run yarn setup * rm unused imports * improve window.ethereum typing across the app --- package.json | 2 + src/backend/proxy/providerPreload.ts | 110 ++++++++++++++++-------- src/common/types.ts | 8 ++ src/frontend/ExtensionHandler/index.tsx | 16 ---- src/frontend/OverlayManager/index.tsx | 9 +- yarn.lock | 97 ++++++++++++++++++++- 6 files changed, 178 insertions(+), 64 deletions(-) diff --git a/package.json b/package.json index e5870cf12..259c458fd 100644 --- a/package.json +++ b/package.json @@ -309,6 +309,8 @@ "@babel/plugin-transform-arrow-functions": "^7.24.7", "@lavamoat/allow-scripts": "^3.2.0", "@lavamoat/preinstall-always-fail": "^2.1.0", + "@metamask/providers": "^18.1.1", + "@metamask/utils": "^10.0.1", "@playwright/test": "^1.46.0", "@tanstack/react-query-devtools": "^5.59.20", "@testing-library/dom": "^7.31.2", diff --git a/src/backend/proxy/providerPreload.ts b/src/backend/proxy/providerPreload.ts index 932a61c21..8588c18e3 100644 --- a/src/backend/proxy/providerPreload.ts +++ b/src/backend/proxy/providerPreload.ts @@ -1,6 +1,7 @@ import { RequestArguments } from 'common/typedefs/ipcBridge' import { JsonRpcCallback } from 'common/types' import { contextBridge, ipcRenderer, webFrame } from 'electron' +import type { JsonRpcResponse } from '@metamask/utils' /** * @dev Extension must be removed prior to loading a page with this preload script. @@ -24,7 +25,11 @@ const enabledTopics = [ ] /* eslint-disable @typescript-eslint/no-explicit-any */ -const listenToRendererCalls = (fxn: string, topic: string, cb: any) => { +const listenToRendererCalls = ( + fxn: 'once' | 'on' | 'addListener', + topic: string, + cb: any +) => { if (!enabledTopics.includes(topic)) { throw `Tried to listen to ${topic} through window.ethereum!` } @@ -66,29 +71,40 @@ const providerApi = { request: async (args: RequestArguments) => { return provRequest(args) }, - send: async (...args: unknown[]) => { - return sendRequest(...args) + send: async (...args: unknown[]): Promise> => { + return sendRequest(...args) as Promise> }, - sendAsync: async (payload: any, callback: JsonRpcCallback) => { - return sendAsyncRequest(payload, callback) + sendAsync: async ( + payload: any, + callback: JsonRpcCallback + ): Promise> => { + return sendAsyncRequest(payload, callback) as Promise< + JsonRpcResponse + > }, once: (topic: string, cb: any) => { listenToRendererCalls('once', topic, cb) + return window.ethereum }, on: (topic: string, cb: any) => { listenToRendererCalls('on', topic, cb) + return window.ethereum }, off: (topic: string, cb: any) => { ipcRenderer.off('providerApi' + topic, cb) + return window.ethereum }, addListener: (topic: string, cb: any) => { listenToRendererCalls('addListener', topic, cb) + return window.ethereum }, removeListener: (topic: string, cb: any) => { ipcRenderer.removeListener('providerApi' + topic, cb) + return window.ethereum }, - removeAllListeners: (topic: string) => { + removeAllListeners: (topic?: string) => { ipcRenderer.removeAllListeners('providerApi' + topic) + return window.ethereum }, enable: async () => { const args: RequestArguments = { @@ -101,37 +117,60 @@ const providerApi = { contextBridge?.exposeInMainWorld('providerApi', providerApi) +declare global { + interface Window { + providerApi: typeof providerApi + } +} + function initProvider() { async function exposeWindowEthereum() { - console.log('exposing window ethereum') - if (!Object.hasOwn(window, 'ethereum')) { - const windowAny = window as any - windowAny.ethereum = { - request: windowAny.providerApi.provider.request, - send: windowAny.providerApi.provider.send, - sendAsync: windowAny.providerApi.provider.sendAsync, - once: windowAny.providerApi.provider.once, - on: windowAny.providerApi.provider.on, - off: windowAny.providerApi.provider.off, - addListener: windowAny.providerApi.provider.addListener, - removeListener: windowAny.providerApi.provider.removeListener, - removeAllListeners: windowAny.providerApi.provider.removeAllListeners, - isMetaMask: true, - enable: windowAny.providerApi.provider.enable, - selectedAddress: undefined, - accounts: undefined - } - - windowAny.ethereum.on('accountsChanged', (accounts: string[]) => { - console.log('accounts changed', accounts) - windowAny.ethereum.selectedAddress = accounts[0] - windowAny.ethereum.accounts = accounts - }) + const windowAny = window + windowAny.ethereum = { + request: windowAny.providerApi.provider.request, + // @ts-expect-error deprecated send call needs to be generic + send: windowAny.providerApi.provider.send, + sendAsync: windowAny.providerApi.provider.sendAsync, + once: windowAny.providerApi.provider.once, + on: windowAny.providerApi.provider.on, + off: windowAny.providerApi.provider.off, + addListener: windowAny.providerApi.provider.addListener, + removeListener: windowAny.providerApi.provider.removeListener, + removeAllListeners: windowAny.providerApi.provider.removeAllListeners, + isMetaMask: true, + enable: windowAny.providerApi.provider.enable, + selectedAddress: null, + accounts: undefined + } - const acct = await windowAny.ethereum.request({ method: 'eth_accounts' }) - windowAny.ethereum.selectedAddress = - acct && acct.length > 0 ? acct[0] : '' - windowAny.ethereum.accounts = acct + // @ts-expect-error TODO fix types in MetaMaskInpageProvider + windowAny.ethereum.on('accountsChanged', (accounts: string[]) => { + console.log('accounts changed', accounts) + // @ts-expect-error TODO fix types in MetaMaskInpageProvider + windowAny.ethereum.selectedAddress = accounts[0] + // @ts-expect-error TODO fix types in MetaMaskInpageProvider + windowAny.ethereum.accounts = accounts + }) + + const ev = new Event('ethereum#initialized') + window.dispatchEvent(ev) + + const timeNow = Date.now() + const acct = await windowAny.ethereum.request({ method: 'eth_accounts' }) + // @ts-expect-error TODO fix types in MetaMaskInpageProvider + windowAny.ethereum.selectedAddress = acct && acct.length > 0 ? acct[0] : '' + // @ts-expect-error TODO fix types in MetaMaskInpageProvider + windowAny.ethereum.accounts = acct + + /** + * opensea performs 4 responsiveness checks by calling eth_accounts with 2 second timeouts. + * if all 4 fail, clicking MetaMask in the connection options will open the metamask.io download page. + * we reload the page so that MetaMask will connect as expected if the user unlocks their wallet + * after this time period. + */ + const timeElapsed = Date.now() - timeNow + if (timeElapsed > 8000) { + window.location.reload() } } @@ -154,9 +193,6 @@ function initProvider() { window.addEventListener('eip6963:requestProvider', () => { announceProvider() }) - - const ev = new Event('ethereum#initialized') - window.dispatchEvent(ev) } const exposeWindowEthereumProvider = `(${initProvider.toString()})()` diff --git a/src/common/types.ts b/src/common/types.ts index cb534ef9c..6c8a40431 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -14,6 +14,8 @@ import { IconDefinition } from '@fortawesome/free-solid-svg-icons' import { DropdownItemType } from '@hyperplay/ui' export type { Quest } from '@hyperplay/utils' +import { MetaMaskInpageProvider } from '@metamask/providers' + export type { Listing as HyperPlayRelease, ProjectMetaApi as HyperPlayProjectMeta, @@ -978,3 +980,9 @@ export interface PointsCollection { } export type { GamePageActions } from '@hyperplay/utils' + +declare global { + interface Window { + ethereum: MetaMaskInpageProvider + } +} diff --git a/src/frontend/ExtensionHandler/index.tsx b/src/frontend/ExtensionHandler/index.tsx index e5e02ab1e..d2bd5ed7a 100644 --- a/src/frontend/ExtensionHandler/index.tsx +++ b/src/frontend/ExtensionHandler/index.tsx @@ -3,22 +3,6 @@ import { observer } from 'mobx-react-lite' import React from 'react' import { useNavigate } from 'react-router-dom' -declare global { - interface Window { - ethereum: { - /*eslint-disable-next-line @typescript-eslint/no-explicit-any */ - request: (args: any) => any - /*eslint-disable-next-line @typescript-eslint/no-explicit-any */ - send: (...args: any) => any - /*eslint-disable-next-line @typescript-eslint/no-explicit-any */ - sendAsync: (...args: any) => any - /*eslint-disable-next-line @typescript-eslint/no-explicit-any */ - on: (topic: string, handler: (...args: any) => void) => void - isConnected: () => boolean - } - } -} - const ExtensionHandler = observer(function () { const navigate = useNavigate() diff --git a/src/frontend/OverlayManager/index.tsx b/src/frontend/OverlayManager/index.tsx index b05cfdfea..44e753046 100644 --- a/src/frontend/OverlayManager/index.tsx +++ b/src/frontend/OverlayManager/index.tsx @@ -1,9 +1,7 @@ import React, { useRef } from 'react' import BrowserGameStyles from './index.module.scss' -import { PROVIDERS } from 'common/types/proxy-types' import { observer } from 'mobx-react-lite' import OverlayState from 'frontend/state/OverlayState' -import WalletState from 'frontend/state/WalletState' import { BrowserGameProps } from './types' import { Overlay } from './Overlay' import { WebviewTag } from 'electron' @@ -64,12 +62,7 @@ const OverlayManager = observer(function ({ =0.10.0 <1.0" +extension-port-stream@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/extension-port-stream/-/extension-port-stream-4.2.0.tgz#c3966488f1c39759bbcec7593945fe40187b770f" + integrity sha512-i5IgiPVMVrHN+Zx8PRjvFsOw8L1A3sboVwPZghDjW9Yp1BMmBDE6mCcTNu4xMXPYduBOwI3CBK7wd72LcOyD6g== + dependencies: + readable-stream "^3.6.2 || ^4.4.2" + extract-zip@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" @@ -12735,7 +12801,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -12829,7 +12904,14 @@ stringify-entities@^4.0.0: character-entities-html4 "^2.0.0" character-entities-legacy "^3.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -14084,7 +14166,7 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -14102,6 +14184,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"