From 0dbe95bf002c9a965360e2ff6b6a7e3df4b1d15b Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Fri, 1 Nov 2024 06:30:38 +0100 Subject: [PATCH 01/20] Migrate to manifest v3 and remove bg page (each popup/tab has own state) --- .changelog/2120.breaking.md | 4 +++ extension/src/background.html | 6 ----- extension/src/background.ts | 6 ----- extension/src/popup/popup.tsx | 30 ++++++++++------------ internals/getSecurityHeaders.js | 7 ++--- internals/scripts/validate-ext-manifest.js | 2 +- playwright/utils/extensionTestExtend.ts | 2 +- public/manifest.json | 13 ++++------ public/oasis-xu-frame.html | 1 - src/utils/webextension.ts | 2 +- 10 files changed, 29 insertions(+), 44 deletions(-) create mode 100644 .changelog/2120.breaking.md delete mode 100644 extension/src/background.html delete mode 100644 extension/src/background.ts delete mode 100644 public/oasis-xu-frame.html diff --git a/.changelog/2120.breaking.md b/.changelog/2120.breaking.md new file mode 100644 index 0000000000..f07b2c7c43 --- /dev/null +++ b/.changelog/2120.breaking.md @@ -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. diff --git a/extension/src/background.html b/extension/src/background.html deleted file mode 100644 index 7838cde936..0000000000 --- a/extension/src/background.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/extension/src/background.ts b/extension/src/background.ts deleted file mode 100644 index 2a14ad0e58..0000000000 --- a/extension/src/background.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { wrapStore } from 'webext-redux' -import { configureAppStore } from 'store/configureStore' - -const store = configureAppStore() - -wrapStore(store) diff --git a/extension/src/popup/popup.tsx b/extension/src/popup/popup.tsx index 4909fa7619..e5e6a5f55b 100644 --- a/extension/src/popup/popup.tsx +++ b/extension/src/popup/popup.tsx @@ -2,7 +2,7 @@ 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' @@ -14,21 +14,17 @@ import { routes } from './routes' const container = document.getElementById('root') as HTMLElement const root = createRoot(container!) -const store = new Store() +const store = configureAppStore() const router = createHashRouter(routes) -store.ready().then(() => { - root.render( - - - - - - - - - , - ) -}) - -console.log('popup') +root.render( + + + + + + + + + , +) diff --git a/internals/getSecurityHeaders.js b/internals/getSecurityHeaders.js index 38c88dda0b..1702ff593d 100644 --- a/internals/getSecurityHeaders.js +++ b/internals/getSecurityHeaders.js @@ -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' diff --git a/internals/scripts/validate-ext-manifest.js b/internals/scripts/validate-ext-manifest.js index 4231949785..61128cf814 100644 --- a/internals/scripts/validate-ext-manifest.js +++ b/internals/scripts/validate-ext-manifest.js @@ -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) } diff --git a/playwright/utils/extensionTestExtend.ts b/playwright/utils/extensionTestExtend.ts index 9b9a4ea06f..ec7da797e7 100644 --- a/playwright/utils/extensionTestExtend.ts +++ b/playwright/utils/extensionTestExtend.ts @@ -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 diff --git a/public/manifest.json b/public/manifest.json index 822d04a898..b7d26e5b8f 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -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": { @@ -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", @@ -28,12 +28,9 @@ "default_title": "ROSE Wallet", "default_popup": "../extension/src/popup.html" }, - "permissions": ["storage", "notifications", "activeTab"], - "content_security_policy": "{{{ EXTENSION_CSP }}}", - "background": { - "page": "../extension/src/background.html", - "persistent": true + "permissions": ["storage", "notifications"], + "content_security_policy": { + "extension_pages": "{{{ EXTENSION_CSP }}}" }, - "web_accessible_resources": ["./oasis-xu-frame.html"], "externally_connectable": { "ids": [] } } diff --git a/public/oasis-xu-frame.html b/public/oasis-xu-frame.html deleted file mode 100644 index 0e76edd65b..0000000000 --- a/public/oasis-xu-frame.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/utils/webextension.ts b/src/utils/webextension.ts index dd260904e3..efb257aa4c 100644 --- a/src/utils/webextension.ts +++ b/src/utils/webextension.ts @@ -8,7 +8,7 @@ type Props = { } const getPopupUrl = (path: string) => - browser.runtime.getURL(`${browser.runtime.getManifest()?.browser_action?.default_popup}${path}`) + browser.runtime.getURL(`${browser.runtime.getManifest()?.action?.default_popup}${path}`) const openPopup = ({ path, height, width, type }: Props) => { const existingPopupWindow = browser.extension From 5bcdbb89d9ba716903d8253368a357165632bc49 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Fri, 1 Nov 2024 06:21:22 +0100 Subject: [PATCH 02/20] Update webextension-polyfill --- package.json | 4 ++-- yarn.lock | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 1674440382..1688172564 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "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", @@ -123,7 +123,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", diff --git a/yarn.lock b/yarn.lock index b766a5279c..78b9492289 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3202,10 +3202,10 @@ resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz#f066abfcd1cbe66267cdbbf0de010d8a41b41597" integrity sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow== -"@types/webextension-polyfill@0.10.7": - version "0.10.7" - resolved "https://registry.yarnpkg.com/@types/webextension-polyfill/-/webextension-polyfill-0.10.7.tgz#de059250599733a60ed26c8a0c81e21e11183b90" - integrity sha512-10ql7A0qzBmFB+F+qAke/nP1PIonS0TXZAOMVOxEUsm+lGSW6uwVcISFNa0I4Oyj0884TZVWGGMIWeXOVSNFHw== +"@types/webextension-polyfill@0.12.1": + version "0.12.1" + resolved "https://registry.yarnpkg.com/@types/webextension-polyfill/-/webextension-polyfill-0.12.1.tgz#8dae244fe094cbb541005362e8e22f16671f6054" + integrity sha512-xPTFWwQ8BxPevPF2IKsf4hpZNss4LxaOLZXypQH4E63BDLmcwX/RMGdI4tB4VO4Nb6xDBH3F/p4gz4wvof1o9w== "@types/yargs-parser@*": version "20.2.0" @@ -9938,10 +9938,10 @@ webext-redux@2.1.9: lodash.assignin "^4.2.0" lodash.clonedeep "^4.5.0" -webextension-polyfill@0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.10.0.tgz#ccb28101c910ba8cf955f7e6a263e662d744dbb8" - integrity sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g== +webextension-polyfill@0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.12.0.tgz#f62c57d2cd42524e9fbdcee494c034cae34a3d69" + integrity sha512-97TBmpoWJEE+3nFBQ4VocyCdLKfw54rFaJ6EVQYLBCXqCIpLSZkwGgASpv4oPt9gdKCJ80RJlcmNzNn008Ag6Q== webidl-conversions@^7.0.0: version "7.0.0" From 9324e62f878c176249b62546c688f19eecea5e35 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Wed, 6 Nov 2024 03:33:08 +0100 Subject: [PATCH 03/20] Remove webext-redux --- package.json | 1 - yarn.lock | 18 ------------------ 2 files changed, 19 deletions(-) diff --git a/package.json b/package.json index 1688172564..efed7760a6 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,6 @@ "tweetnacl": "1.0.3", "typed-redux-saga": "1.5.0", "valid-url": "1.0.9", - "webext-redux": "2.1.9", "webextension-polyfill": "0.12.0" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 78b9492289..a549ca1b21 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7369,16 +7369,6 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash.assignin@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2" - integrity sha1-uo31+4QesKPoBEIysOJjqNxqKKI= - -lodash.clonedeep@^4.5.0: - version "4.5.0" - resolved "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz" - integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= - lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" @@ -9930,14 +9920,6 @@ weak-lru-cache@^1.2.2: resolved "https://registry.yarnpkg.com/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz#fdbb6741f36bae9540d12f480ce8254060dccd19" integrity sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw== -webext-redux@2.1.9: - version "2.1.9" - resolved "https://registry.yarnpkg.com/webext-redux/-/webext-redux-2.1.9.tgz#f7fd01ea4b93191d07bcdd0db5966955766ef634" - integrity sha512-z7frkQ/avFgnMxUH6Q955hU8SDsHT9Zlq9az3OpY891RXw9nKODOTnUhNo9ZAlDFUPHhX2A+z6j74BCaYwEsfQ== - dependencies: - lodash.assignin "^4.2.0" - lodash.clonedeep "^4.5.0" - webextension-polyfill@0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.12.0.tgz#f62c57d2cd42524e9fbdcee494c034cae34a3d69" From dbb0d06d9126ecf8073cdcdf754e6098181aad8d Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Wed, 6 Nov 2024 03:14:10 +0100 Subject: [PATCH 04/20] Retain encryption key between popup reopenings --- src/app/state/persist/encryption.ts | 8 ++++ src/app/state/persist/saga.ts | 57 ++++++++++++++++++++++++++++- src/app/state/persist/selectors.ts | 5 +++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/app/state/persist/encryption.ts b/src/app/state/persist/encryption.ts index e2b7e683c9..a7953bdc92 100644 --- a/src/app/state/persist/encryption.ts +++ b/src/app/state/persist/encryption.ts @@ -47,6 +47,14 @@ export async function decryptWithPassword( ): Promise { const encryptedObj = fromBase64andParse(encryptedString) const derivedKeyWithSalt = await deriveKeyFromPassword(password, encryptedObj.salt) + return await decryptWithKey(derivedKeyWithSalt, encryptedString) +} + +export async function decryptWithKey( + derivedKeyWithSalt: KeyWithSalt, + encryptedString: EncryptedString, +): Promise { + const encryptedObj = fromBase64andParse(encryptedString) const dataBytes = nacl.secretbox.open(encryptedObj.secretbox, encryptedObj.nonce, derivedKeyWithSalt.key) if (!dataBytes) throw new PasswordWrongError() diff --git a/src/app/state/persist/saga.ts b/src/app/state/persist/saga.ts index 7920dc7774..79bed15f34 100644 --- a/src/app/state/persist/saga.ts +++ b/src/app/state/persist/saga.ts @@ -4,18 +4,26 @@ import { isActionSynced } from 'redux-state-sync' import { persistActions, STORAGE_FIELD } from './index' import { base64andStringify, + decryptWithKey, decryptWithPassword, deriveKeyFromPassword, encryptWithKey, fromBase64andParse, } from './encryption' import { RootState } from 'types' -import { EncryptedString, KeyWithSalt, PersistedRootState, SetUnlockedRootStatePayload } from './types' +import { + EncryptedString, + KeyWithSalt, + PersistState, + PersistedRootState, + SetUnlockedRootStatePayload, +} from './types' import { PasswordWrongError } from 'types/errors' import { walletActions } from 'app/state/wallet' import { selectUnlockedStatus } from 'app/state/selectUnlockedStatus' import { runtimeIs } from 'config' import { backupAndDeleteV0ExtProfile, readStorageV0 } from '../../../utils/walletExtensionV0' +import { selectStringifiedEncryptionKey } from './selectors' function* watchPersistAsync() { yield* fork(function* () { @@ -211,8 +219,55 @@ function* encryptAndPersistState(action: AnyAction) { window.localStorage.setItem(STORAGE_FIELD, encryptedState) } +function* retainEncryptionKeyBetweenPopupReopenings() { + if (runtimeIs !== 'extension') return + yield* fork(function* () { + const channelQueue = yield* actionChannel('*') + let previousStringifiedEncryptionKey: PersistState['stringifiedEncryptionKey'] = undefined + while (true) { + yield* take(channelQueue) + const stringifiedEncryptionKey = yield* select(selectStringifiedEncryptionKey) + if (stringifiedEncryptionKey !== previousStringifiedEncryptionKey) { + previousStringifiedEncryptionKey = stringifiedEncryptionKey + yield* call(writeSharedExtMemory, stringifiedEncryptionKey) + } + } + }) + + yield* fork(function* () { + const encryptedState = window.localStorage.getItem( + STORAGE_FIELD, + ) as EncryptedString | null + if (!encryptedState) return // Ignore + try { + const stringifiedEncryptionKey = yield* call(readSharedExtMemory) + if (!stringifiedEncryptionKey) return // Ignore + if (stringifiedEncryptionKey === 'skipped') return // Ignore + const keyWithSalt: KeyWithSalt = fromBase64andParse(stringifiedEncryptionKey) + const persistedRootState = yield* call(decryptWithKey, keyWithSalt, encryptedState) + yield* put(persistActions.setUnlockedRootState({ persistedRootState, stringifiedEncryptionKey })) + } catch (error) { + // Ignore + } + }) +} + +async function writeSharedExtMemory(stringifiedEncryptionKey: PersistState['stringifiedEncryptionKey']) { + if (runtimeIs !== 'extension') return + const browser = await import('webextension-polyfill') + await browser.storage.session.set({ stringifiedEncryptionKey }) +} + +async function readSharedExtMemory() { + if (runtimeIs !== 'extension') return + const browser = await import('webextension-polyfill') + const storage = await browser.storage.session.get('stringifiedEncryptionKey') + return storage.stringifiedEncryptionKey as PersistState['stringifiedEncryptionKey'] +} + export function* persistSaga() { yield* watchPersistAsync() + yield* retainEncryptionKeyBetweenPopupReopenings() const storageV0 = yield* call(readStorageV0) yield* put(persistActions.setHasV0StorageToMigrate(!!storageV0?.chromeStorageLocal.keyringData)) } diff --git a/src/app/state/persist/selectors.ts b/src/app/state/persist/selectors.ts index c112c070f7..dcd6f3b435 100644 --- a/src/app/state/persist/selectors.ts +++ b/src/app/state/persist/selectors.ts @@ -20,3 +20,8 @@ export const selectIsPersistenceUnsupported = createSelector( [selectSlice], state => state.isPersistenceUnsupported, ) + +export const selectStringifiedEncryptionKey = createSelector( + [selectSlice], + state => state.stringifiedEncryptionKey, +) From 9f88d0b5f0a6918639a83ac7b34e5e781b09e181 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Fri, 1 Nov 2024 06:34:00 +0100 Subject: [PATCH 05/20] Force extension users to create a profile --- playwright/tests/extension.spec.ts | 6 +++--- playwright/utils/fillPrivateKey.ts | 16 +++++++++++++--- .../components/Persist/ChoosePasswordFields.tsx | 10 ++++++++++ 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/playwright/tests/extension.spec.ts b/playwright/tests/extension.spec.ts index cfbb81d9b7..fd213cbcd7 100644 --- a/playwright/tests/extension.spec.ts +++ b/playwright/tests/extension.spec.ts @@ -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 }) => { @@ -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() diff --git a/playwright/utils/fillPrivateKey.ts b/playwright/utils/fillPrivateKey.ts index 16e1ac3ada..d226f67d82 100644 --- a/playwright/utils/fillPrivateKey.ts +++ b/playwright/utils/fillPrivateKey.ts @@ -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) diff --git a/src/app/components/Persist/ChoosePasswordFields.tsx b/src/app/components/Persist/ChoosePasswordFields.tsx index 0d7eaae848..cbee4a0d27 100644 --- a/src/app/components/Persist/ChoosePasswordFields.tsx +++ b/src/app/components/Persist/ChoosePasswordFields.tsx @@ -9,6 +9,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { ChoosePasswordInputFields } from './ChoosePasswordInputFields' +import { runtimeIs } from 'config' export function ChoosePasswordFields() { const { t } = useTranslation() @@ -17,6 +18,8 @@ export function ChoosePasswordFields() { const hasUnpersistedAccounts = unlockedStatus === 'openUnpersisted' const [startPersisting, setStartPersisting] = useState(!hasUnpersistedAccounts) + const isExtension = runtimeIs === 'extension' + const isChoiceDisabled = isPersistenceUnsupported || unlockedStatus === 'unlockedProfile' || @@ -41,6 +44,13 @@ export function ChoosePasswordFields() { disabled: true, checked: unlockedStatus === 'unlockedProfile', } + : isExtension + ? { + disabled: true, + // Force creating a profile in Manifest v3 extension because we can't keep state in memory. + // User would lose all progress every time popup closes. + checked: true, + } : { checked: startPersisting, })} From 10a592cb55316f51e7cf69bf72ec4b843609f419 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Sat, 25 Jan 2025 14:59:47 +0700 Subject: [PATCH 06/20] Refactor ExtLedgerAccessPopup as html entry without redux --- extension/src/ExtLedgerAccessPopup/index.html | 16 ++++++++++ extension/src/ExtLedgerAccessPopup/index.tsx | 29 +++++++++++++++++++ extension/src/popup/routes.tsx | 5 ---- public/manifest.json | 6 +++- .../OpenWalletPage/FromLedgerWebExtension.tsx | 6 ++-- src/index.tsx | 4 +-- src/styles/theme/ThemeProvider.tsx | 13 ++++++++- src/utils/webextension.ts | 11 ++----- 8 files changed, 71 insertions(+), 19 deletions(-) create mode 100644 extension/src/ExtLedgerAccessPopup/index.html create mode 100644 extension/src/ExtLedgerAccessPopup/index.tsx diff --git a/extension/src/ExtLedgerAccessPopup/index.html b/extension/src/ExtLedgerAccessPopup/index.html new file mode 100644 index 0000000000..a584a4749f --- /dev/null +++ b/extension/src/ExtLedgerAccessPopup/index.html @@ -0,0 +1,16 @@ + + + + + + + + + ROSE Wallet + + + +
+ + + diff --git a/extension/src/ExtLedgerAccessPopup/index.tsx b/extension/src/ExtLedgerAccessPopup/index.tsx new file mode 100644 index 0000000000..1f9de0010b --- /dev/null +++ b/extension/src/ExtLedgerAccessPopup/index.tsx @@ -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. + + + + + , +) diff --git a/extension/src/popup/routes.tsx b/extension/src/popup/routes.tsx index 57dd628e1e..1e6f78437d 100644 --- a/extension/src/popup/routes.tsx +++ b/extension/src/popup/routes.tsx @@ -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' @@ -22,8 +21,4 @@ export const routes: RouteObject[] = [ }, ], }, - { - path: 'open-wallet/connect-device', - element: , - }, ] diff --git a/public/manifest.json b/public/manifest.json index b7d26e5b8f..9bf0ed0201 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -32,5 +32,9 @@ "content_security_policy": { "extension_pages": "{{{ EXTENSION_CSP }}}" }, - "externally_connectable": { "ids": [] } + "externally_connectable": { "ids": [] }, + "web_accessible_resources": [{ + "matches": [], + "resources": ["../extension/src/ExtLedgerAccessPopup/index.html"] + }] } diff --git a/src/app/pages/OpenWalletPage/FromLedgerWebExtension.tsx b/src/app/pages/OpenWalletPage/FromLedgerWebExtension.tsx index 6bc79136e8..476c733c6c 100644 --- a/src/app/pages/OpenWalletPage/FromLedgerWebExtension.tsx +++ b/src/app/pages/OpenWalletPage/FromLedgerWebExtension.tsx @@ -1,10 +1,12 @@ import React from 'react' -import { useHref } from 'react-router-dom' import { openLedgerAccessPopup } from 'utils/webextension' import { FromLedger } from './Features/FromLedger' +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import type { ExtLedgerAccessPopup } from '../../../../extension/src/ExtLedgerAccessPopup/ExtLedgerAccessPopup' export function FromLedgerWebExtension() { - const href = useHref('/open-wallet/connect-device') + /** See {@link ExtLedgerAccessPopup} */ + const href = new URL('../../../../extension/src/ExtLedgerAccessPopup/index.html', import.meta.url).href return ( { +export const ThemeProvider = (props: { children: React.ReactNode }) => { const theme = deepMerge(grommet, grommetCustomTheme) const mode = useSelector(selectTheme) @@ -392,3 +393,13 @@ export const ThemeProvider = (props: { children: React.ReactChild }) => { ) } +export const ThemeProviderWithoutRedux = (props: { children: React.ReactNode }) => { + const theme = deepMerge(grommet, grommetCustomTheme) + const mode = getInitialThemeState().selected + + return ( + + {React.Children.only(props.children)} + + ) +} diff --git a/src/utils/webextension.ts b/src/utils/webextension.ts index efb257aa4c..4cbe28ec6c 100644 --- a/src/utils/webextension.ts +++ b/src/utils/webextension.ts @@ -7,19 +7,14 @@ type Props = { type?: browser.Windows.CreateType } -const getPopupUrl = (path: string) => - browser.runtime.getURL(`${browser.runtime.getManifest()?.action?.default_popup}${path}`) - -const openPopup = ({ path, height, width, type }: Props) => { - const existingPopupWindow = browser.extension - .getViews() - .find(window => window.location.href === getPopupUrl(path)) +const openPopup = async ({ path, height, width, type }: Props) => { + const existingPopupWindow = browser.extension.getViews().find(window => window.location.href === path) if (existingPopupWindow) { existingPopupWindow.close() } browser.windows.create({ - url: getPopupUrl(path), + url: path, type: type ?? 'popup', width: width, height: height, From a534682b03624555afd0187b0ee8540f36b475cc Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Sat, 25 Jan 2025 15:00:55 +0700 Subject: [PATCH 07/20] Allow only one extension popup, so no need to sync state --- extension/src/popup/popup.tsx | 48 +++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/extension/src/popup/popup.tsx b/extension/src/popup/popup.tsx index e5e6a5f55b..084a5753e3 100644 --- a/extension/src/popup/popup.tsx +++ b/extension/src/popup/popup.tsx @@ -6,25 +6,41 @@ 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 = configureAppStore() -const router = createHashRouter(routes) - -root.render( - - - - - - - - - , -) +// Only allow one popup with redux, so no need to sync state. +if (browser.extension.getViews({ type: 'tab' }).length > 0) { + // Either this is a tab, or something else is. + // If this is a tab, just close. + // 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( + + + + + + + + + , + ) +} From 997f1f6818cc031464af24f5be1bcbca6245a839 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Sun, 26 Jan 2025 17:07:43 +0700 Subject: [PATCH 08/20] Fix E2E tests: allow one tab (playwright can't open default popup) --- extension/src/popup/popup.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/extension/src/popup/popup.tsx b/extension/src/popup/popup.tsx index 084a5753e3..9faa293fa5 100644 --- a/extension/src/popup/popup.tsx +++ b/extension/src/popup/popup.tsx @@ -13,10 +13,8 @@ import 'sanitize.css/sanitize.css' import 'styles/main.css' import { routes } from './routes' -// Only allow one popup with redux, so no need to sync state. -if (browser.extension.getViews({ type: 'tab' }).length > 0) { - // Either this is a tab, or something else is. - // If this is a tab, just close. +// 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 From 29fa1f11d050f6268758f4d85d23d72848d376a3 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Wed, 6 Nov 2024 04:32:51 +0100 Subject: [PATCH 09/20] Focus ledger access popup twice --- src/utils/webextension.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utils/webextension.ts b/src/utils/webextension.ts index 4cbe28ec6c..334c49ff85 100644 --- a/src/utils/webextension.ts +++ b/src/utils/webextension.ts @@ -13,12 +13,14 @@ const openPopup = async ({ path, height, width, type }: Props) => { if (existingPopupWindow) { existingPopupWindow.close() } - browser.windows.create({ + const popup = await browser.windows.create({ url: path, type: type ?? 'popup', width: width, height: height, + focused: true, }) + await browser.windows.update(popup.id!, { focused: true }) // Focus again. Helps in rare cases like when screensharing. } export const openLedgerAccessPopup = (path: string) => { From affa4c9b691e71e181064625ebdf3414fc74af46 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Wed, 6 Nov 2024 06:42:21 +0100 Subject: [PATCH 10/20] Simplify openLedgerAccessPopup --- .../pages/OpenWalletPage/FromLedgerWebExtension.tsx | 13 +------------ src/utils/webextension.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/app/pages/OpenWalletPage/FromLedgerWebExtension.tsx b/src/app/pages/OpenWalletPage/FromLedgerWebExtension.tsx index 476c733c6c..7e5541f0fe 100644 --- a/src/app/pages/OpenWalletPage/FromLedgerWebExtension.tsx +++ b/src/app/pages/OpenWalletPage/FromLedgerWebExtension.tsx @@ -1,18 +1,7 @@ import React from 'react' import { openLedgerAccessPopup } from 'utils/webextension' import { FromLedger } from './Features/FromLedger' -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import type { ExtLedgerAccessPopup } from '../../../../extension/src/ExtLedgerAccessPopup/ExtLedgerAccessPopup' export function FromLedgerWebExtension() { - /** See {@link ExtLedgerAccessPopup} */ - const href = new URL('../../../../extension/src/ExtLedgerAccessPopup/index.html', import.meta.url).href - - return ( - { - openLedgerAccessPopup(href) - }} - /> - ) + return } diff --git a/src/utils/webextension.ts b/src/utils/webextension.ts index 334c49ff85..1782448ebc 100644 --- a/src/utils/webextension.ts +++ b/src/utils/webextension.ts @@ -1,4 +1,6 @@ import browser from 'webextension-polyfill' +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { ExtLedgerAccessPopup } from '../../extension/src/ExtLedgerAccessPopup/ExtLedgerAccessPopup' type Props = { path: string @@ -23,9 +25,11 @@ const openPopup = async ({ path, height, width, type }: Props) => { await browser.windows.update(popup.id!, { focused: true }) // Focus again. Helps in rare cases like when screensharing. } -export const openLedgerAccessPopup = (path: string) => { +export const openLedgerAccessPopup = () => { + /** See {@link ExtLedgerAccessPopup} */ + const href = new URL('../../extension/src/ExtLedgerAccessPopup/index.html', import.meta.url).href openPopup({ - path: path, + path: href, width: 600, height: 850, From 4433493cb0ed33f4be060e80175a8ebbce3d3ada Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Sun, 26 Jan 2025 17:08:01 +0700 Subject: [PATCH 11/20] Fix E2E tests: add empty service worker --- playwright/utils/extensionTestExtend.ts | 8 ++++---- public/manifest.json | 3 +++ public/service-worker.js | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 public/service-worker.js diff --git a/playwright/utils/extensionTestExtend.ts b/playwright/utils/extensionTestExtend.ts index ec7da797e7..4c0123fb46 100644 --- a/playwright/utils/extensionTestExtend.ts +++ b/playwright/utils/extensionTestExtend.ts @@ -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) diff --git a/public/manifest.json b/public/manifest.json index 9bf0ed0201..b848e661c3 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -32,6 +32,9 @@ "content_security_policy": { "extension_pages": "{{{ EXTENSION_CSP }}}" }, + "background": { + "service_worker": "service-worker.js" + }, "externally_connectable": { "ids": [] }, "web_accessible_resources": [{ "matches": [], diff --git a/public/service-worker.js b/public/service-worker.js new file mode 100644 index 0000000000..303ce4a04b --- /dev/null +++ b/public/service-worker.js @@ -0,0 +1 @@ +// Only needed for E2E tests to detect extension ID. From b89d0845ed4b35e99c630242e1deb1250b079ffd Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Sun, 26 Jan 2025 03:09:44 +0700 Subject: [PATCH 12/20] Fix E2E tests: removed background page --- playwright/utils/storage.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/playwright/utils/storage.ts b/playwright/utils/storage.ts index 3393d01985..88dcd3e892 100644 --- a/playwright/utils/storage.ts +++ b/playwright/utils/storage.ts @@ -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( @@ -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`) { @@ -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() } From 21654cd7687a4fa830647b3e5751aa76d4f3af78 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Sun, 26 Jan 2025 17:10:12 +0700 Subject: [PATCH 13/20] Fix E2E tests: can't open two tabs --- .../tests/migrating-v0-ext-persisted-state.spec.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/playwright/tests/migrating-v0-ext-persisted-state.spec.ts b/playwright/tests/migrating-v0-ext-persisted-state.spec.ts index 75090d6d4a..42f888ac3a 100644 --- a/playwright/tests/migrating-v0-ext-persisted-state.spec.ts +++ b/playwright/tests/migrating-v0-ext-persisted-state.spec.ts @@ -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') }) @@ -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 }) From a36179715e2999eea55bab895b528a1e0dc42ed3 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Sun, 26 Jan 2025 17:10:44 +0700 Subject: [PATCH 14/20] Update E2E test fixtures for extension (MV3 fixed mockApi) --- src/utils/__fixtures__/test-inputs.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/utils/__fixtures__/test-inputs.ts b/src/utils/__fixtures__/test-inputs.ts index 65c24abba8..d28f5df012 100644 --- a/src/utils/__fixtures__/test-inputs.ts +++ b/src/utils/__fixtures__/test-inputs.ts @@ -290,6 +290,7 @@ export const walletExtensionV0UnlockedState = { ethPrivateKeyRaw: '', feeAmount: '', feeGas: '', + paraTime: undefined, recipient: '', type: undefined, }, @@ -317,7 +318,7 @@ export const walletExtensionV0UnlockedState = { debonding: '0', delegations: '0', total: '0', - nonce: '0', + nonce: '1', }, name: 'ledger5', path: [44, 474, 0, 0, 5], @@ -334,7 +335,7 @@ export const walletExtensionV0UnlockedState = { debonding: '0', delegations: '0', total: '0', - nonce: '0', + nonce: '1', }, name: 'ledger1', path: [44, 474, 0, 0, 0], @@ -351,7 +352,7 @@ export const walletExtensionV0UnlockedState = { debonding: '0', delegations: '0', total: '0', - nonce: '0', + nonce: '1', }, name: 'Account 1', path: [44, 474, 0], @@ -370,7 +371,7 @@ export const walletExtensionV0UnlockedState = { debonding: '0', delegations: '0', total: '0', - nonce: '0', + nonce: '1', }, name: 'Account 2', path: [44, 474, 1], @@ -389,7 +390,7 @@ export const walletExtensionV0UnlockedState = { debonding: '0', delegations: '0', total: '0', - nonce: '0', + nonce: '1', }, name: 'short privatekey', privateKey: @@ -406,7 +407,7 @@ export const walletExtensionV0UnlockedState = { debonding: '0', delegations: '0', total: '0', - nonce: '0', + nonce: '1', }, name: 'ledger5-1', path: [44, 474, 0, 0, 6], @@ -423,7 +424,7 @@ export const walletExtensionV0UnlockedState = { debonding: '0', delegations: '0', total: '0', - nonce: '0', + nonce: '1', }, name: 'private key1', privateKey: From 50aa411e178192843fb25698db709b440164716d Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Sun, 26 Jan 2025 03:30:00 +0700 Subject: [PATCH 15/20] Fix E2E tests: without persistent bg page fatal error doesn't get stuck --- playwright/tests/extension.spec.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/playwright/tests/extension.spec.ts b/playwright/tests/extension.spec.ts index fd213cbcd7..a10a53a534 100644 --- a/playwright/tests/extension.spec.ts +++ b/playwright/tests/extension.spec.ts @@ -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() From b013c5dd27c2bfab789414571ba65191efbeea0c Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Sun, 26 Jan 2025 04:09:26 +0700 Subject: [PATCH 16/20] Remove openLedgerAccessPopup test --- src/utils/__tests__/webextension.test.ts | 37 ------------------------ 1 file changed, 37 deletions(-) delete mode 100644 src/utils/__tests__/webextension.test.ts diff --git a/src/utils/__tests__/webextension.test.ts b/src/utils/__tests__/webextension.test.ts deleted file mode 100644 index e51e66b30c..0000000000 --- a/src/utils/__tests__/webextension.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import browser from 'webextension-polyfill' -import { openLedgerAccessPopup } from '../webextension' - -jest.mock('webextension-polyfill', () => ({ - extension: { - getViews: jest.fn(), - }, - runtime: { - getManifest: jest.fn(), - getURL: jest.fn(), - }, - windows: { - create: jest.fn(), - }, -})) - -describe('openLedgerAccessPopup', () => { - it('should open a new popup window', () => { - jest.mocked(browser.extension.getViews).mockReturnValue([]) - jest.mocked(browser.runtime.getManifest).mockReturnValue({ - browser_action: { default_popup: 'popup.foo.html' }, - manifest_version: 2, - name: '', - version: '', - }) - jest.mocked(browser.runtime.getURL).mockReturnValue('mockedUrl') - openLedgerAccessPopup('#/foo') - - expect(browser.runtime.getURL).toHaveBeenCalledWith('popup.foo.html#/foo') - expect(browser.windows.create).toHaveBeenCalledWith({ - height: 850, - type: 'normal', - url: 'mockedUrl', - width: 600, - }) - }) -}) From be241cf286e89a405cd6a1f5128fb9ea92c155fd Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Sun, 26 Jan 2025 02:51:25 +0700 Subject: [PATCH 17/20] Fix a type in E2E tests --- playwright/utils/mockApi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright/utils/mockApi.ts b/playwright/utils/mockApi.ts index ef3306242f..de7bab14d4 100644 --- a/playwright/utils/mockApi.ts +++ b/playwright/utils/mockApi.ts @@ -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({ From 65b21ecfec47d24adcca3a05249912d2535107a1 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Thu, 30 Jan 2025 23:03:43 +0700 Subject: [PATCH 18/20] Fix lock profile in extension --- src/app/state/persist/saga.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/app/state/persist/saga.ts b/src/app/state/persist/saga.ts index 79bed15f34..57103359dd 100644 --- a/src/app/state/persist/saga.ts +++ b/src/app/state/persist/saga.ts @@ -255,7 +255,12 @@ function* retainEncryptionKeyBetweenPopupReopenings() { async function writeSharedExtMemory(stringifiedEncryptionKey: PersistState['stringifiedEncryptionKey']) { if (runtimeIs !== 'extension') return const browser = await import('webextension-polyfill') - await browser.storage.session.set({ stringifiedEncryptionKey }) + // Chrome ignores undefined values https://github.com/w3c/webextensions/issues/263 + if (stringifiedEncryptionKey) { + await browser.storage.session.set({ stringifiedEncryptionKey }) + } else { + await browser.storage.session.remove('stringifiedEncryptionKey') + } } async function readSharedExtMemory() { From 65616b74f86bb3f87b1bcb8a071f274482fd152f Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Thu, 30 Jan 2025 23:04:01 +0700 Subject: [PATCH 19/20] Remove explicit empty externally_connectable to avoid warnings --- public/manifest.json | 1 - 1 file changed, 1 deletion(-) diff --git a/public/manifest.json b/public/manifest.json index b848e661c3..cff1d3a0f0 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -35,7 +35,6 @@ "background": { "service_worker": "service-worker.js" }, - "externally_connectable": { "ids": [] }, "web_accessible_resources": [{ "matches": [], "resources": ["../extension/src/ExtLedgerAccessPopup/index.html"] From 330eb29b28a0d882e0b0e698c12c135d60d1c148 Mon Sep 17 00:00:00 2001 From: lukaw3d Date: Fri, 31 Jan 2025 00:49:42 +0700 Subject: [PATCH 20/20] Fix possible race condition in read/write of retained encryption key --- src/app/state/persist/saga.ts | 41 ++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/app/state/persist/saga.ts b/src/app/state/persist/saga.ts index 57103359dd..75f6d4ec2e 100644 --- a/src/app/state/persist/saga.ts +++ b/src/app/state/persist/saga.ts @@ -220,8 +220,30 @@ function* encryptAndPersistState(action: AnyAction) { } function* retainEncryptionKeyBetweenPopupReopenings() { - if (runtimeIs !== 'extension') return yield* fork(function* () { + if (runtimeIs !== 'extension') return + + // Read before rewriting. + const encryptedState = window.localStorage.getItem( + STORAGE_FIELD, + ) as EncryptedString | null + if (encryptedState) { + try { + const stringifiedEncryptionKey = yield* call(readSharedExtMemory) + if (!stringifiedEncryptionKey) throw new Error('skip') + if (stringifiedEncryptionKey === 'skipped') throw new Error('skip') + const keyWithSalt: KeyWithSalt = fromBase64andParse(stringifiedEncryptionKey) + const persistedRootState = yield* call( + decryptWithKey, + keyWithSalt, + encryptedState, + ) + yield* put(persistActions.setUnlockedRootState({ persistedRootState, stringifiedEncryptionKey })) + } catch (error) { + // Ignore + } + } + const channelQueue = yield* actionChannel('*') let previousStringifiedEncryptionKey: PersistState['stringifiedEncryptionKey'] = undefined while (true) { @@ -233,23 +255,6 @@ function* retainEncryptionKeyBetweenPopupReopenings() { } } }) - - yield* fork(function* () { - const encryptedState = window.localStorage.getItem( - STORAGE_FIELD, - ) as EncryptedString | null - if (!encryptedState) return // Ignore - try { - const stringifiedEncryptionKey = yield* call(readSharedExtMemory) - if (!stringifiedEncryptionKey) return // Ignore - if (stringifiedEncryptionKey === 'skipped') return // Ignore - const keyWithSalt: KeyWithSalt = fromBase64andParse(stringifiedEncryptionKey) - const persistedRootState = yield* call(decryptWithKey, keyWithSalt, encryptedState) - yield* put(persistActions.setUnlockedRootState({ persistedRootState, stringifiedEncryptionKey })) - } catch (error) { - // Ignore - } - }) } async function writeSharedExtMemory(stringifiedEncryptionKey: PersistState['stringifiedEncryptionKey']) {