From 2c1fbb6114a0101f97dc5da05bbad6aa0d45b064 Mon Sep 17 00:00:00 2001 From: fox Date: Thu, 16 Apr 2026 16:57:00 -0400 Subject: [PATCH] chore(paywall): regenerate bundle + add recurrence guard The committed @x402/paywall bundle on main was last generated against an older viem than the lockfile currently pins. pnpm install --frozen-lockfile resolves viem 2.47.12 (via #2013), but the bundled src/evm/gen/template.ts (and its Python + Go siblings) were built against an earlier resolution that pre-dated chain definitions for Mezo, MegaETH, Stable, Radius (each with a testnet variant), and 33 others. EvmPaywall.tsx throws "Unsupported chain ID" at init for any eip155: whose chain isn't in the bundled viem/chains. This PR: 1. Regenerates all nine auto-generated template files (TypeScript, Python, and Go for EVM/SVM/AVM) against the current lockfile. No source changes on any runtime path. 2. Teaches build:paywall to emit output matching the repo's formatters (prettier / ruff), so generated files stay clean under pnpm run format + uvx ruff format. 3. Adds a PR-time drift check (.github/workflows/check_paywall_template.yml) so a future lockfile or @x402/evm change that invalidates the bundle fails loudly instead of shipping silently. Scope is limited to @x402/paywall because it is the only package in this repo that bakes viem chain definitions into a built artifact. Verified on a fork-internal test PR running on ubuntu-latest (Node 24, pnpm 10.7.0): check_format, check_lint, check_go, check_python, check_package_lock, and the new check_paywall_template drift check all pass. Additional local evidence: - Chain-presence in regenerated bundle: Mezo 31612/31611, MegaETH 4326/6343, Stable 988/2201, Radius 723487/72344, Base/Arbitrum/ Polygon all present (mainnet/testnet ordering). - Cross-platform determinism: a clean-slate rebuild on macOS produces byte-identical output to ubuntu-latest CI. - Tarball install into a throwaway consumer: all expected chain IDs present in the published dist/esm/evm/index.js. - Browser smoke test (Chrome, served from a local HTTP server): EvmPaywall mounts without Unsupported-chain throw for eip155:31611 (Mezo Testnet) and eip155:723487 (Radius); DOM renders the expected token/chain labels. Fixes #1971. --- .github/workflows/check_paywall_template.yml | 90 +++++++++++++++++++ go/http/avm_paywall_template.go | 5 ++ go/http/evm_paywall_template.go | 2 +- go/http/svm_paywall_template.go | 2 +- .../x402/http/paywall/avm_paywall_template.py | 2 + .../x402/http/paywall/evm_paywall_template.py | 2 +- .../x402/http/paywall/svm_paywall_template.py | 2 +- .../regenerate-paywall-for-viem-2-47-12.md | 22 +++++ .../packages/http/paywall/src/avm/build.ts | 6 +- .../http/paywall/src/avm/gen/template.ts | 2 +- .../packages/http/paywall/src/evm/build.ts | 6 +- .../http/paywall/src/evm/gen/template.ts | 2 +- .../packages/http/paywall/src/genHelpers.ts | 45 ++++++++++ .../packages/http/paywall/src/svm/build.ts | 6 +- .../http/paywall/src/svm/gen/template.ts | 2 +- 15 files changed, 183 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/check_paywall_template.yml create mode 100644 go/http/avm_paywall_template.go create mode 100644 python/x402/http/paywall/avm_paywall_template.py create mode 100644 typescript/.changeset/regenerate-paywall-for-viem-2-47-12.md create mode 100644 typescript/packages/http/paywall/src/genHelpers.ts diff --git a/.github/workflows/check_paywall_template.yml b/.github/workflows/check_paywall_template.yml new file mode 100644 index 0000000000..2ec793ece9 --- /dev/null +++ b/.github/workflows/check_paywall_template.yml @@ -0,0 +1,90 @@ +name: Check Paywall Template + +on: + pull_request: + paths: + - "typescript/packages/http/paywall/**" + - "typescript/packages/mechanisms/evm/**" + - "typescript/pnpm-lock.yaml" + - "python/x402/http/paywall/**" + - "go/http/evm_paywall_template.go" + - "go/http/svm_paywall_template.go" + - "go/http/avm_paywall_template.go" + - ".github/workflows/check_paywall_template.yml" + +jobs: + check-paywall-template: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + with: + version: 10.7.0 + + - uses: actions/setup-node@v4 + with: + node-version: "24" + cache: "pnpm" + cache-dependency-path: ./typescript + + - name: Install dependencies + working-directory: ./typescript + run: pnpm install --frozen-lockfile + + - name: Build workspace dependencies + working-directory: ./typescript + run: pnpm -r run build + + - name: Regenerate paywall bundles + working-directory: ./typescript/packages/http/paywall + run: pnpm run build:paywall + + - name: Verify regenerated templates match committed files + run: | + git diff --exit-code -- \ + typescript/packages/http/paywall/src/evm/gen/template.ts \ + typescript/packages/http/paywall/src/svm/gen/template.ts \ + typescript/packages/http/paywall/src/avm/gen/template.ts \ + python/x402/http/paywall/evm_paywall_template.py \ + python/x402/http/paywall/svm_paywall_template.py \ + python/x402/http/paywall/avm_paywall_template.py \ + go/http/evm_paywall_template.go \ + go/http/svm_paywall_template.go \ + go/http/avm_paywall_template.go \ + || { + echo "" + echo "::error::Paywall bundled templates are stale. Run 'pnpm --filter @x402/paywall run build:paywall' locally and commit the result." + exit 1 + } + + - name: Verify determinism (re-run build, compare to first run) + working-directory: ./typescript/packages/http/paywall + run: | + first_sha=$(sha256sum \ + src/evm/gen/template.ts \ + src/svm/gen/template.ts \ + src/avm/gen/template.ts \ + ../../../../python/x402/http/paywall/evm_paywall_template.py \ + ../../../../python/x402/http/paywall/svm_paywall_template.py \ + ../../../../python/x402/http/paywall/avm_paywall_template.py \ + ../../../../go/http/evm_paywall_template.go \ + ../../../../go/http/svm_paywall_template.go \ + ../../../../go/http/avm_paywall_template.go) + pnpm run build:paywall + second_sha=$(sha256sum \ + src/evm/gen/template.ts \ + src/svm/gen/template.ts \ + src/avm/gen/template.ts \ + ../../../../python/x402/http/paywall/evm_paywall_template.py \ + ../../../../python/x402/http/paywall/svm_paywall_template.py \ + ../../../../python/x402/http/paywall/avm_paywall_template.py \ + ../../../../go/http/evm_paywall_template.go \ + ../../../../go/http/svm_paywall_template.go \ + ../../../../go/http/avm_paywall_template.go) + if [ "$first_sha" != "$second_sha" ]; then + echo "::error::build:paywall is non-deterministic across repeat runs. See x4-23b for context." + diff <(echo "$first_sha") <(echo "$second_sha") + exit 1 + fi diff --git a/go/http/avm_paywall_template.go b/go/http/avm_paywall_template.go new file mode 100644 index 0000000000..dfb6ecd05b --- /dev/null +++ b/go/http/avm_paywall_template.go @@ -0,0 +1,5 @@ +// THIS FILE IS AUTO-GENERATED - DO NOT EDIT +package http + +// AVMPaywallTemplate is the pre-built AVM paywall template with inlined CSS and JS +const AVMPaywallTemplate = "\n \n \n Payment Required\n \n
\n \n \n " diff --git a/go/http/evm_paywall_template.go b/go/http/evm_paywall_template.go index 83867a322e..a9d8e98441 100644 --- a/go/http/evm_paywall_template.go +++ b/go/http/evm_paywall_template.go @@ -2,4 +2,4 @@ package http // EVMPaywallTemplate is the pre-built EVM paywall template with inlined CSS and JS -const EVMPaywallTemplate = "\n \n \n Payment Required\n \n
\n \n \n " +const EVMPaywallTemplate = "\n \n \n Payment Required\n \n
\n \n \n " diff --git a/go/http/svm_paywall_template.go b/go/http/svm_paywall_template.go index dd586b94ee..a5aa40b99c 100644 --- a/go/http/svm_paywall_template.go +++ b/go/http/svm_paywall_template.go @@ -2,4 +2,4 @@ package http // SVMPaywallTemplate is the pre-built SVM paywall template with inlined CSS and JS -const SVMPaywallTemplate = "\n \n \n Payment Required\n \n
\n \n \n " +const SVMPaywallTemplate = "\n \n \n Payment Required\n \n
\n \n \n " diff --git a/python/x402/http/paywall/avm_paywall_template.py b/python/x402/http/paywall/avm_paywall_template.py new file mode 100644 index 0000000000..53cc1c7758 --- /dev/null +++ b/python/x402/http/paywall/avm_paywall_template.py @@ -0,0 +1,2 @@ +# THIS FILE IS AUTO-GENERATED - DO NOT EDIT +AVM_PAYWALL_TEMPLATE = '\n \n \n Payment Required\n \n
\n \n \n ' diff --git a/python/x402/http/paywall/evm_paywall_template.py b/python/x402/http/paywall/evm_paywall_template.py index d39b605ad5..5287909f68 100644 --- a/python/x402/http/paywall/evm_paywall_template.py +++ b/python/x402/http/paywall/evm_paywall_template.py @@ -1,2 +1,2 @@ # THIS FILE IS AUTO-GENERATED - DO NOT EDIT -EVM_PAYWALL_TEMPLATE = '\n \n \n Payment Required\n \n
\n \n \n ' +EVM_PAYWALL_TEMPLATE = '\n \n \n Payment Required\n \n
\n \n \n ' diff --git a/python/x402/http/paywall/svm_paywall_template.py b/python/x402/http/paywall/svm_paywall_template.py index f1d95458ad..400883705f 100644 --- a/python/x402/http/paywall/svm_paywall_template.py +++ b/python/x402/http/paywall/svm_paywall_template.py @@ -1,2 +1,2 @@ # THIS FILE IS AUTO-GENERATED - DO NOT EDIT -SVM_PAYWALL_TEMPLATE = '\n \n \n Payment Required\n \n
\n \n \n ' +SVM_PAYWALL_TEMPLATE = '\n \n \n Payment Required\n \n
\n \n \n ' diff --git a/typescript/.changeset/regenerate-paywall-for-viem-2-47-12.md b/typescript/.changeset/regenerate-paywall-for-viem-2-47-12.md new file mode 100644 index 0000000000..65dcceb8f3 --- /dev/null +++ b/typescript/.changeset/regenerate-paywall-for-viem-2-47-12.md @@ -0,0 +1,22 @@ +--- +"@x402/paywall": patch +--- + +chore(paywall): regenerate EVM/SVM/AVM bundles for viem 2.47.12 + +The bundled paywall templates were last regenerated against a viem version +that predates chain definitions for Mezo (`eip155:31612`), Mezo Testnet +(`eip155:31611`), MegaETH (`eip155:4326`), MegaETH Testnet (`eip155:6343`), +Stable (`eip155:988`), Stable Testnet (`eip155:2201`), Radius +(`eip155:723487`), Radius Testnet (`eip155:72344`), and 33 other chains. The +lockfile moved to viem 2.47.12 in PR #2013 but the bundle was not +regenerated, so @x402/paywall hard-threw `Unsupported chain ID` at component +init for payments on those chains. + +This commit regenerates all nine generated files (TypeScript, Python, and +Go templates for EVM/SVM/AVM) against the current lockfile. Total unique +chain IDs in the EVM bundle goes from 635 to 676. + +No source code changes. Paired with a new PR-time drift check +(`.github/workflows/check_paywall_template.yml`) so this stays fresh +across future viem bumps. diff --git a/typescript/packages/http/paywall/src/avm/build.ts b/typescript/packages/http/paywall/src/avm/build.ts index 329cff2325..8dcbd3538c 100644 --- a/typescript/packages/http/paywall/src/avm/build.ts +++ b/typescript/packages/http/paywall/src/avm/build.ts @@ -3,6 +3,7 @@ import { htmlPlugin } from "@craftamap/esbuild-plugin-html"; import fs from "fs"; import path from "path"; import { getBaseTemplate } from "../baseTemplate"; +import { formatTypeScript, toPythonStringLiteral } from "../genHelpers"; // AVM-specific build - only bundles Algorand dependencies const DIST_DIR = "src/avm/dist"; @@ -84,16 +85,17 @@ async function build() { if (fs.existsSync(OUTPUT_HTML)) { const html = fs.readFileSync(OUTPUT_HTML, "utf8"); - const tsContent = `// THIS FILE IS AUTO-GENERATED - DO NOT EDIT + const rawTsContent = `// THIS FILE IS AUTO-GENERATED - DO NOT EDIT /** * The pre-built AVM paywall template with inlined CSS and JS */ export const AVM_PAYWALL_TEMPLATE = ${JSON.stringify(html)}; `; + const tsContent = await formatTypeScript(OUTPUT_TS, rawTsContent); // Generate Python template file const pyContent = `# THIS FILE IS AUTO-GENERATED - DO NOT EDIT -AVM_PAYWALL_TEMPLATE = ${JSON.stringify(html)} +AVM_PAYWALL_TEMPLATE = ${toPythonStringLiteral(html)} `; // Generate Go template file diff --git a/typescript/packages/http/paywall/src/avm/gen/template.ts b/typescript/packages/http/paywall/src/avm/gen/template.ts index cd7ad5faac..c2d4680985 100644 --- a/typescript/packages/http/paywall/src/avm/gen/template.ts +++ b/typescript/packages/http/paywall/src/avm/gen/template.ts @@ -3,4 +3,4 @@ * The pre-built AVM paywall template with inlined CSS and JS */ export const AVM_PAYWALL_TEMPLATE = - '\n \n \n Payment Required\n \n
\n \n \n '; + '\n \n \n Payment Required\n \n
\n \n \n '; diff --git a/typescript/packages/http/paywall/src/evm/build.ts b/typescript/packages/http/paywall/src/evm/build.ts index 0d7d924882..af1a093b2d 100644 --- a/typescript/packages/http/paywall/src/evm/build.ts +++ b/typescript/packages/http/paywall/src/evm/build.ts @@ -3,6 +3,7 @@ import { htmlPlugin } from "@craftamap/esbuild-plugin-html"; import fs from "fs"; import path from "path"; import { getBaseTemplate } from "../baseTemplate"; +import { formatTypeScript, toPythonStringLiteral } from "../genHelpers"; // EVM-specific build - only bundles EVM dependencies const DIST_DIR = "src/evm/dist"; @@ -76,16 +77,17 @@ async function build() { if (fs.existsSync(OUTPUT_HTML)) { const html = fs.readFileSync(OUTPUT_HTML, "utf8"); - const tsContent = `// THIS FILE IS AUTO-GENERATED - DO NOT EDIT + const rawTsContent = `// THIS FILE IS AUTO-GENERATED - DO NOT EDIT /** * The pre-built EVM paywall template with inlined CSS and JS */ export const EVM_PAYWALL_TEMPLATE = ${JSON.stringify(html)}; `; + const tsContent = await formatTypeScript(OUTPUT_TS, rawTsContent); // Generate Python template file const pyContent = `# THIS FILE IS AUTO-GENERATED - DO NOT EDIT -EVM_PAYWALL_TEMPLATE = ${JSON.stringify(html)} +EVM_PAYWALL_TEMPLATE = ${toPythonStringLiteral(html)} `; // Generate Go template file diff --git a/typescript/packages/http/paywall/src/evm/gen/template.ts b/typescript/packages/http/paywall/src/evm/gen/template.ts index 73ce58d52c..c2c2a5836a 100644 --- a/typescript/packages/http/paywall/src/evm/gen/template.ts +++ b/typescript/packages/http/paywall/src/evm/gen/template.ts @@ -3,4 +3,4 @@ * The pre-built EVM paywall template with inlined CSS and JS */ export const EVM_PAYWALL_TEMPLATE = - '\n \n \n Payment Required\n \n
\n \n \n '; + '\n \n \n Payment Required\n \n
\n \n \n '; diff --git a/typescript/packages/http/paywall/src/genHelpers.ts b/typescript/packages/http/paywall/src/genHelpers.ts new file mode 100644 index 0000000000..0f076291d2 --- /dev/null +++ b/typescript/packages/http/paywall/src/genHelpers.ts @@ -0,0 +1,45 @@ +import prettier from "prettier"; + +/** + * Format a TypeScript file's contents using the paywall package's prettier + * config. Used to keep auto-generated template files byte-identical to what + * `pnpm run format` would emit, so CI drift checks stay reliable. + * + * @param filePath - Absolute or relative path used by prettier for config + * resolution and filetype inference. + * @param contents - Source contents to format. + * @returns The prettier-formatted contents. + */ +export async function formatTypeScript(filePath: string, contents: string): Promise { + const config = await prettier.resolveConfig(filePath); + return prettier.format(contents, { ...config, filepath: filePath }); +} + +/** + * Serialize a string as a Python source literal, choosing the quote style + * that requires fewer escapes. Mirrors ruff/black's default behavior so that + * `ruff format --check` is a no-op on generated files. + * + * @param s - Input string to serialize. + * @returns A Python string literal (quoted and escaped) representing `s`. + */ +export function toPythonStringLiteral(s: string): string { + const singleCount = (s.match(/'/g) || []).length; + const doubleCount = (s.match(/"/g) || []).length; + const useSingle = doubleCount > singleCount; + const quote = useSingle ? "'" : '"'; + + let body = s + .replace(/\\/g, "\\\\") + .replace(/\n/g, "\\n") + .replace(/\r/g, "\\r") + .replace(/\t/g, "\\t") + .replace(/\f/g, "\\f") + .replace(/\v/g, "\\v") + .replace( + /[\x00-\x08\x0e-\x1f\x7f]/g, + c => `\\x${c.charCodeAt(0).toString(16).padStart(2, "0")}`, + ); + body = useSingle ? body.replace(/'/g, "\\'") : body.replace(/"/g, '\\"'); + return `${quote}${body}${quote}`; +} diff --git a/typescript/packages/http/paywall/src/svm/build.ts b/typescript/packages/http/paywall/src/svm/build.ts index bf1bfe0de9..d0101004e9 100644 --- a/typescript/packages/http/paywall/src/svm/build.ts +++ b/typescript/packages/http/paywall/src/svm/build.ts @@ -3,6 +3,7 @@ import { htmlPlugin } from "@craftamap/esbuild-plugin-html"; import fs from "fs"; import path from "path"; import { getBaseTemplate } from "../baseTemplate"; +import { formatTypeScript, toPythonStringLiteral } from "../genHelpers"; // SVM-specific build - only bundles Solana dependencies const DIST_DIR = "src/svm/dist"; @@ -76,16 +77,17 @@ async function build() { if (fs.existsSync(OUTPUT_HTML)) { const html = fs.readFileSync(OUTPUT_HTML, "utf8"); - const tsContent = `// THIS FILE IS AUTO-GENERATED - DO NOT EDIT + const rawTsContent = `// THIS FILE IS AUTO-GENERATED - DO NOT EDIT /** * The pre-built SVM paywall template with inlined CSS and JS */ export const SVM_PAYWALL_TEMPLATE = ${JSON.stringify(html)}; `; + const tsContent = await formatTypeScript(OUTPUT_TS, rawTsContent); // Generate Python template file const pyContent = `# THIS FILE IS AUTO-GENERATED - DO NOT EDIT -SVM_PAYWALL_TEMPLATE = ${JSON.stringify(html)} +SVM_PAYWALL_TEMPLATE = ${toPythonStringLiteral(html)} `; // Generate Go template file diff --git a/typescript/packages/http/paywall/src/svm/gen/template.ts b/typescript/packages/http/paywall/src/svm/gen/template.ts index a6e7bd39b0..ef3f3fcd18 100644 --- a/typescript/packages/http/paywall/src/svm/gen/template.ts +++ b/typescript/packages/http/paywall/src/svm/gen/template.ts @@ -3,4 +3,4 @@ * The pre-built SVM paywall template with inlined CSS and JS */ export const SVM_PAYWALL_TEMPLATE = - '\n \n \n Payment Required\n \n
\n \n \n '; + '\n \n \n Payment Required\n \n
\n \n \n ';