diff --git a/package.json b/package.json index aff93cdcc..6f57f9f53 100644 --- a/package.json +++ b/package.json @@ -9,15 +9,19 @@ "lint:fix": "prettier packages/**/*.{ts,tsx,css,md,json} --write", "prepare": "husky install", "start": "env-cmd --silent turbo run start --filter=@200ms/appstore-web --filter=@200ms/extension --filter=@200ms/example-client --filter=@200ms/example-plugin-app --filter=@200ms/example-plugin-table-mango --filter=@200ms/example-plugin-table-cmc --filter=@200ms/example-plugin-table-degods --filter=@200ms/ledger-injection --parallel", + "start:fresh": "yarn install && yarn clean && yarn install && yarn build && yarn start", "test": "env-cmd --silent turbo run test -- --passWithNoTests --watchAll=false", "build": "env-cmd --silent turbo run build", "e2e": "env-cmd --silent turbo run e2e", - "clean": "npx rimraf .turbo node_modules/ packages/**/node_modules packages/**/.turbo packages/**/build packages/**/dist packages/extension/dev" + "clean": "npx rimraf .turbo node_modules/ packages/**/node_modules packages/**/.turbo packages/**/build packages/**/dist packages/extension/dev", + "postinstall": "patch-package" }, "devDependencies": { "env-cmd": "^10.1.0", "husky": "^8.0.1", "lint-staged": "^12.4.1", + "patch-package": "^6.4.7", + "postinstall-postinstall": "^2.1.0", "prettier": "^2.6.2", "turbo": "^1.2.9" }, @@ -27,6 +31,7 @@ "resolutions": { "react-dom": "^17.0.0", "react": "^17.0.0", + "@solana/web3.js": "1.41.6", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0" } diff --git a/packages/example-plugin-app/package.json b/packages/example-plugin-app/package.json index 7e0825f74..764c924e3 100644 --- a/packages/example-plugin-app/package.json +++ b/packages/example-plugin-app/package.json @@ -16,7 +16,7 @@ "react": "^17.0.2" }, "devDependencies": { - "parcel": "^2.4.1", + "parcel": "^2.5.0", "rimraf": "^3.0.2", "typescript": "^4.6.3" }, diff --git a/packages/example-plugin-table-cmc/package.json b/packages/example-plugin-table-cmc/package.json index 737776d4d..ac3b6ea28 100644 --- a/packages/example-plugin-table-cmc/package.json +++ b/packages/example-plugin-table-cmc/package.json @@ -17,7 +17,7 @@ "react": "^17.0.2" }, "devDependencies": { - "parcel": "^2.4.1", + "parcel": "^2.5.0", "rimraf": "^3.0.2", "typescript": "^4.6.3" }, diff --git a/packages/example-plugin-table-degods/package.json b/packages/example-plugin-table-degods/package.json index 878b0cb2d..497c91698 100644 --- a/packages/example-plugin-table-degods/package.json +++ b/packages/example-plugin-table-degods/package.json @@ -17,7 +17,7 @@ "react": "^17.0.2" }, "devDependencies": { - "parcel": "^2.4.1", + "parcel": "^2.5.0", "rimraf": "^3.0.2", "typescript": "^4.6.3" }, diff --git a/packages/example-plugin-table-mango/package.json b/packages/example-plugin-table-mango/package.json index c8b284be0..749f85f1a 100644 --- a/packages/example-plugin-table-mango/package.json +++ b/packages/example-plugin-table-mango/package.json @@ -17,7 +17,7 @@ "react": "^17.0.2" }, "devDependencies": { - "parcel": "^2.4.1", + "parcel": "^2.5.0", "rimraf": "^3.0.2", "typescript": "^4.6.3" }, diff --git a/packages/extension/integration_tests/installation.test.ts b/packages/extension/integration_tests/installation.test.ts index 343bbe1e7..49c0397fe 100644 --- a/packages/extension/integration_tests/installation.test.ts +++ b/packages/extension/integration_tests/installation.test.ts @@ -2,31 +2,37 @@ import { generateMnemonic } from "bip39"; import type { Page } from "puppeteer"; import manifest from "../public/manifest.json"; +let clientPage: Page; let extensionPopupPage: Page; let setupPage: Page; describe("Installing Anchor Wallet", () => { // Our test browser has already installed the extension code in ./build // see jest-puppeteer.config.js for details about that. - // Now we need to get the unique ID of the browser extension and - // then we can open a URL like chrome-extension://EXTENSION_ID/popup.html beforeAll((done) => { (async () => { + clientPage = await browser.newPage(); + + // We need to load a webpage here for some reason, maybe for code injection? + await clientPage.goto("http://localhost:3333"); + + // Now we need to get the unique ID of the browser extension and + // then we can open a URL like chrome-extension://EXTENSION_ID/popup.html + const extensionID = await (async () => { - const targets: any = await browser.targets(); + const targets = await browser.targets(); const extensionTarget = targets.find( - ({ _targetInfo: { title, type } }) => - title === manifest.name && type === "background_page" + (target) => target.type() === "service_worker" ); - const extensionUrl = extensionTarget._targetInfo.url; - return extensionUrl.split("/")[2]; + // @ts-ignore + const partialExtensionUrl = extensionTarget._targetInfo.url; + const [, , id] = partialExtensionUrl.split("/"); + return id; })(); - extensionPopupPage = await browser.newPage(); - - const popupFile = manifest.browser_action.default_popup; + const popupFile = manifest.action.default_popup; const popupURL = `chrome-extension://${extensionID}/${popupFile}`; - + extensionPopupPage = await browser.newPage(); await extensionPopupPage.goto(popupURL); setupPage = await ( @@ -35,8 +41,6 @@ describe("Installing Anchor Wallet", () => { ) ).page(); - // using callback for now, because of issues with flaky tests - // see: https://github.com/200ms-labs/anchor-wallet/issues/57 done(); })(); }); @@ -157,6 +161,37 @@ describe("Installing Anchor Wallet", () => { // Ensure the wallet is unlocked and the balance page loads await expect(extensionPopupPage).toMatch("Total Balance"); - }, 120_000 /** allow 2 mins for test to run due to loading external data */); + + await extensionPopupPage.close(); + + await clientPage.bringToFront(); + + await expect(clientPage).toClick("button", { + text: "Select Wallet", + }); + + await expect(clientPage).toClick("button", { + text: "Anchor", + }); + + // XXX: this is a hack to wait for the popup to open + await sleep(500); + + const browserPages = await browser.pages(); + const approvePopup = browserPages[browserPages.length - 1]; + + await expect(approvePopup).toMatch("Connect Wallet to localhost"); + await expect(approvePopup).toClick("button", { text: "Approve" }); + + // Wallet is now connected + await expect(clientPage).toClick("button", { + text: "Disconnect", + }); + + // Wallet is now disconnected, expect to see 'Select Wallet' button + await expect(clientPage).toMatch("Select Wallet"); + }, 30_000 /** allow 30s for test to run due to loading external data */); }); }); + +const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms)); diff --git a/packages/extension/jest-puppeteer.config.js b/packages/extension/jest-puppeteer.config.js index b61703607..5e5569571 100644 --- a/packages/extension/jest-puppeteer.config.js +++ b/packages/extension/jest-puppeteer.config.js @@ -12,6 +12,9 @@ const executablePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; module.exports = { + server: { + command: "serve -p 3333 ../example-client/build", + }, launch: { headless: false, executablePath, diff --git a/packages/extension/package.json b/packages/extension/package.json index 45f750eb1..e4c5fcc5e 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -70,6 +70,7 @@ "@types/react-custom-scrollbars": "^4.0.10", "@types/react-dom": "^17.0.0", "@types/react-sidebar": "^3.0.2", + "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.22.0", "@typescript-eslint/parser": "^5.22.0", "crypto-browserify": "^3.12.0", @@ -81,6 +82,7 @@ "jest": "^28.1.0", "jest-puppeteer": "^6.1.0", "puppeteer-core": "^14.1.1", + "serve": "^13.0.2", "typescript": "^4.6.3" } } diff --git a/packages/extension/public/manifest.json b/packages/extension/public/manifest.json index 3a9f94595..ae650db15 100644 --- a/packages/extension/public/manifest.json +++ b/packages/extension/public/manifest.json @@ -2,15 +2,19 @@ "name": "Anchor", "version": "0.1.0", "description": "Anchor Wallet", - "manifest_version": 2, - "browser_action": { + "manifest_version": 3, + "action": { "default_popup": "popup.html", "default_title": "Anchor" }, - "web_accessible_resources": ["injected.js"], + "web_accessible_resources": [ + { + "resources": ["injected.js"], + "matches": [""] + } + ], "background": { - "persistent": true, - "scripts": ["background.bundle.js"] + "service_worker": "background.bundle.js" }, "content_scripts": [ { @@ -25,6 +29,5 @@ "192": "anchor.png", "512": "anchor.png" }, - "permissions": ["storage"], - "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'" + "permissions": ["storage"] } diff --git a/packages/extension/src/background/popup.ts b/packages/extension/src/background/popup.ts index a1e865703..5c05396ab 100644 --- a/packages/extension/src/background/popup.ts +++ b/packages/extension/src/background/popup.ts @@ -84,12 +84,12 @@ async function openPopupWindow(ctx: Context, url: string): Promise { export function openOnboarding() { const url = `${EXPANDED_HTML}?${QUERY_ONBOARDING}`; - window.open(chrome.extension.getURL(url), "_blank"); + window.open(chrome.runtime.getURL(url), "_blank"); } export function openConnectHardware() { const url = `${EXPANDED_HTML}?${QUERY_CONNECT_HARDWARE}`; - window.open(chrome.extension.getURL(url), "_blank"); + window.open(chrome.runtime.getURL(url), "_blank"); } export function isExtensionPopup() { diff --git a/packages/extension/src/components/LedgerIframe.tsx b/packages/extension/src/components/LedgerIframe.tsx new file mode 100644 index 000000000..640e3897e --- /dev/null +++ b/packages/extension/src/components/LedgerIframe.tsx @@ -0,0 +1,51 @@ +import { + LEDGER_IFRAME_URL, + LEDGER_INJECTED_CHANNEL_RESPONSE, +} from "@200ms/common"; +import { useEffect, useRef } from "react"; + +/** + * A hidden iframe that's used to communicate (as a proxy) with a Ledger + */ +const LedgerIframe = () => { + const iframe = useRef(null); + useEffect(() => { + let handleMessage: (event: MessageEvent) => void; + + navigator.serviceWorker.ready.then((_registration) => { + handleMessage = ({ data }) => { + if (data.type !== LEDGER_INJECTED_CHANNEL_RESPONSE) { + return; + } + navigator.serviceWorker.controller?.postMessage(data); + }; + window.addEventListener("message", handleMessage); + + navigator.serviceWorker.onmessage = ({ data }) => { + // Forward the message to be sent from the iframe so that it has + // permissions to communicate with the ledger + iframe.current!.contentWindow!.postMessage(data, "*"); + }; + }); + + return () => { + // TODO: check if this cleanup is adequate + navigator.serviceWorker.onmessage = null; + window.removeEventListener("message", handleMessage); + }; + }, []); + + // allow="hid 'src'" is why this component is necessary, because it allows + // us to communicate with a ledger using the Human Interface Device API + return ( +