Author SHA1 Message Date
sneak cd2a1ef825 fix: the first wallet's first address is active as soon as it is added (closes #512)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
The popup picked an active address only when it opened, so after the first
wallet was created nothing was active until a reopen: Home had no headline,
and Send and Receive refused. Each of the three import paths on AddWallet now
makes the new wallet's first address active when no address is. The
background already answered sites with that address through its own
fallback, so nothing is broadcast. The address scan stand-in in the popup
test boot now answers in the real scan's shape, which the phrase and xprv
imports read.

Model: opus-5-5
2026-10-14 19:42:37 +00:00
6 changed files with 94 additions and 43 deletions
+7 -8
View File
@@ -1488,8 +1488,10 @@ view would leave a wallet one click from deletion.
- **Transitions**:
- "Import" with a valid entry and a matching password of at least 12
characters → creates the wallet, clears the navigation stack, and →
**Home**. The phrase and xprv modes then scan for further used addresses
and report the count as a flash message.
**Home**. When no address is active yet, as on the first wallet, the new
wallet's first address becomes the active one. The phrase and xprv modes
then scan for further used addresses and report the count as a flash
message.
- "Import" with a missing, short or mismatched password → full-sentence
error on the error line, no screen change
- "Import" with an invalid entry or a duplicate wallet or address → flash
@@ -2080,12 +2082,9 @@ view would leave a wallet one click from deletion.
`wallet_requestPermissions` and is on neither the allowed nor the denied list.
A site is its full origin, `scheme://host[:port]`, on both lists and for a
connection allowed without "Remember": a choice for `https://dapp.example`
says nothing about `http://dapp.example` or another port of that host. A page
with an opaque origin, such as one its server sandboxes with a
Content-Security-Policy, is reported with the origin `null`, which is no site:
every request from it is refused with EIP-1193 code `4100` and opens no
prompt. The background script prefers the toolbar popup (`action.openPopup()`)
and falls back to a separate popup window (`src/background/index.js`,
says nothing about `http://dapp.example` or another port of that host. The
background script prefers the toolbar popup (`action.openPopup()`) and falls
back to a separate popup window (`src/background/index.js`,
`requestApproval()`). Only one exists per site at a time: a further connection
request from a site whose prompt is still unanswered is refused with EIP-1193
code `-32002` and opens no new prompt. If that prompt was in a toolbar popup
+1 -6
View File
@@ -1428,11 +1428,6 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
// the origin of sender.url is used: the frame that sent the message,
// not the tab's page, which may be another site embedding that
// frame. With neither, the request is refused.
//
// A page with an opaque origin, such as one its server sandboxes with
// a Content-Security-Policy, is reported with the origin "null". That
// is no site, so the request is refused too, and not credited to the
// site its URL names.
let trustedOrigin = sender.origin;
if (!trustedOrigin && sender.url) {
try {
@@ -1441,7 +1436,7 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
// an unparseable URL leaves the origin unknown
}
}
if (!trustedOrigin || trustedOrigin === "null") {
if (!trustedOrigin) {
sendResponse({
error: {
code: 4100,
+9
View File
@@ -194,6 +194,9 @@ async function importMnemonic(ctx) {
};
state.wallets.push(wallet);
state.hasWallet = true;
if (state.activeAddress === null) {
state.activeAddress = firstAddress;
}
await saveState();
clearViewStack();
ctx.renderWalletList();
@@ -254,6 +257,9 @@ async function importPrivateKey(ctx) {
addresses: [{ address: addr, balance: "0.0000", tokenBalances: [] }],
});
state.hasWallet = true;
if (state.activeAddress === null) {
state.activeAddress = addr;
}
await saveState();
clearViewStack();
ctx.renderWalletList();
@@ -315,6 +321,9 @@ async function importXprvKey(ctx) {
};
state.wallets.push(wallet);
state.hasWallet = true;
if (state.activeAddress === null) {
state.activeAddress = firstAddress;
}
await saveState();
clearViewStack();
ctx.renderWalletList();
+69
View File
@@ -0,0 +1,69 @@
// Adding the first wallet left no address active until the popup was reopened,
// so Send and Receive on Home only said "No active address selected."
// (https://git.eeqj.de/sneak/AutistMask/issues/512). Each test boots a fresh
// install, adds the first wallet through one of the three import tabs, and
// opens Receive from Home in the same popup.
const { HDNodeWallet, Mnemonic, Wallet } = require("ethers");
const { bootPopup, cleanupPopup } = require("./support/popupBoot");
const PHRASE =
"abandon abandon abandon abandon abandon abandon " +
"abandon abandon abandon abandon abandon about";
const PRIVATE_KEY = "0x" + "11".repeat(32);
// The master key of PHRASE, so its first address is the phrase's.
const XPRV = HDNodeWallet.fromSeed(
Mnemonic.fromPhrase(PHRASE).computeSeed(),
).extendedKey;
const PASSWORD = "correct horse battery staple";
const PHRASE_ADDRESS = HDNodeWallet.fromPhrase(PHRASE).address;
const TABS = [
{
tab: "tab-mnemonic",
field: "wallet-mnemonic",
secret: PHRASE,
address: PHRASE_ADDRESS,
},
{
tab: "tab-privkey",
field: "import-private-key",
secret: PRIVATE_KEY,
address: new Wallet(PRIVATE_KEY).address,
},
{
tab: "tab-xprv",
field: "import-xprv-key",
secret: XPRV,
address: PHRASE_ADDRESS,
},
];
afterEach(() => {
cleanupPopup();
});
describe("the first wallet's first address is active right away", () => {
test.each(TABS)(
"added from $tab, Receive opens on it",
async ({ tab, field, secret, address }) => {
const page = await bootPopup(undefined);
expect(page.pageErrors).toEqual([]);
await page.click("btn-welcome-add");
await page.click(tab);
page.node(field).value = secret;
page.node("add-wallet-password").value = PASSWORD;
page.node("add-wallet-password-confirm").value = PASSWORD;
await page.click("btn-add-wallet-confirm");
expect(page.visibleViews()).toEqual(["main"]);
await page.click("btn-main-receive");
expect(page.visibleViews()).toEqual(["receive"]);
expect(page.node("receive-address-block").dataset.full).toBe(
address,
);
},
);
});
+2 -28
View File
@@ -8,14 +8,8 @@
// and a request the browser said nothing about was credited to whatever the
// page wrote (https://git.eeqj.de/sneak/AutistMask/issues/407).
//
// A page with an opaque origin, such as one its server sandboxes with a
// Content-Security-Policy, is reported with the origin "null". That names no
// site, so its request is refused
// (https://git.eeqj.de/sneak/AutistMask/issues/527).
//
// Every other sender here lacks sender.origin, as on old Firefox. The
// connection check on eth_accounts is what shows which site a request was
// credited to.
// Every sender here lacks sender.origin, as on old Firefox. The connection
// check on eth_accounts is what shows which site a request was credited to.
const { makeStorageStub } = require("./support/storageStub");
@@ -150,23 +144,3 @@ describe("a request the browser does not say the sender of", () => {
});
});
});
describe("a request from a page with an opaque origin", () => {
test("is refused, whatever the page's URL says", async () => {
const accounts = loadBackground();
const result = await accounts({
origin: "null",
url: CONNECTED_ORIGIN + "/sandboxed.html",
tab: { url: CONNECTED_ORIGIN + "/sandboxed.html" },
});
expect(result).toEqual({
error: {
code: 4100,
message:
"The wallet could not tell which site sent this request.",
},
});
});
});
+6 -1
View File
@@ -258,12 +258,17 @@ async function bootPopup(stored, options) {
formatAddressTotal: () => "",
getAddressValue: () => ({ usd: null, partial: false }),
}));
// scanForAddresses() answers in the real one's shape, a scan that found
// no used address: AddWallet reads `addresses` from it.
jest.doMock("../../src/shared/balances", () => ({
fetchTokenBalances: jest.fn(async () => []),
refreshBalances: jest.fn(async () => {}),
lookupTokenInfo: jest.fn(async () => null),
getProvider: () => ({}),
scanForAddresses: jest.fn(async () => []),
scanForAddresses: jest.fn(async () => ({
addresses: [],
nextIndex: 1,
})),
}));
// filterTransactions() answers in the real one's shape: Home,
// AddressDetail and AddressToken read both fields, and a bare list makes