Skip to content

Commit

Permalink
Merge pull request #2120 from oasisprotocol/lw/manifest-v3-3
Browse files Browse the repository at this point in the history
Migrate extension wallet to Manifest V3 architecture
  • Loading branch information
lukaw3d authored Feb 1, 2025
2 parents b340359 + 330eb29 commit 94420f5
Show file tree
Hide file tree
Showing 30 changed files with 249 additions and 178 deletions.
4 changes: 4 additions & 0 deletions .changelog/2120.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Migrate extension wallet to Manifest V3 architecture

New limitations: extension users must create a profile while importing a
wallet. And only one popup/tab can be opened at the same time.
16 changes: 16 additions & 0 deletions extension/src/ExtLedgerAccessPopup/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="Content-Security-Policy" content="{{{ REACT_APP_META_CSP }}}" />
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<title>ROSE Wallet</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="./index.tsx"></script>
</body>
</html>
29 changes: 29 additions & 0 deletions extension/src/ExtLedgerAccessPopup/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'react-app-polyfill/stable'

import * as React from 'react'
import { createRoot } from 'react-dom/client'

// Use consistent styling
import 'sanitize.css/sanitize.css'

import { ThemeProviderWithoutRedux } from 'styles/theme/ThemeProvider'

// Initialize languages
import 'locales/i18n'

// Fonts
import 'styles/main.css'
import { ExtLedgerAccessPopup } from './ExtLedgerAccessPopup'

const container = document.getElementById('root') as HTMLElement
const root = createRoot(container!)

root.render(
// Avoid redux: it's not necessary and has it has a little potential to cause
// conflicts in stored state because it runs in parallel with wallet popup.
<ThemeProviderWithoutRedux>
<React.StrictMode>
<ExtLedgerAccessPopup />
</React.StrictMode>
</ThemeProviderWithoutRedux>,
)
6 changes: 0 additions & 6 deletions extension/src/background.html

This file was deleted.

6 changes: 0 additions & 6 deletions extension/src/background.ts

This file was deleted.

30 changes: 20 additions & 10 deletions extension/src/popup/popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,34 @@ import React from 'react'
import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux'
import { HelmetProvider } from 'react-helmet-async'
import { Store } from 'webext-redux'
import { configureAppStore } from 'store/configureStore'
import { createHashRouter, RouterProvider } from 'react-router-dom'

import { ThemeProvider } from 'styles/theme/ThemeProvider'
import browser from 'webextension-polyfill'

import 'locales/i18n'
import 'sanitize.css/sanitize.css'
import 'styles/main.css'
import { routes } from './routes'

const container = document.getElementById('root') as HTMLElement
const root = createRoot(container!)
const store = new Store()
const router = createHashRouter(routes)

store.ready().then(() => {
// Only allow one popup/tab with redux, so no need to sync state.
if (browser.extension.getViews().length > 1) {
// If something else is a tab, it must be ExtLedgerAccessPopup. Focus that and close self.
;(async () => {
// Unexpected: persistent popup is classified as 'TAB' in contexts API
const tabsAndPersistentPopups = await browser.runtime.getContexts({ contextTypes: ['TAB'] })
for (const c of tabsAndPersistentPopups) {
await browser.windows.update(c.windowId, { focused: true })
await browser.tabs.update(c.tabId, { active: true })
}
window.close()
})()
} else {
const container = document.getElementById('root') as HTMLElement
const root = createRoot(container!)
const store = configureAppStore()
const router = createHashRouter(routes)
root.render(
<Provider store={store}>
<ThemeProvider>
Expand All @@ -29,6 +41,4 @@ store.ready().then(() => {
</ThemeProvider>
</Provider>,
)
})

console.log('popup')
}
5 changes: 0 additions & 5 deletions extension/src/popup/routes.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from 'react'
import { RouteObject } from 'react-router-dom'
import { App } from 'app'
import { ExtLedgerAccessPopup } from '../ExtLedgerAccessPopup/ExtLedgerAccessPopup'
import { FromLedgerWebExtension } from 'app/pages/OpenWalletPage/FromLedgerWebExtension'
import { commonRoutes } from '../../../src/commonRoutes'
import { SelectOpenMethod } from '../../../src/app/pages/OpenWalletPage'
Expand All @@ -22,8 +21,4 @@ export const routes: RouteObject[] = [
},
],
},
{
path: 'open-wallet/connect-device',
element: <ExtLedgerAccessPopup />,
},
]
7 changes: 4 additions & 3 deletions internals/getSecurityHeaders.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ const getCsp = ({ isExtension, isDev }) =>
default-src 'none';
script-src
'self'
${isDev ? reactErrorOverlay : ''}
${isDev ? hmrScripts : ''}
'report-sample';
${!isExtension && isDev ? reactErrorOverlay : '' /* Manifest v3 doesn't allow anything */}
${!isExtension && isDev ? hmrScripts : ''}
${!isExtension ? 'report-sample' : ''}
;
style-src
'self'
'unsafe-inline'
Expand Down
2 changes: 1 addition & 1 deletion internals/scripts/validate-ext-manifest.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const manifest = require('../../build-ext/manifest.json')

if (manifest.content_security_policy.includes('EXTENSION_CSP')) {
if (manifest.content_security_policy.extension_pages.includes('EXTENSION_CSP')) {
console.error('CSP rules were not injected by parcel-transformer-env-variables-injection!')
process.exit(1)
}
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,7 @@
"tweetnacl": "1.0.3",
"typed-redux-saga": "1.5.0",
"valid-url": "1.0.9",
"webext-redux": "2.1.9",
"webextension-polyfill": "0.10.0"
"webextension-polyfill": "0.12.0"
},
"devDependencies": {
"@capacitor/cli": "6.0.0",
Expand All @@ -123,7 +122,7 @@
"@types/testing-library__jest-dom": "5.14.9",
"@types/valid-url": "1.0.7",
"@types/w3c-web-usb": "1.0.10",
"@types/webextension-polyfill": "0.10.7",
"@types/webextension-polyfill": "0.12.1",
"@typescript-eslint/eslint-plugin": "6.9.1",
"@typescript-eslint/parser": "6.9.1",
"babel-plugin-istanbul": "6.1.1",
Expand Down
21 changes: 3 additions & 18 deletions playwright/tests/extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { expect } from '@playwright/test'
import { warnSlowApi } from '../utils/warnSlowApi'
import { mockApi } from '../utils/mockApi'
import { expectNoErrorsInConsole } from '../utils/expectNoErrorsInConsole'
import { fillPrivateKeyWithoutPassword } from '../utils/fillPrivateKey'
import { fillPrivateKeyAndPassword } from '../utils/fillPrivateKey'
import { privateKey, privateKeyAddress } from '../../src/utils/__fixtures__/test-inputs'

test.beforeEach(async ({ context }) => {
Expand Down Expand Up @@ -60,10 +60,10 @@ test.describe('The extension popup should load', () => {
},
})
await page.goto(`${extensionPopupURL}/open-wallet/private-key`)
await fillPrivateKeyWithoutPassword(page, {
await fillPrivateKeyAndPassword(page, {
privateKey: privateKey,
privateKeyAddress: privateKeyAddress,
persistenceCheckboxDisabled: false,
persistenceCheckboxDisabled: 'disabled-checked',
})
await expect(page.getByTestId('account-selector')).toBeVisible()
await page.getByRole('link', { name: 'Buy' }).click()
Expand All @@ -89,23 +89,8 @@ test.describe('The extension popup should load', () => {
await page.goto(`${extensionPopupURL}/e2e`)
await page.getByRole('button', { name: 'Trigger fatal saga error' }).click()
await expect(page.getByTestId('fatalerror-stacktrace')).toBeVisible()
await page.close()
}

{
// Gets stuck on error despite reloading or reopening the popup
const page = await context.newPage()
await page.goto(`${extensionPopupURL}/`)
await expect(page.getByTestId('fatalerror-stacktrace')).toBeVisible()
await page.reload()
await expect(page.getByTestId('fatalerror-stacktrace')).toBeVisible()
await page.close()
}

{
// Gets unstuck with a button
const page = await context.newPage()
await page.goto(`${extensionPopupURL}/`)
await page
.getByRole('button', { name: 'Reload app' })
.click()
Expand Down
10 changes: 4 additions & 6 deletions playwright/tests/migrating-v0-ext-persisted-state.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ test('Migrate from V0 extension persisted state to valid RootState', async ({
}) => {
await test.step('start migration', async () => {
await addPersistedStorageV0(page, extensionManifestURL)
await page.goto(`${extensionPopupURL}/`)
await page.goto(`${extensionPopupURL}/e2e`)
await page.getByPlaceholder('Enter your password', { exact: true }).fill(password)
await page.keyboard.press('Enter')
})
Expand Down Expand Up @@ -53,11 +53,9 @@ test('Migrate from V0 extension persisted state to valid RootState', async ({
})

await test.step('should result in valid RootState', async () => {
const tab2 = await context.newPage()
await tab2.goto(`${extensionPopupURL}/e2e`)
await tab2.getByTestId('account-selector').click({ timeout: 15_000 })
await expect(tab2.getByTestId('account-choice')).toHaveCount(7)
const decryptedStateV1 = await tab2.evaluate(() => {
await page.getByTestId('account-selector').click({ timeout: 15_000 })
await expect(page.getByTestId('account-choice')).toHaveCount(7)
const decryptedStateV1 = await page.evaluate(() => {
const store: any = window['store']
return store.getState() as RootState
})
Expand Down
10 changes: 5 additions & 5 deletions playwright/utils/extensionTestExtend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const extensionPath = path.join(__dirname, '..', process.env.EXTENSION_PATH ?? '
const getPopupFile = () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const extensionManifest = require(path.join(extensionPath, '/manifest.json'))
return extensionManifest.browser_action.default_popup
return extensionManifest.action.default_popup
}

// From https://playwright.dev/docs/chrome-extensions
Expand All @@ -29,12 +29,12 @@ const test = base.extend<{
},
extensionId: async ({ context }, use) => {
// for manifest v2:
let [background] = context.backgroundPages()
if (!background) background = await context.waitForEvent('backgroundpage')
// let [background] = context.backgroundPages()
// if (!background) background = await context.waitForEvent('backgroundpage')

// for manifest v3:
// let [background] = context.serviceWorkers()
// if (!background) background = await context.waitForEvent('serviceworker')
let [background] = context.serviceWorkers()
if (!background) background = await context.waitForEvent('serviceworker')

const extensionId = background.url().split('/')[2]
await use(extensionId)
Expand Down
16 changes: 13 additions & 3 deletions playwright/utils/fillPrivateKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,24 @@ export async function fillPrivateKeyWithoutPassword(

export async function fillPrivateKeyAndPassword(
page: Page,
params: { privateKey?: string; privateKeyAddress?: string; ticker?: string } = {},
params: {
privateKey?: string
privateKeyAddress?: string
ticker?: string
persistenceCheckboxDisabled?: false | 'disabled-checked'
} = {},
) {
await test.step('fillPrivateKeyAndPassword', async () => {
await expect(page).toHaveURL(new RegExp('/open-wallet/private-key'))

const persistence = await page.getByText('Create a profile')
await expect(persistence).toBeEnabled()
await persistence.check()
if (params.persistenceCheckboxDisabled === 'disabled-checked') {
await expect(persistence).toBeDisabled()
await expect(persistence).toBeChecked()
} else {
await expect(persistence).toBeEnabled()
await persistence.check()
}

await page.getByPlaceholder('Enter your private key here').fill(params.privateKey ?? privateKey)
await page.getByPlaceholder('Enter your password', { exact: true }).fill(password)
Expand Down
2 changes: 1 addition & 1 deletion playwright/utils/mockApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {
} from '../../src/vendors/nexus/index'
import { StringifiedBigInt } from '../../src/types/StringifiedBigInt'

export async function mockApi(context: BrowserContext | Page, balance: StringifiedBigInt) {
export async function mockApi(context: BrowserContext | Page, balance: number | StringifiedBigInt) {
await context.addInitScript(() => ((window as any).REACT_APP_BACKEND = 'nexus'))
await context.route('**/consensus/accounts/*', route => {
route.fulfill({
Expand Down
10 changes: 3 additions & 7 deletions playwright/utils/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export async function clearPersistedStorage(
// v0 ext
const chrome = (window as any).chrome
chrome?.storage?.local?.clear(() => {})
chrome?.extension?.getBackgroundPage?.().location.reload()
})
await page.reload()
}

export async function addPersistedStorageV1(
Expand All @@ -32,10 +32,7 @@ export async function addPersistedStorageV1(
},
[privateKeyPersistedState],
)
await page.evaluate(() => {
const chrome = (window as any).chrome
chrome?.extension?.getBackgroundPage?.().location.reload()
})
await page.reload()
}

export async function addPersistedStorageV0(page: Page, url: `chrome-extension://${string}/manifest.json`) {
Expand All @@ -57,9 +54,8 @@ export async function addPersistedStorageV0(page: Page, url: `chrome-extension:/
window.localStorage.removeItem('DISMISSED_NEW_EXTENSION_WARNING')
DISMISSED_NEW_EXTENSION_WARNING &&
window.localStorage.setItem('DISMISSED_NEW_EXTENSION_WARNING', DISMISSED_NEW_EXTENSION_WARNING)

chrome.extension.getBackgroundPage().location.reload()
},
[walletExtensionV0PersistedState],
)
await page.reload()
}
19 changes: 11 additions & 8 deletions public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "__MSG_appName__",
"short_name": "__MSG_appName__",
"description": "__MSG_appDescription__",
"manifest_version": 2,
"manifest_version": 3,
"version": "2.1.0",
"default_locale": "en",
"icons": {
Expand All @@ -15,7 +15,7 @@
"128": "./Icon Blue 512.png",
"512": "./Icon Blue 512.png"
},
"browser_action": {
"action": {
"default_icon": {
"16": "./Icon Blue 512.png",
"19": "./Icon Blue 512.png",
Expand All @@ -28,12 +28,15 @@
"default_title": "ROSE Wallet",
"default_popup": "../extension/src/popup.html"
},
"permissions": ["storage", "notifications", "activeTab"],
"content_security_policy": "{{{ EXTENSION_CSP }}}",
"permissions": ["storage", "notifications"],
"content_security_policy": {
"extension_pages": "{{{ EXTENSION_CSP }}}"
},
"background": {
"page": "../extension/src/background.html",
"persistent": true
"service_worker": "service-worker.js"
},
"web_accessible_resources": ["./oasis-xu-frame.html"],
"externally_connectable": { "ids": [] }
"web_accessible_resources": [{
"matches": [],
"resources": ["../extension/src/ExtLedgerAccessPopup/index.html"]
}]
}
1 change: 0 additions & 1 deletion public/oasis-xu-frame.html

This file was deleted.

1 change: 1 addition & 0 deletions public/service-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// Only needed for E2E tests to detect extension ID.
Loading

0 comments on commit 94420f5

Please sign in to comment.