From 9c4f6cbbbcd54789727269d92f4decf3c5cdec5f Mon Sep 17 00:00:00 2001 From: RishabhS7 <59636880+RishabhS7@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:58:43 +0530 Subject: [PATCH 1/2] fix: add migrate to trustvc banner (#69) * fix: add GitHub Actions workflow for automated S3 deployment * fix: update yml * ci: add dev deployment workflow and rename prod workflow for S3/CloudFront deployments * fix add migrate to trustvc banner * fix: update baner link * fix: update tests * fix: update tests * fix: update tests * fix: update tests * fix: update tests * fix: update tests * fix: update tests --------- Co-authored-by: isaackps --- .github/workflows/ci.yml | 12 +++- integration/issue.mjs | 2 +- integration/metamask-init.mjs | 83 ++++++++++++++++++++-- integration/prefetch-metamask.mjs | 81 +++++++++++++++++++++ package.json | 8 ++- src/app.tsx | 15 +++- src/components/common/migration-banner.tsx | 28 ++++++++ 7 files changed, 215 insertions(+), 14 deletions(-) create mode 100644 integration/prefetch-metamask.mjs create mode 100644 src/components/common/migration-banner.tsx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc3e733..b031230 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: test: name: Lint & Test runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 25 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v3 @@ -29,7 +29,7 @@ jobs: - name: Install Dependencies env: - PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 'true' + PUPPETEER_SKIP_DOWNLOAD: 'true' run: npm i - name: Check Lint @@ -47,3 +47,11 @@ jobs: CI: "true" with: args: npm run integration:dappeteer + + - name: Upload integration debug screenshot + if: always() + uses: actions/upload-artifact@v4 + with: + name: integration-debug + path: integration/debug-metamask-timeout.png + if-no-files-found: ignore diff --git a/integration/issue.mjs b/integration/issue.mjs index 29b9308..ce6dd0c 100644 --- a/integration/issue.mjs +++ b/integration/issue.mjs @@ -4,7 +4,7 @@ import { sleep, waitForAndClickElement, waitForElement } from "./utils.mjs"; export const issue = async (metamask, browser) => { try { const page = await browser.newPage(); - await page.goto("http://localhost:4173/", { waitUntil: "networkidle" }); + await page.goto("http://localhost:4173/", { waitUntil: "networkidle0" }); // START - approve application once, subsequent tests no longer need await metamask.approve(); diff --git a/integration/metamask-init.mjs b/integration/metamask-init.mjs index 54256c7..cabd2d8 100644 --- a/integration/metamask-init.mjs +++ b/integration/metamask-init.mjs @@ -2,6 +2,53 @@ import dappeteer from "@chainsafe/dappeteer"; import { addNetwork } from "./addNetwork.mjs"; import { sleep } from "./utils.mjs"; +// dappeteer's setupMetaMask() waits on a browser "targetcreated" event with no timeout +// and no fallback if the extension never opens its home.html tab. Wrap it so a stuck +// extension fails fast with diagnostics instead of hanging until the CI job is killed. +const withDiagnosticTimeout = (promise, rawBrowser, ms) => + new Promise((resolve, reject) => { + const timer = setTimeout(() => { + dumpDiagnostics(rawBrowser).finally(() => { + reject(new Error(`Timed out after ${ms}ms waiting for MetaMask setup`)); + }); + }, ms); + promise.then( + (value) => { + clearTimeout(timer); + resolve(value); + }, + (error) => { + clearTimeout(timer); + reject(error); + } + ); + }); + +const dumpDiagnostics = async (rawBrowser) => { + try { + const pages = await rawBrowser.pages(); + console.info(`Open pages at timeout (${pages.length}):`); + for (const [i, page] of pages.entries()) { + console.info(` [${i}] ${page.url()}`); + } + const lastPage = pages[pages.length - 1]; + if (lastPage) { + await lastPage.screenshot({ path: "integration/debug-metamask-timeout.png", fullPage: true }); + console.info("Saved screenshot to integration/debug-metamask-timeout.png"); + } + } catch (diagError) { + console.error("Failed to capture MetaMask timeout diagnostics:", diagError); + } +}; + +const wireConsoleLogging = (page, label) => { + page.on("console", (msg) => console.info(`[console ${label}] ${msg.text()}`)); + page.on("pageerror", (err) => console.info(`[pageerror ${label}] ${err}`)); + page.on("requestfailed", (req) => { + console.info(`[requestfailed ${label}] ${req.url()} ${req.failure()?.errorText ?? ""}`); + }); +}; + export const metamaskInit = async () => { try { const browser = await dappeteer.launch({ @@ -10,11 +57,31 @@ export const metamaskInit = async () => { headless: false, puppeteerOptions: { args: ["--no-sandbox"], - executablePath: process.env.PUPPETEER_EXEC_PATH, + // Deliberately NOT using process.env.PUPPETEER_EXEC_PATH here: the + // puppeteer-headful Docker image points it at the system's current + // google-chrome-stable, which no longer supports the Manifest V2 + // extension format this pinned MetaMask build uses. Puppeteer's own + // bundled Chrome (pinned to a version from when MV2 still worked) + // is fetched by the integration:prefetch-chromium script instead. defaultViewport: null, slowMo: process.argv[2] || 0, }, }); + const rawBrowser = browser.getSource(); + + // Surface every target and its console/page errors as soon as they happen, so a + // failed extension load is visible instead of a silent hang. + rawBrowser.on("targetcreated", async (target) => { + console.info(`[target created] type=${target.type()} url=${target.url()}`); + const page = await target.page().catch(() => null); + if (page) { + wireConsoleLogging(page, target.url()); + } + }); + for (const page of await rawBrowser.pages()) { + console.info(`[existing target] url=${page.url()}`); + wireConsoleLogging(page, page.url()); + } await sleep(2000); const pages = await browser.pages(); @@ -25,15 +92,19 @@ export const metamaskInit = async () => { } // Get metamask - const metamask = await dappeteer.setupMetaMask(browser, { - seed: "indicate swing place chair flight used hammer soon photo region volume shuffle", - showTestNets: true, - }); + const metamask = await withDiagnosticTimeout( + dappeteer.setupMetaMask(browser, { + seed: "indicate swing place chair flight used hammer soon photo region volume shuffle", + showTestNets: true, + }), + rawBrowser, + 45000 + ); // Add network // https://github.com/ChainSafe/dappeteer/blob/b79ab4c74fab87747933d8f428624dcbffc3dd19/test/basic.spec.ts#L117-L119 const dappPage = await browser.newPage(); - await dappPage.goto("https://github.com", { waitUntil: "networkidle" }); + await dappPage.goto("https://github.com", { waitUntil: "networkidle0" }); dappPage.evaluate(addNetwork); try { diff --git a/integration/prefetch-metamask.mjs b/integration/prefetch-metamask.mjs new file mode 100644 index 0000000..37938b2 --- /dev/null +++ b/integration/prefetch-metamask.mjs @@ -0,0 +1,81 @@ +import https from "https"; +import fs from "fs"; +import path from "path"; +import { execSync } from "child_process"; + +// dappeteer's own downloader only reads page 1 (30 releases) of the GitHub API with no +// pagination, so an older pinned version falls off the list as MetaMask keeps shipping +// releases. Pre-populating dappeteer's cache directory here makes it skip that search +// entirely (it only searches when the extraction directory is missing). +const METAMASK_VERSION = "v10.31.0"; +const CACHE_DIR = path.resolve("node_modules", ".cache", ".metamask"); +const EXTRACT_DIR = path.resolve(CACHE_DIR, METAMASK_VERSION.replace(/\./g, "_")); +const DOWNLOAD_DIR = path.resolve(CACHE_DIR, "download"); +const RELEASES_URL = "https://api.github.com/repos/metamask/metamask-extension/releases"; +const MAX_PAGES = 10; + +const getJson = (url) => + new Promise((resolve, reject) => { + https + .get(url, { headers: { "User-Agent": "Mozilla/5.0" } }, (res) => { + let body = ""; + res.on("data", (chunk) => (body += chunk)); + res.on("end", () => resolve(JSON.parse(body))); + }) + .on("error", reject); + }); + +const findChromeAssetUrl = async () => { + for (let page = 1; page <= MAX_PAGES; page++) { + const releases = await getJson(`${RELEASES_URL}?per_page=100&page=${page}`); + if (!Array.isArray(releases) || releases.length === 0) break; + + const release = releases.find((r) => r.tag_name === METAMASK_VERSION); + if (release) { + const asset = release.assets.find((a) => a.name.startsWith("metamask-chrome-")); + if (asset) return asset.browser_download_url; + } + } + throw new Error(`Could not find MetaMask ${METAMASK_VERSION} chrome release asset`); +}; + +const download = (url, dest) => + new Promise((resolve, reject) => { + const request = (requestUrl) => + https + .get(requestUrl, (res) => { + if (res.statusCode === 301 || res.statusCode === 302) { + request(res.headers.location); + return; + } + const file = fs.createWriteStream(dest); + res.pipe(file); + file.on("finish", () => file.close(resolve)); + }) + .on("error", reject); + request(url); + }); + +const main = async () => { + if (fs.existsSync(EXTRACT_DIR)) { + console.info(`MetaMask ${METAMASK_VERSION} already cached at ${EXTRACT_DIR}`); + return; + } + + console.info(`Searching MetaMask releases for ${METAMASK_VERSION}...`); + const downloadUrl = await findChromeAssetUrl(); + + fs.mkdirSync(DOWNLOAD_DIR, { recursive: true }); + const zipPath = path.resolve(DOWNLOAD_DIR, `${METAMASK_VERSION}.zip`); + console.info(`Downloading ${downloadUrl}`); + await download(downloadUrl, zipPath); + + fs.mkdirSync(EXTRACT_DIR, { recursive: true }); + execSync(`unzip -q -o "${zipPath}" -d "${EXTRACT_DIR}"`); + console.info(`Extracted MetaMask ${METAMASK_VERSION} to ${EXTRACT_DIR}`); +}; + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/package.json b/package.json index 20c415a..9ffd6cd 100644 --- a/package.json +++ b/package.json @@ -9,13 +9,15 @@ "start": "npm-run-all -p start:*", "start:css": "postcss src/tailwind.css -o src/index.css --watch", "start:dev": "sleep 3 && vite", - "serve": "npm run build && vite preview", + "serve": "npm run build && vite preview --host 127.0.0.1 --port 4173", "test": "jest", "test:watch": "jest --watch", "integration:blockchain": "ganache --mnemonic \"indicate swing place chair flight used hammer soon photo region volume shuffle\"", "integration:local": "npm run serve", - "integration:metamask": "npm-run-all --serial integration:wait \"integration:transfers {1} \" --", - "integration:wait": "wait-on -l -i 1000 tcp:8545 http-get://localhost:4173", + "integration:metamask": "npm-run-all --serial integration:prefetch-metamask integration:prefetch-chromium integration:wait \"integration:transfers {1} \" --", + "integration:prefetch-metamask": "node ./integration/prefetch-metamask.mjs", + "integration:prefetch-chromium": "node node_modules/puppeteer/install.mjs", + "integration:wait": "wait-on -l -i 1000 -t 60000 tcp:8545 http-get://127.0.0.1:4173", "integration:transfers": "node --experimental-modules ./integration/index.mjs", "integration:dappeteer": "npm-run-all --parallel integration:blockchain integration:local \"integration:metamask {1}\" --race --", "lint": "eslint ./src ./integration --ext .js,.ts,.tsx,.mjs --max-warnings 0", diff --git a/src/app.tsx b/src/app.tsx index fe6b7b7..d5a191f 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -4,6 +4,7 @@ import { MainPage } from "./page/main-page"; import { MetaMaskError } from "./page/metamask-error"; import { MetaMaskPending } from "./page/metamask-pending"; import { EthereumProviderError } from "eth-rpc-errors"; +import { MigrationBanner } from "./components/common/migration-banner"; const App: FunctionComponent = () => { const [metamaskConnected, setMetamaskConnected] = useState(false); @@ -30,10 +31,20 @@ const App: FunctionComponent = () => { }; if (!window.ethereum) { - return ; + return ( + <> + + + + ); } - return metamaskConnected ? : ; + return ( + <> + + {metamaskConnected ? : } + + ); }; export default App; diff --git a/src/components/common/migration-banner.tsx b/src/components/common/migration-banner.tsx new file mode 100644 index 0000000..cc61e56 --- /dev/null +++ b/src/components/common/migration-banner.tsx @@ -0,0 +1,28 @@ +import React, { FunctionComponent } from "react"; + +const MIGRATION_GUIDE_URL = "https://docs.opencerts.io/docs/migrations/oa_to_trustvc"; + +export const MigrationBanner: FunctionComponent = () => ( +
+ Please note that as of 1 October 2025, OpenAttestation (OA) has been deprecated. OpenCerts has since migrated to + TrustVC, which uses the W3C Verifiable Credentials (VC) format. +
+ Documents previously issued in OA format remain verifiable — no action is required for existing certificates. + However, we recommend upgrading to the W3C VC format for all new issuances. +
+ To migrate, please refer to our{" "} + + migration guide + + . +
+); From d7f6a9fe4545043d67cfd9330ecae4e394898248 Mon Sep 17 00:00:00 2001 From: RishabhS7 <59636880+RishabhS7@users.noreply.github.com> Date: Thu, 9 Jul 2026 10:53:34 +0530 Subject: [PATCH 2/2] fix: use PUPPETEER_SKIP_DOWNLOAD in deploy workflows (#70) PUPPETEER_SKIP_CHROMIUM_DOWNLOAD is not read by Puppeteer 21+, so npm i was downloading Chromium on every deploy and hanging past the job's timeout-minutes (confirmed on the dev pipeline run 28939125376, which never shipped). Matches the same fix already applied to ci.yml. --- .github/workflows/deploy-s3-dev.yml | 2 +- .github/workflows/deploy-s3-prod.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-s3-dev.yml b/.github/workflows/deploy-s3-dev.yml index 030f67f..100c033 100644 --- a/.github/workflows/deploy-s3-dev.yml +++ b/.github/workflows/deploy-s3-dev.yml @@ -37,7 +37,7 @@ jobs: - name: Install Dependencies env: - PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: "true" + PUPPETEER_SKIP_DOWNLOAD: "true" run: npm i - name: Build diff --git a/.github/workflows/deploy-s3-prod.yml b/.github/workflows/deploy-s3-prod.yml index 1a07265..67db4d2 100644 --- a/.github/workflows/deploy-s3-prod.yml +++ b/.github/workflows/deploy-s3-prod.yml @@ -36,7 +36,7 @@ jobs: - name: Install Dependencies env: - PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: "true" + PUPPETEER_SKIP_DOWNLOAD: "true" run: npm i - name: Build