test(wallet): Tier-2 browser-signing e2e (Playwright + mock provider on anvil) - #380

Merged
MuncleUscles merged 1 commit into
v0.40-devfrom
feat/cli-wallet-e2e
Jul 9, 2026
Merged

test(wallet): Tier-2 browser-signing e2e (Playwright + mock provider on anvil)#380
MuncleUscles merged 1 commit into
v0.40-devfrom
feat/cli-wallet-e2e

Conversation

@MuncleUscles

Copy link
Copy Markdown
Member

What

Adds the Tier-2 browser-wallet signing e2e harness for genlayer-cli, per the design doc. It drives the real daemon bridge page (src/lib/wallet/bridgePage.ts) in a headless chromium with an injected mock window.ethereum, and delegates signing to Node via page.exposeFunction("__glSign", …) where a viem local account (anvil dev key) actually signs + broadcasts eth_sendTransaction to an ephemeral anvil, returning a real tx hash.

So the full loop runs for real — bridge + served page JS + session daemon + real chain — with zero human and zero MetaMask.

How the mock works

  • e2e/fixtures/mockProvider.tsinstallMockProvider({address, chainIdHex, behavior}) returns an init-script string installed via page.addInitScript(...)beforepage.goto() (beats the injection race). Behaviors: approve | reject (4001) | wrong-network (4901). eth_sendTransactionwindow.__glSign.
  • e2e/helpers/bridgePage.ts — launches chromium, wires __glSign to a viem sendTransaction on anvil, injects the mock, and drives connect.
  • e2e/fixtures/chain.tsstartAnvil() boots anvil on a random port and deploys the recording StakingStub.
  • e2e/fixtures/cli.ts — runs dist/index.js under a scratch HOME with a seeded custom-network config (chain-id / rpc / staking → the anvil + stub) so ensureChain() matches; spawnConnect() scrapes the printed session URL.

Lanes (all green on anvil, ~16s, serial workers:1)

  • S1 connect/status/disconnect — descriptor 0600{pid live, port, token, address, chainId}, /api/ping 200, "Connected as …", teardown removes descriptor + pid.
  • S2staking validator-join --wallet browser — page auto-signs → asserted via a real receipt + on-chain stub callCount.
  • S4 session reuse — two sequential joins over one daemon/tab, distinct hashes, same pid.
  • S5 config default walletMode=browser — bare validator-join signs via session; --wallet keystore overrides (keystore path, no enqueue).
  • S6 user-reject (4001 → "Transaction rejected in wallet", session survives) + tab-closed (page.close() → stale heartbeat → fail-fast "tab appears to be closed"). Short GENLAYER_E2E_* timeout overrides keep these in seconds.

Staking stub

e2e/fixtures/StakingStub.sol (+ committed compiled StakingStub.json) is a ~10-line recording stub: validatorJoin() / validatorJoin(address) are payable, record the call (callCount, lastOperator/Validator/Amount), and emit the ValidatorJoin(operator, validator, amount) event the CLI decodes. It proves the sign→broadcast→receipt loop only — no consensus.

Deferred (nightly follow-up)

  • Lane B (S3) — IC deploy on Docker localnet: describe.skip, guarded by GENLAYER_E2E_LOCALNET, documented in e2e/lane-b-deploy.e2e.ts. Bare anvil has no GenVM/consensus.
  • Tier 3 (Synpress / real MetaMask) — out of scope.

Prod-safe support changes

  • sessionConstants.tsGENLAYER_E2E_{LONG_POLL,HEARTBEAT_DEAD,CONNECT_TIMEOUT}_MS overrides; unset/invalid → production defaults unchanged.
  • browserBridge.ts — skip openUrl when GENLAYER_E2E_NO_OPEN is set (harness drives its own chromium); production unaffected.

CI

New .github/workflows/e2e-wallet.yml (distinct name from the synced e2e.yml): ubuntu + node 20 → npm ci → build → foundry-toolchain → playwright install --with-deps chromiumnpm run test:e2e. Informational — keep off required checks until stable, then promote.

How to run

npm run build # specs spawn dist/index.js
npx playwright install chromium
npm run test:e2e

Verification

  • npm run test:e2e9 passed, 1 skipped (Lane B), stable across 3 runs.
  • npm run test:coverage (Tier-1 vitest) → 733 passed; Playwright specs excluded from the vitest glob.
  • npm run build clean; tsc --noEmit = 32 errors (pre-existing baseline, zero new).

…on anvil)
Add a headless, deterministic Tier-2 e2e harness that drives the real
bridge page (bridgePage.ts) in chromium with an injected mock
window.ethereum. eth_sendTransaction is delegated to Node via
page.exposeFunction(__glSign), where a viem local account (anvil dev
key) actually signs+broadcasts to an ephemeral anvil, so the full loop
runs for real: bridge + page JS + session daemon + chain, with zero
human/extension.
Lanes (all green on anvil):
- S1 connect/status/disconnect (descriptor 0600, /api/ping, teardown)
- S2 validator-join --wallet browser signs + mines against a recording
StakingStub, asserted via real receipt + on-chain callCount
- S4 session reuse: two sequential joins over one daemon/tab
- S5 config default walletMode=browser; --wallet keystore overrides
- S6 user-reject (4001) + tab-closed fail-fast (short env timeouts)
Lane B (S3, IC deploy on Docker localnet) is deferred as a nightly
follow-up: describe.skip guarded by GENLAYER_E2E_LOCALNET.
Prod-safe support changes: GENLAYER_E2E_* env overrides for the timing
constants (unset in prod) and a GENLAYER_E2E_NO_OPEN guard so the daemon
does not auto-open a system browser under test. New e2e-wallet.yml CI job
(informational, not a required gate initially). Playwright specs are kept
out of the vitest glob so test:coverage is unaffected.
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 31e970ee-5801-430d-8c8d-58cc5c61255f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cli-wallet-e2e

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@MuncleUscles
MuncleUscles merged commit 4b26cc6 into v0.40-devJul 9, 2026
10 of 11 checks passed
@MuncleUscles
MuncleUscles deleted the feat/cli-wallet-e2e branch July 9, 2026 20:01
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@MuncleUscles
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

test(wallet): Tier-2 browser-signing e2e (Playwright + mock provider on anvil) - #380

Merged
MuncleUscles merged 1 commit into
v0.40-devfrom
feat/cli-wallet-e2e
Jul 9, 2026
Merged

test(wallet): Tier-2 browser-signing e2e (Playwright + mock provider on anvil)#380
MuncleUscles merged 1 commit into
v0.40-devfrom
feat/cli-wallet-e2e

Conversation

@MuncleUscles

Copy link
Copy Markdown
Member

What

Adds the Tier-2 browser-wallet signing e2e harness for genlayer-cli, per the design doc. It drives the real daemon bridge page (src/lib/wallet/bridgePage.ts) in a headless chromium with an injected mock window.ethereum, and delegates signing to Node via page.exposeFunction("__glSign", …) where a viem local account (anvil dev key) actually signs + broadcasts eth_sendTransaction to an ephemeral anvil, returning a real tx hash.

So the full loop runs for real — bridge + served page JS + session daemon + real chain — with zero human and zero MetaMask.

How the mock works

  • e2e/fixtures/mockProvider.tsinstallMockProvider({address, chainIdHex, behavior}) returns an init-script string installed via page.addInitScript(...)beforepage.goto() (beats the injection race). Behaviors: approve | reject (4001) | wrong-network (4901). eth_sendTransactionwindow.__glSign.
  • e2e/helpers/bridgePage.ts — launches chromium, wires __glSign to a viem sendTransaction on anvil, injects the mock, and drives connect.
  • e2e/fixtures/chain.tsstartAnvil() boots anvil on a random port and deploys the recording StakingStub.
  • e2e/fixtures/cli.ts — runs dist/index.js under a scratch HOME with a seeded custom-network config (chain-id / rpc / staking → the anvil + stub) so ensureChain() matches; spawnConnect() scrapes the printed session URL.

Lanes (all green on anvil, ~16s, serial workers:1)

  • S1 connect/status/disconnect — descriptor 0600{pid live, port, token, address, chainId}, /api/ping 200, "Connected as …", teardown removes descriptor + pid.
  • S2staking validator-join --wallet browser — page auto-signs → asserted via a real receipt + on-chain stub callCount.
  • S4 session reuse — two sequential joins over one daemon/tab, distinct hashes, same pid.
  • S5 config default walletMode=browser — bare validator-join signs via session; --wallet keystore overrides (keystore path, no enqueue).
  • S6 user-reject (4001 → "Transaction rejected in wallet", session survives) + tab-closed (page.close() → stale heartbeat → fail-fast "tab appears to be closed"). Short GENLAYER_E2E_* timeout overrides keep these in seconds.

Staking stub

e2e/fixtures/StakingStub.sol (+ committed compiled StakingStub.json) is a ~10-line recording stub: validatorJoin() / validatorJoin(address) are payable, record the call (callCount, lastOperator/Validator/Amount), and emit the ValidatorJoin(operator, validator, amount) event the CLI decodes. It proves the sign→broadcast→receipt loop only — no consensus.

Deferred (nightly follow-up)

  • Lane B (S3) — IC deploy on Docker localnet: describe.skip, guarded by GENLAYER_E2E_LOCALNET, documented in e2e/lane-b-deploy.e2e.ts. Bare anvil has no GenVM/consensus.
  • Tier 3 (Synpress / real MetaMask) — out of scope.

Prod-safe support changes

  • sessionConstants.tsGENLAYER_E2E_{LONG_POLL,HEARTBEAT_DEAD,CONNECT_TIMEOUT}_MS overrides; unset/invalid → production defaults unchanged.
  • browserBridge.ts — skip openUrl when GENLAYER_E2E_NO_OPEN is set (harness drives its own chromium); production unaffected.

CI

New .github/workflows/e2e-wallet.yml (distinct name from the synced e2e.yml): ubuntu + node 20 → npm ci → build → foundry-toolchain → playwright install --with-deps chromiumnpm run test:e2e. Informational — keep off required checks until stable, then promote.

How to run

npm run build # specs spawn dist/index.js
npx playwright install chromium
npm run test:e2e

Verification

  • npm run test:e2e9 passed, 1 skipped (Lane B), stable across 3 runs.
  • npm run test:coverage (Tier-1 vitest) → 733 passed; Playwright specs excluded from the vitest glob.
  • npm run build clean; tsc --noEmit = 32 errors (pre-existing baseline, zero new).

…on anvil)
Add a headless, deterministic Tier-2 e2e harness that drives the real
bridge page (bridgePage.ts) in chromium with an injected mock
window.ethereum. eth_sendTransaction is delegated to Node via
page.exposeFunction(__glSign), where a viem local account (anvil dev
key) actually signs+broadcasts to an ephemeral anvil, so the full loop
runs for real: bridge + page JS + session daemon + chain, with zero
human/extension.
Lanes (all green on anvil):
- S1 connect/status/disconnect (descriptor 0600, /api/ping, teardown)
- S2 validator-join --wallet browser signs + mines against a recording
StakingStub, asserted via real receipt + on-chain callCount
- S4 session reuse: two sequential joins over one daemon/tab
- S5 config default walletMode=browser; --wallet keystore overrides
- S6 user-reject (4001) + tab-closed fail-fast (short env timeouts)
Lane B (S3, IC deploy on Docker localnet) is deferred as a nightly
follow-up: describe.skip guarded by GENLAYER_E2E_LOCALNET.
Prod-safe support changes: GENLAYER_E2E_* env overrides for the timing
constants (unset in prod) and a GENLAYER_E2E_NO_OPEN guard so the daemon
does not auto-open a system browser under test. New e2e-wallet.yml CI job
(informational, not a required gate initially). Playwright specs are kept
out of the vitest glob so test:coverage is unaffected.
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 31e970ee-5801-430d-8c8d-58cc5c61255f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cli-wallet-e2e

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@MuncleUscles
MuncleUscles merged commit 4b26cc6 into v0.40-devJul 9, 2026
10 of 11 checks passed
@MuncleUscles
MuncleUscles deleted the feat/cli-wallet-e2e branch July 9, 2026 20:01
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@MuncleUscles
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(wallet): Tier-2 browser-signing e2e (Playwright + mock provider on anvil) - #380

Merged
MuncleUscles merged 1 commit into
v0.40-devfrom
feat/cli-wallet-e2e
Jul 9, 2026
Merged

test(wallet): Tier-2 browser-signing e2e (Playwright + mock provider on anvil)#380
MuncleUscles merged 1 commit into
v0.40-devfrom
feat/cli-wallet-e2e

Conversation

@MuncleUscles

Copy link
Copy Markdown
Member

What

Adds the Tier-2 browser-wallet signing e2e harness for genlayer-cli, per the design doc. It drives the real daemon bridge page (src/lib/wallet/bridgePage.ts) in a headless chromium with an injected mock window.ethereum, and delegates signing to Node via page.exposeFunction("__glSign", …) where a viem local account (anvil dev key) actually signs + broadcasts eth_sendTransaction to an ephemeral anvil, returning a real tx hash.

So the full loop runs for real — bridge + served page JS + session daemon + real chain — with zero human and zero MetaMask.

How the mock works

  • e2e/fixtures/mockProvider.tsinstallMockProvider({address, chainIdHex, behavior}) returns an init-script string installed via page.addInitScript(...)beforepage.goto() (beats the injection race). Behaviors: approve | reject (4001) | wrong-network (4901). eth_sendTransactionwindow.__glSign.
  • e2e/helpers/bridgePage.ts — launches chromium, wires __glSign to a viem sendTransaction on anvil, injects the mock, and drives connect.
  • e2e/fixtures/chain.tsstartAnvil() boots anvil on a random port and deploys the recording StakingStub.
  • e2e/fixtures/cli.ts — runs dist/index.js under a scratch HOME with a seeded custom-network config (chain-id / rpc / staking → the anvil + stub) so ensureChain() matches; spawnConnect() scrapes the printed session URL.

Lanes (all green on anvil, ~16s, serial workers:1)

  • S1 connect/status/disconnect — descriptor 0600{pid live, port, token, address, chainId}, /api/ping 200, "Connected as …", teardown removes descriptor + pid.
  • S2staking validator-join --wallet browser — page auto-signs → asserted via a real receipt + on-chain stub callCount.
  • S4 session reuse — two sequential joins over one daemon/tab, distinct hashes, same pid.
  • S5 config default walletMode=browser — bare validator-join signs via session; --wallet keystore overrides (keystore path, no enqueue).
  • S6 user-reject (4001 → "Transaction rejected in wallet", session survives) + tab-closed (page.close() → stale heartbeat → fail-fast "tab appears to be closed"). Short GENLAYER_E2E_* timeout overrides keep these in seconds.

Staking stub

e2e/fixtures/StakingStub.sol (+ committed compiled StakingStub.json) is a ~10-line recording stub: validatorJoin() / validatorJoin(address) are payable, record the call (callCount, lastOperator/Validator/Amount), and emit the ValidatorJoin(operator, validator, amount) event the CLI decodes. It proves the sign→broadcast→receipt loop only — no consensus.

Deferred (nightly follow-up)

  • Lane B (S3) — IC deploy on Docker localnet: describe.skip, guarded by GENLAYER_E2E_LOCALNET, documented in e2e/lane-b-deploy.e2e.ts. Bare anvil has no GenVM/consensus.
  • Tier 3 (Synpress / real MetaMask) — out of scope.

Prod-safe support changes

  • sessionConstants.tsGENLAYER_E2E_{LONG_POLL,HEARTBEAT_DEAD,CONNECT_TIMEOUT}_MS overrides; unset/invalid → production defaults unchanged.
  • browserBridge.ts — skip openUrl when GENLAYER_E2E_NO_OPEN is set (harness drives its own chromium); production unaffected.

CI

New .github/workflows/e2e-wallet.yml (distinct name from the synced e2e.yml): ubuntu + node 20 → npm ci → build → foundry-toolchain → playwright install --with-deps chromiumnpm run test:e2e. Informational — keep off required checks until stable, then promote.

How to run

npm run build # specs spawn dist/index.js
npx playwright install chromium
npm run test:e2e

Verification

  • npm run test:e2e9 passed, 1 skipped (Lane B), stable across 3 runs.
  • npm run test:coverage (Tier-1 vitest) → 733 passed; Playwright specs excluded from the vitest glob.
  • npm run build clean; tsc --noEmit = 32 errors (pre-existing baseline, zero new).

…on anvil)
Add a headless, deterministic Tier-2 e2e harness that drives the real
bridge page (bridgePage.ts) in chromium with an injected mock
window.ethereum. eth_sendTransaction is delegated to Node via
page.exposeFunction(__glSign), where a viem local account (anvil dev
key) actually signs+broadcasts to an ephemeral anvil, so the full loop
runs for real: bridge + page JS + session daemon + chain, with zero
human/extension.
Lanes (all green on anvil):
- S1 connect/status/disconnect (descriptor 0600, /api/ping, teardown)
- S2 validator-join --wallet browser signs + mines against a recording
StakingStub, asserted via real receipt + on-chain callCount
- S4 session reuse: two sequential joins over one daemon/tab
- S5 config default walletMode=browser; --wallet keystore overrides
- S6 user-reject (4001) + tab-closed fail-fast (short env timeouts)
Lane B (S3, IC deploy on Docker localnet) is deferred as a nightly
follow-up: describe.skip guarded by GENLAYER_E2E_LOCALNET.
Prod-safe support changes: GENLAYER_E2E_* env overrides for the timing
constants (unset in prod) and a GENLAYER_E2E_NO_OPEN guard so the daemon
does not auto-open a system browser under test. New e2e-wallet.yml CI job
(informational, not a required gate initially). Playwright specs are kept
out of the vitest glob so test:coverage is unaffected.
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 31e970ee-5801-430d-8c8d-58cc5c61255f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cli-wallet-e2e

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@MuncleUscles
MuncleUscles merged commit 4b26cc6 into v0.40-devJul 9, 2026
10 of 11 checks passed
@MuncleUscles
MuncleUscles deleted the feat/cli-wallet-e2e branch July 9, 2026 20:01
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@MuncleUscles
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(wallet): Tier-2 browser-signing e2e (Playwright + mock provider on anvil) - #380

Merged
MuncleUscles merged 1 commit into
v0.40-devfrom
feat/cli-wallet-e2e
Jul 9, 2026
Merged

test(wallet): Tier-2 browser-signing e2e (Playwright + mock provider on anvil)#380
MuncleUscles merged 1 commit into
v0.40-devfrom
feat/cli-wallet-e2e

Conversation

@MuncleUscles

Copy link
Copy Markdown
Member

What

Adds the Tier-2 browser-wallet signing e2e harness for genlayer-cli, per the design doc. It drives the real daemon bridge page (src/lib/wallet/bridgePage.ts) in a headless chromium with an injected mock window.ethereum, and delegates signing to Node via page.exposeFunction("__glSign", …) where a viem local account (anvil dev key) actually signs + broadcasts eth_sendTransaction to an ephemeral anvil, returning a real tx hash.

So the full loop runs for real — bridge + served page JS + session daemon + real chain — with zero human and zero MetaMask.

How the mock works

  • e2e/fixtures/mockProvider.tsinstallMockProvider({address, chainIdHex, behavior}) returns an init-script string installed via page.addInitScript(...)beforepage.goto() (beats the injection race). Behaviors: approve | reject (4001) | wrong-network (4901). eth_sendTransactionwindow.__glSign.
  • e2e/helpers/bridgePage.ts — launches chromium, wires __glSign to a viem sendTransaction on anvil, injects the mock, and drives connect.
  • e2e/fixtures/chain.tsstartAnvil() boots anvil on a random port and deploys the recording StakingStub.
  • e2e/fixtures/cli.ts — runs dist/index.js under a scratch HOME with a seeded custom-network config (chain-id / rpc / staking → the anvil + stub) so ensureChain() matches; spawnConnect() scrapes the printed session URL.

Lanes (all green on anvil, ~16s, serial workers:1)

  • S1 connect/status/disconnect — descriptor 0600{pid live, port, token, address, chainId}, /api/ping 200, "Connected as …", teardown removes descriptor + pid.
  • S2staking validator-join --wallet browser — page auto-signs → asserted via a real receipt + on-chain stub callCount.
  • S4 session reuse — two sequential joins over one daemon/tab, distinct hashes, same pid.
  • S5 config default walletMode=browser — bare validator-join signs via session; --wallet keystore overrides (keystore path, no enqueue).
  • S6 user-reject (4001 → "Transaction rejected in wallet", session survives) + tab-closed (page.close() → stale heartbeat → fail-fast "tab appears to be closed"). Short GENLAYER_E2E_* timeout overrides keep these in seconds.

Staking stub

e2e/fixtures/StakingStub.sol (+ committed compiled StakingStub.json) is a ~10-line recording stub: validatorJoin() / validatorJoin(address) are payable, record the call (callCount, lastOperator/Validator/Amount), and emit the ValidatorJoin(operator, validator, amount) event the CLI decodes. It proves the sign→broadcast→receipt loop only — no consensus.

Deferred (nightly follow-up)

  • Lane B (S3) — IC deploy on Docker localnet: describe.skip, guarded by GENLAYER_E2E_LOCALNET, documented in e2e/lane-b-deploy.e2e.ts. Bare anvil has no GenVM/consensus.
  • Tier 3 (Synpress / real MetaMask) — out of scope.

Prod-safe support changes

  • sessionConstants.tsGENLAYER_E2E_{LONG_POLL,HEARTBEAT_DEAD,CONNECT_TIMEOUT}_MS overrides; unset/invalid → production defaults unchanged.
  • browserBridge.ts — skip openUrl when GENLAYER_E2E_NO_OPEN is set (harness drives its own chromium); production unaffected.

CI

New .github/workflows/e2e-wallet.yml (distinct name from the synced e2e.yml): ubuntu + node 20 → npm ci → build → foundry-toolchain → playwright install --with-deps chromiumnpm run test:e2e. Informational — keep off required checks until stable, then promote.

How to run

npm run build # specs spawn dist/index.js
npx playwright install chromium
npm run test:e2e

Verification

  • npm run test:e2e9 passed, 1 skipped (Lane B), stable across 3 runs.
  • npm run test:coverage (Tier-1 vitest) → 733 passed; Playwright specs excluded from the vitest glob.
  • npm run build clean; tsc --noEmit = 32 errors (pre-existing baseline, zero new).

…on anvil)
Add a headless, deterministic Tier-2 e2e harness that drives the real
bridge page (bridgePage.ts) in chromium with an injected mock
window.ethereum. eth_sendTransaction is delegated to Node via
page.exposeFunction(__glSign), where a viem local account (anvil dev
key) actually signs+broadcasts to an ephemeral anvil, so the full loop
runs for real: bridge + page JS + session daemon + chain, with zero
human/extension.
Lanes (all green on anvil):
- S1 connect/status/disconnect (descriptor 0600, /api/ping, teardown)
- S2 validator-join --wallet browser signs + mines against a recording
StakingStub, asserted via real receipt + on-chain callCount
- S4 session reuse: two sequential joins over one daemon/tab
- S5 config default walletMode=browser; --wallet keystore overrides
- S6 user-reject (4001) + tab-closed fail-fast (short env timeouts)
Lane B (S3, IC deploy on Docker localnet) is deferred as a nightly
follow-up: describe.skip guarded by GENLAYER_E2E_LOCALNET.
Prod-safe support changes: GENLAYER_E2E_* env overrides for the timing
constants (unset in prod) and a GENLAYER_E2E_NO_OPEN guard so the daemon
does not auto-open a system browser under test. New e2e-wallet.yml CI job
(informational, not a required gate initially). Playwright specs are kept
out of the vitest glob so test:coverage is unaffected.
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 31e970ee-5801-430d-8c8d-58cc5c61255f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cli-wallet-e2e

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@MuncleUscles
MuncleUscles merged commit 4b26cc6 into v0.40-devJul 9, 2026
10 of 11 checks passed
@MuncleUscles
MuncleUscles deleted the feat/cli-wallet-e2e branch July 9, 2026 20:01
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@MuncleUscles
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

test(wallet): Tier-2 browser-signing e2e (Playwright + mock provider on anvil) - #380

Merged
MuncleUscles merged 1 commit into
v0.40-devfrom
feat/cli-wallet-e2e
Jul 9, 2026
Merged

test(wallet): Tier-2 browser-signing e2e (Playwright + mock provider on anvil)#380
MuncleUscles merged 1 commit into
v0.40-devfrom
feat/cli-wallet-e2e

Conversation

@MuncleUscles

Copy link
Copy Markdown
Member

What

Adds the Tier-2 browser-wallet signing e2e harness for genlayer-cli, per the design doc. It drives the real daemon bridge page (src/lib/wallet/bridgePage.ts) in a headless chromium with an injected mock window.ethereum, and delegates signing to Node via page.exposeFunction("__glSign", …) where a viem local account (anvil dev key) actually signs + broadcasts eth_sendTransaction to an ephemeral anvil, returning a real tx hash.

So the full loop runs for real — bridge + served page JS + session daemon + real chain — with zero human and zero MetaMask.

How the mock works

  • e2e/fixtures/mockProvider.tsinstallMockProvider({address, chainIdHex, behavior}) returns an init-script string installed via page.addInitScript(...)beforepage.goto() (beats the injection race). Behaviors: approve | reject (4001) | wrong-network (4901). eth_sendTransactionwindow.__glSign.
  • e2e/helpers/bridgePage.ts — launches chromium, wires __glSign to a viem sendTransaction on anvil, injects the mock, and drives connect.
  • e2e/fixtures/chain.tsstartAnvil() boots anvil on a random port and deploys the recording StakingStub.
  • e2e/fixtures/cli.ts — runs dist/index.js under a scratch HOME with a seeded custom-network config (chain-id / rpc / staking → the anvil + stub) so ensureChain() matches; spawnConnect() scrapes the printed session URL.

Lanes (all green on anvil, ~16s, serial workers:1)

  • S1 connect/status/disconnect — descriptor 0600{pid live, port, token, address, chainId}, /api/ping 200, "Connected as …", teardown removes descriptor + pid.
  • S2staking validator-join --wallet browser — page auto-signs → asserted via a real receipt + on-chain stub callCount.
  • S4 session reuse — two sequential joins over one daemon/tab, distinct hashes, same pid.
  • S5 config default walletMode=browser — bare validator-join signs via session; --wallet keystore overrides (keystore path, no enqueue).
  • S6 user-reject (4001 → "Transaction rejected in wallet", session survives) + tab-closed (page.close() → stale heartbeat → fail-fast "tab appears to be closed"). Short GENLAYER_E2E_* timeout overrides keep these in seconds.

Staking stub

e2e/fixtures/StakingStub.sol (+ committed compiled StakingStub.json) is a ~10-line recording stub: validatorJoin() / validatorJoin(address) are payable, record the call (callCount, lastOperator/Validator/Amount), and emit the ValidatorJoin(operator, validator, amount) event the CLI decodes. It proves the sign→broadcast→receipt loop only — no consensus.

Deferred (nightly follow-up)

  • Lane B (S3) — IC deploy on Docker localnet: describe.skip, guarded by GENLAYER_E2E_LOCALNET, documented in e2e/lane-b-deploy.e2e.ts. Bare anvil has no GenVM/consensus.
  • Tier 3 (Synpress / real MetaMask) — out of scope.

Prod-safe support changes

  • sessionConstants.tsGENLAYER_E2E_{LONG_POLL,HEARTBEAT_DEAD,CONNECT_TIMEOUT}_MS overrides; unset/invalid → production defaults unchanged.
  • browserBridge.ts — skip openUrl when GENLAYER_E2E_NO_OPEN is set (harness drives its own chromium); production unaffected.

CI

New .github/workflows/e2e-wallet.yml (distinct name from the synced e2e.yml): ubuntu + node 20 → npm ci → build → foundry-toolchain → playwright install --with-deps chromiumnpm run test:e2e. Informational — keep off required checks until stable, then promote.

How to run

npm run build # specs spawn dist/index.js
npx playwright install chromium
npm run test:e2e

Verification

  • npm run test:e2e9 passed, 1 skipped (Lane B), stable across 3 runs.
  • npm run test:coverage (Tier-1 vitest) → 733 passed; Playwright specs excluded from the vitest glob.
  • npm run build clean; tsc --noEmit = 32 errors (pre-existing baseline, zero new).

…on anvil)
Add a headless, deterministic Tier-2 e2e harness that drives the real
bridge page (bridgePage.ts) in chromium with an injected mock
window.ethereum. eth_sendTransaction is delegated to Node via
page.exposeFunction(__glSign), where a viem local account (anvil dev
key) actually signs+broadcasts to an ephemeral anvil, so the full loop
runs for real: bridge + page JS + session daemon + chain, with zero
human/extension.
Lanes (all green on anvil):
- S1 connect/status/disconnect (descriptor 0600, /api/ping, teardown)
- S2 validator-join --wallet browser signs + mines against a recording
StakingStub, asserted via real receipt + on-chain callCount
- S4 session reuse: two sequential joins over one daemon/tab
- S5 config default walletMode=browser; --wallet keystore overrides
- S6 user-reject (4001) + tab-closed fail-fast (short env timeouts)
Lane B (S3, IC deploy on Docker localnet) is deferred as a nightly
follow-up: describe.skip guarded by GENLAYER_E2E_LOCALNET.
Prod-safe support changes: GENLAYER_E2E_* env overrides for the timing
constants (unset in prod) and a GENLAYER_E2E_NO_OPEN guard so the daemon
does not auto-open a system browser under test. New e2e-wallet.yml CI job
(informational, not a required gate initially). Playwright specs are kept
out of the vitest glob so test:coverage is unaffected.
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 31e970ee-5801-430d-8c8d-58cc5c61255f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cli-wallet-e2e

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@MuncleUscles
MuncleUscles merged commit 4b26cc6 into v0.40-devJul 9, 2026
10 of 11 checks passed
@MuncleUscles
MuncleUscles deleted the feat/cli-wallet-e2e branch July 9, 2026 20:01
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@MuncleUscles
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(wallet): Tier-2 browser-signing e2e (Playwright + mock provider on anvil) - #380

Merged
MuncleUscles merged 1 commit into
v0.40-devfrom
feat/cli-wallet-e2e
Jul 9, 2026
Merged

test(wallet): Tier-2 browser-signing e2e (Playwright + mock provider on anvil)#380
MuncleUscles merged 1 commit into
v0.40-devfrom
feat/cli-wallet-e2e

Conversation

@MuncleUscles

Copy link
Copy Markdown
Member

What

Adds the Tier-2 browser-wallet signing e2e harness for genlayer-cli, per the design doc. It drives the real daemon bridge page (src/lib/wallet/bridgePage.ts) in a headless chromium with an injected mock window.ethereum, and delegates signing to Node via page.exposeFunction("__glSign", …) where a viem local account (anvil dev key) actually signs + broadcasts eth_sendTransaction to an ephemeral anvil, returning a real tx hash.

So the full loop runs for real — bridge + served page JS + session daemon + real chain — with zero human and zero MetaMask.

How the mock works

  • e2e/fixtures/mockProvider.tsinstallMockProvider({address, chainIdHex, behavior}) returns an init-script string installed via page.addInitScript(...)beforepage.goto() (beats the injection race). Behaviors: approve | reject (4001) | wrong-network (4901). eth_sendTransactionwindow.__glSign.
  • e2e/helpers/bridgePage.ts — launches chromium, wires __glSign to a viem sendTransaction on anvil, injects the mock, and drives connect.
  • e2e/fixtures/chain.tsstartAnvil() boots anvil on a random port and deploys the recording StakingStub.
  • e2e/fixtures/cli.ts — runs dist/index.js under a scratch HOME with a seeded custom-network config (chain-id / rpc / staking → the anvil + stub) so ensureChain() matches; spawnConnect() scrapes the printed session URL.

Lanes (all green on anvil, ~16s, serial workers:1)

  • S1 connect/status/disconnect — descriptor 0600{pid live, port, token, address, chainId}, /api/ping 200, "Connected as …", teardown removes descriptor + pid.
  • S2staking validator-join --wallet browser — page auto-signs → asserted via a real receipt + on-chain stub callCount.
  • S4 session reuse — two sequential joins over one daemon/tab, distinct hashes, same pid.
  • S5 config default walletMode=browser — bare validator-join signs via session; --wallet keystore overrides (keystore path, no enqueue).
  • S6 user-reject (4001 → "Transaction rejected in wallet", session survives) + tab-closed (page.close() → stale heartbeat → fail-fast "tab appears to be closed"). Short GENLAYER_E2E_* timeout overrides keep these in seconds.

Staking stub

e2e/fixtures/StakingStub.sol (+ committed compiled StakingStub.json) is a ~10-line recording stub: validatorJoin() / validatorJoin(address) are payable, record the call (callCount, lastOperator/Validator/Amount), and emit the ValidatorJoin(operator, validator, amount) event the CLI decodes. It proves the sign→broadcast→receipt loop only — no consensus.

Deferred (nightly follow-up)

  • Lane B (S3) — IC deploy on Docker localnet: describe.skip, guarded by GENLAYER_E2E_LOCALNET, documented in e2e/lane-b-deploy.e2e.ts. Bare anvil has no GenVM/consensus.
  • Tier 3 (Synpress / real MetaMask) — out of scope.

Prod-safe support changes

  • sessionConstants.tsGENLAYER_E2E_{LONG_POLL,HEARTBEAT_DEAD,CONNECT_TIMEOUT}_MS overrides; unset/invalid → production defaults unchanged.
  • browserBridge.ts — skip openUrl when GENLAYER_E2E_NO_OPEN is set (harness drives its own chromium); production unaffected.

CI

New .github/workflows/e2e-wallet.yml (distinct name from the synced e2e.yml): ubuntu + node 20 → npm ci → build → foundry-toolchain → playwright install --with-deps chromiumnpm run test:e2e. Informational — keep off required checks until stable, then promote.

How to run

npm run build # specs spawn dist/index.js
npx playwright install chromium
npm run test:e2e

Verification

  • npm run test:e2e9 passed, 1 skipped (Lane B), stable across 3 runs.
  • npm run test:coverage (Tier-1 vitest) → 733 passed; Playwright specs excluded from the vitest glob.
  • npm run build clean; tsc --noEmit = 32 errors (pre-existing baseline, zero new).

…on anvil)
Add a headless, deterministic Tier-2 e2e harness that drives the real
bridge page (bridgePage.ts) in chromium with an injected mock
window.ethereum. eth_sendTransaction is delegated to Node via
page.exposeFunction(__glSign), where a viem local account (anvil dev
key) actually signs+broadcasts to an ephemeral anvil, so the full loop
runs for real: bridge + page JS + session daemon + chain, with zero
human/extension.
Lanes (all green on anvil):
- S1 connect/status/disconnect (descriptor 0600, /api/ping, teardown)
- S2 validator-join --wallet browser signs + mines against a recording
StakingStub, asserted via real receipt + on-chain callCount
- S4 session reuse: two sequential joins over one daemon/tab
- S5 config default walletMode=browser; --wallet keystore overrides
- S6 user-reject (4001) + tab-closed fail-fast (short env timeouts)
Lane B (S3, IC deploy on Docker localnet) is deferred as a nightly
follow-up: describe.skip guarded by GENLAYER_E2E_LOCALNET.
Prod-safe support changes: GENLAYER_E2E_* env overrides for the timing
constants (unset in prod) and a GENLAYER_E2E_NO_OPEN guard so the daemon
does not auto-open a system browser under test. New e2e-wallet.yml CI job
(informational, not a required gate initially). Playwright specs are kept
out of the vitest glob so test:coverage is unaffected.
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 31e970ee-5801-430d-8c8d-58cc5c61255f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cli-wallet-e2e

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@MuncleUscles
MuncleUscles merged commit 4b26cc6 into v0.40-devJul 9, 2026
10 of 11 checks passed
@MuncleUscles
MuncleUscles deleted the feat/cli-wallet-e2e branch July 9, 2026 20:01
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@MuncleUscles
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(wallet): Tier-2 browser-signing e2e (Playwright + mock provider on anvil) - #380

Merged
MuncleUscles merged 1 commit into
v0.40-devfrom
feat/cli-wallet-e2e
Jul 9, 2026
Merged

test(wallet): Tier-2 browser-signing e2e (Playwright + mock provider on anvil)#380
MuncleUscles merged 1 commit into
v0.40-devfrom
feat/cli-wallet-e2e

Conversation

@MuncleUscles

Copy link
Copy Markdown
Member

What

Adds the Tier-2 browser-wallet signing e2e harness for genlayer-cli, per the design doc. It drives the real daemon bridge page (src/lib/wallet/bridgePage.ts) in a headless chromium with an injected mock window.ethereum, and delegates signing to Node via page.exposeFunction("__glSign", …) where a viem local account (anvil dev key) actually signs + broadcasts eth_sendTransaction to an ephemeral anvil, returning a real tx hash.

So the full loop runs for real — bridge + served page JS + session daemon + real chain — with zero human and zero MetaMask.

How the mock works

  • e2e/fixtures/mockProvider.tsinstallMockProvider({address, chainIdHex, behavior}) returns an init-script string installed via page.addInitScript(...)beforepage.goto() (beats the injection race). Behaviors: approve | reject (4001) | wrong-network (4901). eth_sendTransactionwindow.__glSign.
  • e2e/helpers/bridgePage.ts — launches chromium, wires __glSign to a viem sendTransaction on anvil, injects the mock, and drives connect.
  • e2e/fixtures/chain.tsstartAnvil() boots anvil on a random port and deploys the recording StakingStub.
  • e2e/fixtures/cli.ts — runs dist/index.js under a scratch HOME with a seeded custom-network config (chain-id / rpc / staking → the anvil + stub) so ensureChain() matches; spawnConnect() scrapes the printed session URL.

Lanes (all green on anvil, ~16s, serial workers:1)

  • S1 connect/status/disconnect — descriptor 0600{pid live, port, token, address, chainId}, /api/ping 200, "Connected as …", teardown removes descriptor + pid.
  • S2staking validator-join --wallet browser — page auto-signs → asserted via a real receipt + on-chain stub callCount.
  • S4 session reuse — two sequential joins over one daemon/tab, distinct hashes, same pid.
  • S5 config default walletMode=browser — bare validator-join signs via session; --wallet keystore overrides (keystore path, no enqueue).
  • S6 user-reject (4001 → "Transaction rejected in wallet", session survives) + tab-closed (page.close() → stale heartbeat → fail-fast "tab appears to be closed"). Short GENLAYER_E2E_* timeout overrides keep these in seconds.

Staking stub

e2e/fixtures/StakingStub.sol (+ committed compiled StakingStub.json) is a ~10-line recording stub: validatorJoin() / validatorJoin(address) are payable, record the call (callCount, lastOperator/Validator/Amount), and emit the ValidatorJoin(operator, validator, amount) event the CLI decodes. It proves the sign→broadcast→receipt loop only — no consensus.

Deferred (nightly follow-up)

  • Lane B (S3) — IC deploy on Docker localnet: describe.skip, guarded by GENLAYER_E2E_LOCALNET, documented in e2e/lane-b-deploy.e2e.ts. Bare anvil has no GenVM/consensus.
  • Tier 3 (Synpress / real MetaMask) — out of scope.

Prod-safe support changes

  • sessionConstants.tsGENLAYER_E2E_{LONG_POLL,HEARTBEAT_DEAD,CONNECT_TIMEOUT}_MS overrides; unset/invalid → production defaults unchanged.
  • browserBridge.ts — skip openUrl when GENLAYER_E2E_NO_OPEN is set (harness drives its own chromium); production unaffected.

CI

New .github/workflows/e2e-wallet.yml (distinct name from the synced e2e.yml): ubuntu + node 20 → npm ci → build → foundry-toolchain → playwright install --with-deps chromiumnpm run test:e2e. Informational — keep off required checks until stable, then promote.

How to run

npm run build # specs spawn dist/index.js
npx playwright install chromium
npm run test:e2e

Verification

  • npm run test:e2e9 passed, 1 skipped (Lane B), stable across 3 runs.
  • npm run test:coverage (Tier-1 vitest) → 733 passed; Playwright specs excluded from the vitest glob.
  • npm run build clean; tsc --noEmit = 32 errors (pre-existing baseline, zero new).

…on anvil)
Add a headless, deterministic Tier-2 e2e harness that drives the real
bridge page (bridgePage.ts) in chromium with an injected mock
window.ethereum. eth_sendTransaction is delegated to Node via
page.exposeFunction(__glSign), where a viem local account (anvil dev
key) actually signs+broadcasts to an ephemeral anvil, so the full loop
runs for real: bridge + page JS + session daemon + chain, with zero
human/extension.
Lanes (all green on anvil):
- S1 connect/status/disconnect (descriptor 0600, /api/ping, teardown)
- S2 validator-join --wallet browser signs + mines against a recording
StakingStub, asserted via real receipt + on-chain callCount
- S4 session reuse: two sequential joins over one daemon/tab
- S5 config default walletMode=browser; --wallet keystore overrides
- S6 user-reject (4001) + tab-closed fail-fast (short env timeouts)
Lane B (S3, IC deploy on Docker localnet) is deferred as a nightly
follow-up: describe.skip guarded by GENLAYER_E2E_LOCALNET.
Prod-safe support changes: GENLAYER_E2E_* env overrides for the timing
constants (unset in prod) and a GENLAYER_E2E_NO_OPEN guard so the daemon
does not auto-open a system browser under test. New e2e-wallet.yml CI job
(informational, not a required gate initially). Playwright specs are kept
out of the vitest glob so test:coverage is unaffected.
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 31e970ee-5801-430d-8c8d-58cc5c61255f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cli-wallet-e2e

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@MuncleUscles
MuncleUscles merged commit 4b26cc6 into v0.40-devJul 9, 2026
10 of 11 checks passed
@MuncleUscles
MuncleUscles deleted the feat/cli-wallet-e2e branch July 9, 2026 20:01
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@MuncleUscles
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

test(wallet): Tier-2 browser-signing e2e (Playwright + mock provider on anvil) - #380

Merged
MuncleUscles merged 1 commit into
v0.40-devfrom
feat/cli-wallet-e2e
Jul 9, 2026
Merged

test(wallet): Tier-2 browser-signing e2e (Playwright + mock provider on anvil)#380
MuncleUscles merged 1 commit into
v0.40-devfrom
feat/cli-wallet-e2e

Conversation

@MuncleUscles

Copy link
Copy Markdown
Member

What

Adds the Tier-2 browser-wallet signing e2e harness for genlayer-cli, per the design doc. It drives the real daemon bridge page (src/lib/wallet/bridgePage.ts) in a headless chromium with an injected mock window.ethereum, and delegates signing to Node via page.exposeFunction("__glSign", …) where a viem local account (anvil dev key) actually signs + broadcasts eth_sendTransaction to an ephemeral anvil, returning a real tx hash.

So the full loop runs for real — bridge + served page JS + session daemon + real chain — with zero human and zero MetaMask.

How the mock works

  • e2e/fixtures/mockProvider.tsinstallMockProvider({address, chainIdHex, behavior}) returns an init-script string installed via page.addInitScript(...)beforepage.goto() (beats the injection race). Behaviors: approve | reject (4001) | wrong-network (4901). eth_sendTransactionwindow.__glSign.
  • e2e/helpers/bridgePage.ts — launches chromium, wires __glSign to a viem sendTransaction on anvil, injects the mock, and drives connect.
  • e2e/fixtures/chain.tsstartAnvil() boots anvil on a random port and deploys the recording StakingStub.
  • e2e/fixtures/cli.ts — runs dist/index.js under a scratch HOME with a seeded custom-network config (chain-id / rpc / staking → the anvil + stub) so ensureChain() matches; spawnConnect() scrapes the printed session URL.

Lanes (all green on anvil, ~16s, serial workers:1)

  • S1 connect/status/disconnect — descriptor 0600{pid live, port, token, address, chainId}, /api/ping 200, "Connected as …", teardown removes descriptor + pid.
  • S2staking validator-join --wallet browser — page auto-signs → asserted via a real receipt + on-chain stub callCount.
  • S4 session reuse — two sequential joins over one daemon/tab, distinct hashes, same pid.
  • S5 config default walletMode=browser — bare validator-join signs via session; --wallet keystore overrides (keystore path, no enqueue).
  • S6 user-reject (4001 → "Transaction rejected in wallet", session survives) + tab-closed (page.close() → stale heartbeat → fail-fast "tab appears to be closed"). Short GENLAYER_E2E_* timeout overrides keep these in seconds.

Staking stub

e2e/fixtures/StakingStub.sol (+ committed compiled StakingStub.json) is a ~10-line recording stub: validatorJoin() / validatorJoin(address) are payable, record the call (callCount, lastOperator/Validator/Amount), and emit the ValidatorJoin(operator, validator, amount) event the CLI decodes. It proves the sign→broadcast→receipt loop only — no consensus.

Deferred (nightly follow-up)

  • Lane B (S3) — IC deploy on Docker localnet: describe.skip, guarded by GENLAYER_E2E_LOCALNET, documented in e2e/lane-b-deploy.e2e.ts. Bare anvil has no GenVM/consensus.
  • Tier 3 (Synpress / real MetaMask) — out of scope.

Prod-safe support changes

  • sessionConstants.tsGENLAYER_E2E_{LONG_POLL,HEARTBEAT_DEAD,CONNECT_TIMEOUT}_MS overrides; unset/invalid → production defaults unchanged.
  • browserBridge.ts — skip openUrl when GENLAYER_E2E_NO_OPEN is set (harness drives its own chromium); production unaffected.

CI

New .github/workflows/e2e-wallet.yml (distinct name from the synced e2e.yml): ubuntu + node 20 → npm ci → build → foundry-toolchain → playwright install --with-deps chromiumnpm run test:e2e. Informational — keep off required checks until stable, then promote.

How to run

npm run build # specs spawn dist/index.js
npx playwright install chromium
npm run test:e2e

Verification

  • npm run test:e2e9 passed, 1 skipped (Lane B), stable across 3 runs.
  • npm run test:coverage (Tier-1 vitest) → 733 passed; Playwright specs excluded from the vitest glob.
  • npm run build clean; tsc --noEmit = 32 errors (pre-existing baseline, zero new).

…on anvil)
Add a headless, deterministic Tier-2 e2e harness that drives the real
bridge page (bridgePage.ts) in chromium with an injected mock
window.ethereum. eth_sendTransaction is delegated to Node via
page.exposeFunction(__glSign), where a viem local account (anvil dev
key) actually signs+broadcasts to an ephemeral anvil, so the full loop
runs for real: bridge + page JS + session daemon + chain, with zero
human/extension.
Lanes (all green on anvil):
- S1 connect/status/disconnect (descriptor 0600, /api/ping, teardown)
- S2 validator-join --wallet browser signs + mines against a recording
StakingStub, asserted via real receipt + on-chain callCount
- S4 session reuse: two sequential joins over one daemon/tab
- S5 config default walletMode=browser; --wallet keystore overrides
- S6 user-reject (4001) + tab-closed fail-fast (short env timeouts)
Lane B (S3, IC deploy on Docker localnet) is deferred as a nightly
follow-up: describe.skip guarded by GENLAYER_E2E_LOCALNET.
Prod-safe support changes: GENLAYER_E2E_* env overrides for the timing
constants (unset in prod) and a GENLAYER_E2E_NO_OPEN guard so the daemon
does not auto-open a system browser under test. New e2e-wallet.yml CI job
(informational, not a required gate initially). Playwright specs are kept
out of the vitest glob so test:coverage is unaffected.
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 31e970ee-5801-430d-8c8d-58cc5c61255f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cli-wallet-e2e

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@MuncleUscles
MuncleUscles merged commit 4b26cc6 into v0.40-devJul 9, 2026
10 of 11 checks passed
@MuncleUscles
MuncleUscles deleted the feat/cli-wallet-e2e branch July 9, 2026 20:01
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@MuncleUscles