fix: Firefox target is non-functional — Chrome callback APIs used against the promise-only browser namespace #153
Reference in New Issue
Block a user
Delete Branch "%!s()"
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?
Problem
Firefox is half the reason this project exists (
README.md:12: "None of thecommon alternatives work on Firefox"), and
docs/README.mdpromises "Work onboth Chrome and Firefox. Same codebase, same features, both browsers." On
main, the Firefox build is produced but dApp connectivity, all approvalpopups, and chain/account event broadcasts are non-functional.
build.jsdoes emit a coherent MV2 artifact —manifest/firefox.jsonusesbrowser_action, abackground.scriptsarray, flatweb_accessible_resources, andbrowser_specific_settings.gecko.id, and itcorrectly omits
"world": "MAIN"in favour of runtime injection atsrc/content/index.js:7-14. The manifest is not the problem.The problem is that the code resolves to the
browser.*namespace on Firefox(the
typeof browser !== "undefined"ternaries atbackground/index.js:25-35,content/index.js:42-43,approval.js:18-19) and then calls it withChrome-style callbacks.
browser.*is promise-only; passing a functionwhere an options/getInfo argument is expected does not invoke the callback.
Affected call sites:
src/content/index.js:23—storage.get("eip6963Uuid", (items) => {...}).Breaks EIP-6963 UUID generation/announcement.
src/content/index.js:45-55—runtime.sendMessage({...}, (response) => {...}).This is the entire page-to-background RPC relay; every
window.ethereumrequest from a dApp fails.
src/background/index.js:113—windowsApi.getLastFocused(cb).src/background/index.js:128—windowsApi.create(opts, cb). This is alsowhere
pendingApprovals[id].windowIdis assigned, so thewindow-closed-equals-rejection listener at
background/index.js:622-640can never match.
src/background/index.js:520-534—tabsApi.query({}, cb)/tabsApi.sendMessage(id, msg, cb)inbroadcastChainChanged.src/background/index.js:555—windowsApi.remove(windowId, cb);:567-590— same pattern inbroadcastAccountsChanged.src/popup/views/approval.js:375(AUTISTMASK_GET_APPROVAL),:443-460(
AUTISTMASK_TX_RESPONSE),:482-501(AUTISTMASK_SIGN_RESPONSE) — theapproval popup can never load its details or receive the tx hash.
background/index.js:530,:557,:584checkruntime.lastError, whichis a
chrome.*concept and is never populated forbrowser.*calls.Implementation requirements
src/shared/(e.g.src/shared/browserApi.js) and route every call siteabove through it. Do not scatter per-call-site ternaries further.
which and why in the PR:
(
sendMessage,storageGet,tabsQuery,windowsCreate, …) that usebrowser.*directly where available and wrapchrome.*callbacks innew Promiseotherwise. Convert the call sites toawait.chrome.*namespace — Firefox aliaseschrome.*withcallback semantics, so preferring
chrome.*on both browsers makes theexisting callback code correct as-written. Lower diff, but leaves
callback style in place.
Option (a) is the cleaner long-term shape and composes better with the
existing
asynccode; option (b) is the smaller, lower-risk change. Eitheris acceptable if applied uniformly.
runtime.lastErrorhandling must be replaced with whatever the chosenstrategy makes correct (rejected promise, or retained for the chrome path).
manifest/firefox.jsonMV2-correct; no manifest changes should beneeded, but say so explicitly if any are.
service-worker
setIntervalproblem and thelocalStorage-in-a-workerproblem are tracked separately — do not fold them in.
Definition of done
(1-8) goes through it. No remaining
typeof browser !== "undefined"ternaries outside that module.dist/firefox/: connect a dApp viaeth_requestAccountsand get theapproval popup; approve; the site receives the account.
details, and returns the tx hash to the page.
personal_signapproval popup opens andreturns a signature.
pending request with EIP-1193 code 4001.
versions used.
TODO.mdupdated in the same commit.make checkpasses.Your six manual checks are now machine-verifiable, and the premise here still stands
Two separate things, and it matters that they are kept apart.
1. The DoD is no longer blocked on a human
This issue's definition of done is six manual browser checks, four of them on
Firefox. #173 claimed no agent could
perform them. That claim was wrong for Chrome and it is also wrong for
Firefox: a containerized Firefox 153 driven by geckodriver installs the MV2
build from
dist/firefox/as a temporary add-on and drives the popup, and ithas been demonstrated failing and then passing on a real bug. Full detail and
the pinning requirements are in
#184.
So the four Firefox checks and the two Chrome checks in the DoD above become
automated assertions in the harnesses rather than a QA pass by you.
2. But this issue's own premise was NOT written from a bad assumption
The Firefox probe reported, as an incidental finding, that the popup is "not
non-functional" under Firefox - wallet creation, BIP-39, libsodium encryption,
HD derivation, state persistence and render all complete with zero console
errors - and suggested this issue needed re-grounding.
I checked that before repeating it, and it does not refute this issue. The
probe walked the popup wallet-management paths. Those go through
src/shared/state.js, which resolvesbrowser.storage.localand uses it inits promise form - correct on Firefox, which is exactly why that flow works
cleanly.
Every call site enumerated as 1-8 in the issue body is somewhere else: the
content script, the background page, and
approval.js. Those use thecallback form, and the probe never walked them, because reaching them
requires a dApp page speaking EIP-1193 rather than the popup UI. Confirmed
directly -
src/content/index.js:23and:45still pass callbacks tostorage.getandruntime.sendMessage.The probe also reported that Firefox exposes
chrome.*as a distinct,callback-flavored object alongside
browser.*. That is not a contradictioneither; it is precisely the fact that option (b) in this issue's implementation
requirements is built on.
So: premise intact, analysis intact, call-site list intact. Unlike
#173, this issue was written from
reading the code rather than from guessing about the environment, and it holds
up. I am recording this explicitly because a passing observation from a probe
is not evidence about paths the probe never executed, and it would have been
easy - and wrong - to relay it as though it were.
Consequences for this issue
assertions once #184 and
#181 land. I will do that rather
than asking you to run six browser passes.
#183 - a local page speaking
EIP-1193 through the real content script and background. Building it twice
would be waste, so 183 should be written against both harnesses, or at least
with the Firefox backend in mind.
verification for its own fix already sitting there waiting for it. That is a
much better position than fixing it blind and hoping.
No change to the strategy question. Option (a) versus (b) is still open and
still the implementer's call to make and justify in the PR.
Plan
Strategy (a), the promise shim.
src/shared/browserApi.jsbecomes the onlyfile in the tree that names
browserorchrome. It exports lazily-resolvednamespace handles for the parts that are events or synchronous
(
runtimeApi(),windowsApi(),tabsApi(),actionApi(),storageLocal(),alarmsApi()) and promise-returning wrappers for every call that iscallback-shaped on Chrome (
sendMessage,tabsQuery,tabsSendMessage,windowsCreate,windowsGetLastFocused,windowsRemove). Call sitesawait.Why (a) over (b):
src/shared/state.js,src/shared/alarms.jsandsrc/shared/phishingDomains.jsalready resolvebrowser.*and use it in itspromise form — which is exactly why the popup wallet flows work on Firefox
today while everything in this issue's list does not. Strategy (b) would make
the callback half correct by regressing the working half to callbacks, or leave
the codebase split down the middle. (a) makes the whole tree one shape, and it
composes with the
asynchandlers insrc/background/index.js.Two details worth stating up front rather than being found in review:
new Promise.chrome.storage.local.get()returns a promise on MV3 and thethree modules above already depend on that. Wrapping it would be a change,
not a fix.
AUTISTMASK_ACTIVE_CHANGED,AUTISTMASK_REMOVE_SITE) gets its ownnotify(), which sends with nocallback and swallows the no-receiver rejection. Appending a callback to a
send whose answer nobody reads would be noise.
runtime.lastErrordisappears entirely: on the Chrome path the wrapper readsit inside the callback and turns it into a rejection, so the three sites that
checked it become
.catch(() => {})on the send itself.Call sites 1-8 all still exist, at moved line numbers, and all get converted.
Beyond them, the remaining
typeof browser !== "undefined"ternaries(
state.js,alarms.js,phishingDomains.js,walletDelete.js,popup/views/home.js,popup/views/settings.js) are re-sourced from themodule so the DoD's "no ternaries outside that module" holds. Those are a
namespace re-source only, no call-shape change.
src/content/inpage.jsis nottouched.
No manifest change is expected; I will say so explicitly in the PR either way.
Verification
Machine, not manual, per the comment above. The Chrome suite already asserts
the four flows. For Firefox I extend
tests/e2e/firefox/with the same shapeas
tests/e2e/run.jscases 28-37.The obstacle named in
#184 is
--network none, sothere is no
http://origin to inject a content script into. Loopback survives--network none, so the harness serves the dApp page and a JSON-RPC stub froma node server on
127.0.0.1inside the container and points the extension'srpcUrlat it. That also turns the harness's UNVERIFIED content-script captureclaim into an exercised one.