|
| 1 | +--- |
| 2 | +name: stark-cdp-with-wallet |
| 3 | +description: Use when a task needs a real Braavos wallet driven end-to-end (unlock, connect, approve a transaction) against a live network like Starknet Sepolia, without a human clicking the extension popup. Triggers on requests to automate Braavos, drive a real wallet transaction, or record a browser demo that needs a connected wallet. |
| 4 | +--- |
| 5 | + |
| 6 | +# stark-cdp-with-wallet |
| 7 | + |
| 8 | +## Overview |
| 9 | + |
| 10 | +Proving the frontend half of a real Sepolia deploy needs an actual Braavos |
| 11 | +wallet extension connected and signing — `stark-smoke-test`'s devnet gate |
| 12 | +only exercises the burner wallet, never a real extension. This skill closes |
| 13 | +that gap with raw Chrome DevTools Protocol over WebSocket, **zero npm |
| 14 | +dependencies**, matching the in-repo precedent |
| 15 | +`.claude/skills/stark-smoke-test/stark-smoke-test-browser.mjs` (same CDP |
| 16 | +client shape, same `waitFor`/`openTab`/`connectSession` idiom). |
| 17 | + |
| 18 | +This is an extraction, not a rewrite: every function here used to live |
| 19 | +inline in `stark-demo-web.mjs` (formerly `.claude/workflows/`). Splitting it |
| 20 | +out mirrors scaffold-stylus's `cdp-with-wallet` skill |
| 21 | +(`.claude/skills/cdp-with-wallet/` at `origin/release/v0.2.0` in |
| 22 | +scaffold-stylus) — same shape (`launch.mjs` for Chrome/profile management, |
| 23 | +a wallet-primitives file for unlock/connect/approve), adapted for Braavos |
| 24 | +instead of MetaMask. The blueprint technique for both is an internal |
| 25 | +Playwright e2e helper's `braavos.ts`: enumerate CDP targets from |
| 26 | +`GET http://localhost:<port>/json`, find the extension's own page by URL |
| 27 | +prefix, attach a WebSocket directly to it. Only the *technique* carries |
| 28 | +over — Braavos and MetaMask have different selectors, different popup |
| 29 | +shapes (a side-panel singleton vs. a fresh `notification.html` popup per |
| 30 | +request), and different discovery protocols (Wallet Standard vs. EIP-6963). |
| 31 | + |
| 32 | +## Files |
| 33 | + |
| 34 | +``` |
| 35 | +.claude/skills/stark-cdp-with-wallet/ |
| 36 | + SKILL.md this file |
| 37 | + launch.mjs Chrome launcher against the PERSISTENT debug profile |
| 38 | + (Braavos already installed there), CDP port/target helpers, |
| 39 | + profile-lock detection |
| 40 | + braavos.mjs Keychain password read, wallet setup-state check, unlock(), |
| 41 | + and approveBraavosRequest() (connect + tx-confirm — Braavos |
| 42 | + renders both through the same side-panel component) |
| 43 | +``` |
| 44 | + |
| 45 | +`stark-demo-web.mjs` (`.claude/skills/stark-demo-video/`) is the only current |
| 46 | +caller — it imports these primitives and supplies the app-specific parts |
| 47 | +(clicking this app's own "Connect Wallet" button, reading `deployedContracts.ts`, |
| 48 | +screencast/MP4 assembly). That split follows the same boundary as |
| 49 | +scaffold-stylus's `cdp-with-wallet` vs. `demo-video`: this skill only knows |
| 50 | +how to drive Braavos's own extension pages; it has no opinion about the |
| 51 | +dapp under test. |
| 52 | + |
| 53 | +## Can-do / cannot-do |
| 54 | + |
| 55 | +| Can do | Cannot do | |
| 56 | +|---|---| |
| 57 | +| Unlock an already-initialised Braavos vault given its password, via CDP `Input.insertText` (never the page's own `HTMLInputElement` value setter, which some wallet UIs scuttle) | Create a wallet, import a seed phrase, or otherwise initialise a vault — by design, not a limitation | |
| 58 | +| Distinguish "not set up" from "locked"/"unlocked" from real `chrome.storage.local` evidence (`checkWalletSetupState`), not from the popup's password-field signal alone (both states show no password field) | Guarantee any *other* extension's popups are automatable the same way — only this Braavos build's (4.19.6) side-panel shape has been reasoned about here | |
| 59 | +| Approve a connect or transaction-confirm request by finding and clicking its real button via CDP DOM search — structural queries first (locale-proof), text-word fallback only once structural search is measured to find nothing | Read or validate what a transaction actually does before approving it — `approveBraavosRequest` clicks the primary action, it does not decide whether confirming is safe. Isolation (testnet-only funds) is the actual safety control | |
| 60 | +| Recover from the side panel losing its pending-request state mid-unlock (`lostContext`) so the caller can retry the click that re-triggers a fresh request | Assume Braavos's side panel is a fresh popup per request — it's a **singleton** target reused across requests; matching "any new target" misses this and was a real bug caught before shipping | |
| 61 | + |
| 62 | +## Secret handling |
| 63 | + |
| 64 | +```bash |
| 65 | +security find-generic-password -s scaffold-stark-braavos -w |
| 66 | +``` |
| 67 | + |
| 68 | +`readBraavosPasswordFromKeychain()` reads this and only this — never an env |
| 69 | +var, never logged, never returned in a caller's report detail. Finding an |
| 70 | +entry is **not** the same as the password being correct: the entry can hold |
| 71 | +a stale value from before the wallet's password last changed. Callers must |
| 72 | +always follow this with a real `attemptUnlock()`/`approveBraavosRequest()` |
| 73 | +call; never trust `found` alone as "password confirmed". |
| 74 | + |
| 75 | +The wallet being automated must hold **testnet funds only**. |
| 76 | +`approveBraavosRequest()` clicks the primary action; it does not read or |
| 77 | +understand what it's confirming. |
| 78 | + |
| 79 | +## Procedure |
| 80 | + |
| 81 | +```js |
| 82 | +import { CDP_PORT, PROFILE_DIR, launchPersistentChrome, killPersistentChrome } from "./launch.mjs"; |
| 83 | +import { readBraavosPasswordFromKeychain, checkWalletSetupState, attemptUnlock, waitForBraavosRequestTarget, approveBraavosRequest } from "./braavos.mjs"; |
| 84 | + |
| 85 | +const { password } = readBraavosPasswordFromKeychain(); |
| 86 | +const launch = await launchPersistentChrome(); |
| 87 | +try { |
| 88 | + const setup = await checkWalletSetupState(); |
| 89 | + if (!setup.vaultPresent) throw new Error("Braavos not set up on this profile"); |
| 90 | + |
| 91 | + const unlock = await attemptUnlock(password); |
| 92 | + // unlock.state is "UNLOCKED" | "NO_PASSWORD_FIELD" | "REJECTED" | "INSERT_FAILED" | "UNKNOWN" |
| 93 | + |
| 94 | + // Caller drives the dapp's own "Connect" button here (app-specific — see |
| 95 | + // stark-demo-web.mjs's connectBraavosWallet), then: |
| 96 | + const request = await waitForBraavosRequestTarget(8_000); |
| 97 | + if (request) { |
| 98 | + const approved = await approveBraavosRequest(request, password, logDir); |
| 99 | + // A 4th, optional argument (attachExtraNetworkCapture) lets the caller |
| 100 | + // attach its own app-specific Network-domain instrumentation to the |
| 101 | + // Braavos session — e.g. stark-demo-web.mjs's attachTxHashCapture — so |
| 102 | + // this module never depends back on its own caller. |
| 103 | + // approved.lostContext === true means the side panel reset mid-unlock — |
| 104 | + // retry the dapp-side click that re-triggers the request. |
| 105 | + } |
| 106 | +} finally { |
| 107 | + await killPersistentChrome(); |
| 108 | +} |
| 109 | +``` |
| 110 | + |
| 111 | +## Known traps (measured, not assumed) |
| 112 | + |
| 113 | +- **Braavos's side panel is a singleton**, reused across requests — its URL |
| 114 | + goes from `side-panel.html?nav={"path":"/dapp-request",...}` to bare |
| 115 | + `side-panel.html` after an in-panel unlock. `waitForBraavosRequestTarget` |
| 116 | + matches on URL path content, never on "any new target ID". |
| 117 | +- **The service-worker target can exist before `chrome.*` finishes |
| 118 | + bootstrapping.** `checkWalletSetupState` forces a fresh wake via the popup |
| 119 | + on every attempt (not just when no target is found) rather than trusting a |
| 120 | + possibly-still-starting worker reference. |
| 121 | +- **A blind last-candidate click is banned.** An earlier version clicked |
| 122 | + "whatever's last" by screen position and left a real tx request pending |
| 123 | + forever (neither confirmed nor rejected). `approveBraavosRequest` only |
| 124 | + clicks a candidate identified by text/role, and re-polls with a fresh DOM |
| 125 | + query if the click doesn't visibly change the panel. |
| 126 | +- **The tx-confirm Sign button stays disabled until fee estimation |
| 127 | + completes** — a loading shimmer can make the fee line read empty rather |
| 128 | + than "-", which a naive check reads as "settled". Only a fee line |
| 129 | + containing a digit counts; an explicit "Transaction execution error" fails |
| 130 | + fast instead of waiting out the poll. |
0 commit comments