test: drive the EIP-1193 dApp approval round trips in the browser (closes #183) #273
Reference in New Issue
Block a user
Delete Branch "test/issue-183-dapp-round-trips"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #183.
Test-only.
git diff origin/next --stattouchesREADME.md,TODO.md,tests/e2e/network.jsandtests/e2e/run.jsand nothing undersrc/.What this covers
Ten new tests, taking the Chrome e2e suite from 27 to 37. The route handler serves a local page on
https://dapp.e2e.test/;window.ethereumgets there from the shippedMAIN-world content script, and the page's own EIP-6963 announcement is required to be that object by identity, so nothing the fixture could have installed itself satisfies it.eth_chainIdis round-tripped through the content script and background before any prompt is driven.DAPP_HTMLintests/e2e/network.js, test 28eth_requestAccounts/personal_sign/eth_signTypedData_v4/eth_sendTransactionthrough real promptsSignatures are recovered in the runner with
verifyMessage,verifyTypedDataandTransaction.from(), against the active address read out of extension storage. The background verifies too; nothing here leans on its verdict. The transaction assertions run against the raw signed bytes captured ateth_sendRawTransaction— the stub records them and answers with the transaction's real hash, because ethers refuses a hash mismatch — and compare signer, recipient, value, call data, chain id, and that the hash the page received is the hash of those bytes. The rejected transaction is required to have put nothing on the wire at all.The password check wraps
chrome.runtime.sendMessagein the approval window before Approve is clicked and requires the resultingAUTISTMASK_SIGN_RESPONSE/AUTISTMASK_TX_RESPONSErecord to be present before asserting on it, so it cannot pass by observing nothing. Test 37 re-reads both channels for the whole section and requires all four methods to appear in the page-side record first.Item 6 — demonstrated to fail
Six runs against deliberately broken variants, all reverted. Captured output:
a. Signature recovered against the wrong address (the recovered value corrupted in
run.js):b. The password put back in the payload —
payload.password = $("approve-sign-password").valueand the same forapprove-tx-passwordinsrc/popup/views/approval.js, i.e. the shape #157 removed. Caught in four places:c. A provider that resolves instead of rejecting —
p.resolve(result)unconditionally insrc/content/inpage.js. All four rejection paths bind:d, e, f. Approval screens displaying something other than what is signed. Three separate runs, because the assertions shadow each other in order — a wrong origin on the sign screen, a wrong message, a wrong value and wrong call data on the transaction screen:
Worth knowing when reading those: the suite is stateful, so a failing test leaves an approval window open and the tests after it fail on the debris. The first failure in each block is the one the mutation caused.
What could NOT be driven, and why
src/background/index.jsraises the site-connection prompt throughchrome.action.setPopup()+chrome.action.openPopup(). Headless Chromium does open that popup, but Playwright never exposes it as a page — it is not inctx.pages()after 60s, with the action URL confirmed already set. So that one prompt is driven at the URL the extension put on the action: same page, same approval id, same code path, and the route README documents for reopening a pending approval. Whether a real toolbar click puts it on screen is not observable from a headless harness and is not claimed. The sign and transaction prompts need none of this — they open real windows and are driven as-is.README.mdandTODO.md.Two findings, reported rather than fixed
Neither is touched by this PR.
{"code":4001,"message":"User rejected the request."}and the test asserts that on the wire, butsrc/content/inpage.jsrebuilds it asnew Error(error.message), so the calling page catches anErrorwith nocodeproperty —page Error.code=undefined page Error carries a code=false, printed on every run.RULES.mdrequires "Rejected approvals return EIP-1193 error code 4001", and a dApp checkingerr.code === 4001seesundefined. The test records this rather than asserting either way, because asserting the current behaviour would make the gap permanent.btn-approvecallsruntime.sendMessage()and thenwindow.close()on the next line; closing disconnects the approval port, and the disconnect handler settles a pending site approval as a rejection. Driven in a tab the teardown wins every time — measured: with the close in place the page is told the user rejected, with it deferred the approval goes through. The harness therefore deferswindow.close()on that one page and closes it itself once the outcome has been observed, which is documented at the call site. This does not apply to the sign or transaction prompts: their disconnect handler deliberately keeps the approval pending. Whether the same ordering holds in a real toolbar popup is not observable here.Verification
make check— green (includesscript/test-verify-build:test-verify-build: 18 case(s) passed;prettier --check .:All matched files use Prettier code style!), re-run after the rebase.make test-e2e—# 37/37 tests passed, re-run after rebasing onto52c7c1b(which brought in #216, changing the transaction approval path these tests drive).ALLOWED_ERRORSis still[],TRAILING_WATCH_MSis unchanged, the interception canary is unchanged, and the unstubbed-request guard is unchanged — including the empty-batch fix that landed in the meantime.The seam between the content script, the inpage provider, the background worker and the approval popup had no coverage at all. The unit suite covers each side in isolation, so a wallet that signed the wrong payload, handed back a signature from the wrong key, hung on a rejected prompt, or put the user's password back on the extension messaging boundary would have passed everything. Ten end-to-end tests now drive it. The route handler serves a local test page on a reserved-TLD origin; window.ethereum arrives there from the shipped MAIN-world content script, not from anything the fixture installs, and the page's own EIP-6963 announcement is required to be that provider by object identity. The page then speaks eth_requestAccounts, personal_sign, eth_signTypedData_v4 and eth_sendTransaction through the real prompts, approved and rejected. The assertions are the point: - every signature is recovered in the runner with verifyMessage, verifyTypedData and Transaction.from(), and compared against the active address read out of extension storage. The background verifies too; nothing here leans on its verdict. - the transaction is checked against the raw signed bytes captured at eth_sendRawTransaction, which the RPC stub now records and answers with the transaction's real hash. Signer, recipient, value, call data and chain id are all compared there, and the hash the page received is required to be the hash of those bytes. - a rejected prompt must reach the page as a rejection rather than hang or resolve, and must carry EIP-1193 code 4001 across the boundary. The code is asserted on the wire because that is where it survives: the inpage provider rebuilds the rejection as a bare Error, so the calling page catches a message and no code. Reported, not asserted either way. - the password must appear in no message the approval window sends to the background, observed directly by wrapping chrome.runtime.sendMessage before Approve is clicked, with the response message that would carry it required to be present so the check cannot pass on an empty record. Every one of those was run against a deliberately broken variant and seen to fail: a corrupted recovered signer, a payload carrying the password again, a provider that resolves instead of rejecting, and approval screens displaying the wrong message, value and call data. Two honest limits. The RPC is stubbed throughout, so this does not discharge a real dApp with real funds against a real network. And the site-connection prompt goes through chrome.action.openPopup(), whose browser-action popup headless Chromium will not expose as a page, so that one prompt is driven at the URL the extension puts on the action instead — same page, same approval id, but a real toolbar click is not observable from a headless harness. Both are stated in README.md rather than presented as covered. Test-only: nothing under src/ changes.FAIL —
needs-rebase. The substance holds up under adversarial probing; the branch no longer applies tonext.Finding
TODO.md:48— conflicts with currentorigin/next. The new# Completed Stepsbullet is inserted at the same point as the#260bullet that landed ine4c3708. Bothgit rebase origin/nextandgit merge origin/nextstop withCONFLICT (content): Merge conflict in TODO.md. Gitea still reportsmergeable: true, but that is computed against the recorded base52c7c1b, which is two commits stale. Acceptable: rebase onto currentorigin/nextwith the #183 bullet kept at the top of# Completed Stepsand the #260 bullet directly beneath it, re-runmake checkandmake test-e2e, force-push.Nothing else. Everything below was verified on head
13d444cin an independent clone.Probes run (mine, distinct from the six in the PR body — all reverted, tree left clean)
personal_signverified against a one-word-altered message:not ok 31 … recovers to 0xE003…79ae.chainIdchanged 1 -> 5:not ok 33 … recovers to 0xf137…32bD.eth_sendRawTransactionwire record replaced with a transaction over identical fields signed by a foreign key:not ok 35 … signed by 0x19E7…ff2A. The transaction check is a real recovery from the captured bytes, not a shape check.not ok 31 / 33 / 37.AUTISTMASK_TELEMETRY:not ok 35 … the password crossed the extension messaging boundary on the eth_sendTransaction approval: [{"type":"AUTISTMASK_TELEMETRY",…,"password":"e2e-harness-password"}]. The observation is not narrowed to the response messages it asserts on.make checkgreen and executed (637 unit tests,test-verify-build: 18 case(s) passed, prettier clean);make test-e2e# 37/37 tests passed, executed not cached; CIcheck / check (push)success on13d444c.Disclosures
git diff origin/next --stat -- src/is non-empty only because the branch is behindnext; against the merge base52c7c1bit is empty. The commit changes no file undersrc/.chrome.runtime.sendMessageonly.src/popup/views/approval.js:444opens an approval port, but nothing undersrc/popup/orsrc/background/ever callsport.postMessage, so it misses no live channel today — a future port-carried payload would not be seen.make checkonly; the e2e suite these tests live in is not run by CI. Pre-existing, not introduced here.window.close()deferral is confined toreserveApprovalTab()'s own tab and documented at that call site;src/background/index.js:292keepstxandsignapprovals pending on disconnect, so #275 is not masked for the signature or transaction paths. #274 is printed on every run (page Error.code=undefined page Error carries a code=false), asserted only on the wire — the broken behaviour is not cemented.chrome.action.getPopup()'s URL is judged legitimate: same page, same approval id, same code path, and the PR states plainly that a real toolbar click is not observable.13d444c3aatodbd83fee02