Skip to content

Commit

Permalink
[Fix] ext lock connect (#1149)
Browse files Browse the repository at this point in the history
* fix mm ext unlock after webview launch

* run yarn setup

* rm unused imports

* improve window.ethereum typing across the app
  • Loading branch information
BrettCleary authored Nov 15, 2024
1 parent 7a9cb7e commit 543463a
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 64 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
110 changes: 73 additions & 37 deletions src/backend/proxy/providerPreload.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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!`
}
Expand Down Expand Up @@ -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<JsonRpcResponse<any>> => {
return sendRequest(...args) as Promise<JsonRpcResponse<any>>
},
sendAsync: async (payload: any, callback: JsonRpcCallback) => {
return sendAsyncRequest(payload, callback)
sendAsync: async (
payload: any,
callback: JsonRpcCallback
): Promise<JsonRpcResponse<any>> => {
return sendAsyncRequest(payload, callback) as Promise<
JsonRpcResponse<any>
>
},
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 = {
Expand All @@ -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()
}
}

Expand All @@ -154,9 +193,6 @@ function initProvider() {
window.addEventListener('eip6963:requestProvider', () => {
announceProvider()
})

const ev = new Event('ethereum#initialized')
window.dispatchEvent(ev)
}

const exposeWindowEthereumProvider = `(${initProvider.toString()})()`
Expand Down
8 changes: 8 additions & 0 deletions src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -978,3 +980,9 @@ export interface PointsCollection {
}

export type { GamePageActions } from '@hyperplay/utils'

declare global {
interface Window {
ethereum: MetaMaskInpageProvider
}
}
16 changes: 0 additions & 16 deletions src/frontend/ExtensionHandler/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
9 changes: 1 addition & 8 deletions src/frontend/OverlayManager/index.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -64,12 +62,7 @@ const OverlayManager = observer(function ({
<webview
src={url}
className={BrowserGameStyles.browserGame}
partition={
WalletState.provider === PROVIDERS.METAMASK_MOBILE ||
PROVIDERS.WALLET_CONNECT
? 'persist:InPageWindowEthereumExternalWallet'
: undefined
}
partition={'persist:InPageWindowEthereumExternalWallet'}
webpreferences="contextIsolation=true"
// setting = to {true} does not work :(
allowpopups={trueAsStr}
Expand Down
Loading

0 comments on commit 543463a

Please sign in to comment.