From 2c882c75d6a99ad4ddfcdb0f650d89e1d03b5e9d Mon Sep 17 00:00:00 2001
From: clawbot <35+clawbot@noreply.example.org>
Date: Mon, 19 Oct 2026 20:50:02 +0200
Subject: [PATCH] fix: refusing a From address the wallet does not hold moves
nothing (closes #609)
Sign & Send wrote its refusal of a From address the selected wallet does not
hold onto the password error line, which reserves one line. The sentence runs
to about three, so it pushed Sign & Send down.
The refusal now has its own box above the password field, its sentence fixed
in index.html, holding its space from the first paint and only made visible,
as https://git.eeqj.de/sneak/AutistMask/issues/583 did for the other
refusals. Drawing the screen and each press hide it again. A Chrome test
writes the other wallet into the stored selection, reopens the popup onto the
screen, presses Sign & Send and checks that nothing was broadcast and nothing
moved.
Model: opus-5-5
---
README.md | 18 ++++--
src/popup/index.html | 14 ++++-
src/popup/views/confirmTx.js | 8 +--
tests/addressKey.test.js | 20 +++++-
tests/e2e/run.js | 119 +++++++++++++++++++++++++++++++++++
5 files changed, 162 insertions(+), 17 deletions(-)
diff --git a/README.md b/README.md
index c325f27..c388a34 100644
--- a/README.md
+++ b/README.md
@@ -433,7 +433,11 @@ previously correct by reading only.
It also covers a send that Sign & Send refuses for its network fee: the stub
node quotes an ordinary fee while the confirmation screen is drawn and one over
the bound when Sign & Send asks again, and the refusal must broadcast nothing
-and leave the password field and Sign & Send where they were.
+and leave the password field and Sign & Send where they were. It checks the same
+for a send refused because the selected wallet does not hold its From address:
+with the confirmation screen showing a send from the HD wallet, the key wallet
+is selected by writing it straight into extension storage, as another popup
+selecting it would save it, and the popup is reopened onto that screen.
It also covers both ways the wait for a sent transaction's receipt ends on the
error screen: lookups that still find no receipt 60 seconds after the broadcast,
@@ -1814,9 +1818,10 @@ view would leave a wallet one click from deletion.
estimated, and it says the wallet does not know how many decimal places
the token uses and that the transaction cannot be sent; for any other
failure it asks the user to go back and try again
- - Two more reserved error boxes, made visible when "Sign & Send" refuses a
- send because the active network changed or because of its gas limit or fee
- (see Transitions), so the refusal moves nothing
+ - Three more reserved error boxes, made visible when "Sign & Send" refuses a
+ send because the active network changed, because the selected wallet does
+ not hold the From address, or because of its gas limit or fee (see
+ Transitions), so the refusal moves nothing
- Password: an inline field on this screen, not a modal, with its own error
line
- "Sign & Send" button (disabled if errors, while the balance is being read,
@@ -1860,8 +1865,9 @@ view would leave a wallet one click from deletion.
- "Sign & Send" signs as the From address above, not as whichever address is
selected: the two are saved separately, so the popup can reopen onto this
screen with another address selected. When the selected wallet does not
- hold the From address → full-sentence error on the password error line,
- nothing sent (no screen change)
+ hold the From address → nothing is signed; "The selected wallet does not
+ hold the From address above, so nothing was sent. Please go back and send
+ again." in its own box, no screen change
- "Sign & Send" (correct password, but the From address's key is refused,
see Data Model) → nothing is signed → **ErrorTx** saying why
- "Back" → **Send**
diff --git a/src/popup/index.html b/src/popup/index.html
index 89e3fac..ba3197f 100644
--- a/src/popup/index.html
+++ b/src/popup/index.html
@@ -697,9 +697,10 @@
id="confirm-fee-unknown-error"
class="mb-2 border border-border border-dashed p-2 text-xs invisible"
>
-
+
+
+ The selected wallet does not hold the From address above, so
+ nothing was sent. Please go back and send again.
+
{
// A press shows only its own refusal, if any.
setVisible("confirm-network-changed-error", false);
+ setVisible("confirm-from-address-error", false);
setVisible("confirm-fee-bound-error", false);
const wallet = state.wallets[state.selectedWallet];
@@ -686,11 +688,7 @@ function init(_ctx) {
// address selected.
const addr = wallet.addresses.find((a) => a.address === pendingTx.from);
if (!addr) {
- showError(
- "confirm-tx-password-error",
- "The selected wallet does not hold the From address above, " +
- "so nothing was sent. Please go back and send again.",
- );
+ setVisible("confirm-from-address-error", true);
return;
}
diff --git a/tests/addressKey.test.js b/tests/addressKey.test.js
index 57aa04d..322b87c 100644
--- a/tests/addressKey.test.js
+++ b/tests/addressKey.test.js
@@ -442,11 +442,25 @@ describe("a confirmation screen whose From is not the selected address", () => {
expect(Transaction.from(signed).from).toBe(derived(1).address);
});
+ // The refusal's sentence is fixed in index.html, in a box of its own that
+ // holds its space from the first paint, so showing it moves nothing.
test("nothing is sent when the selected wallet does not hold it", async () => {
expect(await confirmSend(derived(5).address, 0)).toBeUndefined();
- expect(node("confirm-tx-password-error").textContent).toBe(
- "The selected wallet does not hold the From address above, " +
- "so nothing was sent. Please go back and send again.",
+ expect(node("confirm-from-address-error").style.visibility).toBe(
+ "visible",
+ );
+ expect(node("confirm-tx-password-error").textContent).toBe("");
+
+ // Reviewing a send again draws the screen again, which hides it.
+ confirmTx.show({
+ from: derived(0).address,
+ to: RECIPIENT,
+ amount: "0.5",
+ token: "ETH",
+ });
+ await settle();
+ expect(node("confirm-from-address-error").style.visibility).toBe(
+ "hidden",
);
});
});
diff --git a/tests/e2e/run.js b/tests/e2e/run.js
index 56e2e4e..24d2aec 100644
--- a/tests/e2e/run.js
+++ b/tests/e2e/run.js
@@ -3153,6 +3153,125 @@ test("a send refused for its fee leaves the password field and Sign & Send where
}
});
+// --------- a send from an address the selected wallet does not hold (#609)
+//
+// The selection is saved apart from the confirmation screen, so the popup can
+// reopen onto the screen with a wallet selected that does not hold its From
+// address, and Sign & Send then refuses the send. Another popup selecting the
+// key wallet would save that selection; here it is written into storage, and
+// the popup reopened.
+
+test("a send from an address the selected wallet does not hold leaves the password field and Sign & Send where they were (#609)", async (env) => {
+ try {
+ // From the HD wallet's address.
+ await goToConfirm(env.page, {
+ token: "ETH",
+ balance: FUNDED_ETH_TEXT + " ETH",
+ amount: COMFORTABLE_AMOUNT,
+ });
+ // The screen's own save lands first, so it cannot write over the
+ // selection below.
+ await waitForPersisted(
+ env.page,
+ "currentView",
+ "confirm-tx",
+ "before selecting the key wallet",
+ );
+ await env.page.evaluate(
+ () =>
+ new Promise((resolve) => {
+ chrome.storage.local.get("autistmask", (r) => {
+ const record = {
+ ...r.autistmask,
+ selectedWallet: 1,
+ selectedAddress: 0,
+ };
+ chrome.storage.local.set(
+ { autistmask: record },
+ resolve,
+ );
+ });
+ }),
+ );
+ await reopenPopup(env, "confirm-tx");
+ await waitForEstimate(env.page);
+ // The refusal shown anywhere on the screen, not only in its own box:
+ // shown on the password error line instead, the test fails on the
+ // moves checked below rather than on the wait for it.
+ const refusal = env.page
+ .locator("#view-confirm-tx")
+ .getByText("does not hold the From address above")
+ .filter({ visible: true });
+ const before = await confirmState(env.page);
+ assert(
+ !before.sendDisabled && (await refusal.count()) === 0,
+ "the screen is not ready to send: " + JSON.stringify(before),
+ );
+
+ const sent = env.routeOpts.broadcastTransactions.length;
+ await fillPasswordAndSend(env.page);
+ await refusal.waitFor({ timeout: 60000 });
+ const after = await confirmState(env.page);
+
+ // Printed pass or fail, as the dust threshold test does.
+ console.log(
+ "# refused From address: password field top " +
+ before.passwordTop +
+ " -> " +
+ after.passwordTop +
+ "px, Sign & Send top " +
+ before.sendTop +
+ " -> " +
+ after.sendTop +
+ "px",
+ );
+ assert(
+ env.routeOpts.broadcastTransactions.length === sent,
+ "a send from an address the selected wallet does not hold" +
+ " reached the RPC",
+ );
+ assert(
+ after.passwordTop === before.passwordTop,
+ "the password field moved " +
+ (after.passwordTop - before.passwordTop) +
+ "px when the send was refused",
+ );
+ assert(
+ after.sendTop === before.sendTop,
+ "Sign & Send moved " +
+ (after.sendTop - before.sendTop) +
+ "px when the send was refused",
+ );
+ // Only now, where the refusal is shown. Its sentence is wrapped across
+ // lines in index.html.
+ const shownIn = await refusal.evaluate((el) => ({
+ id: el.id,
+ text: el.textContent.replace(/\s+/g, " ").trim(),
+ }));
+ assert(
+ shownIn.id === "confirm-from-address-error" &&
+ shownIn.text ===
+ "The selected wallet does not hold the From address above," +
+ " so nothing was sent. Please go back and send again.",
+ "wrong From address refusal: " + JSON.stringify(shownIn),
+ );
+ } finally {
+ // A reopened popup restores no Send screen beneath this one, so Back
+ // lands on the selected address. Then Home, and the HD wallet's
+ // address, which the later tests send from.
+ if (await env.page.isVisible("#view-confirm-tx")) {
+ await env.page.click("#btn-confirm-back");
+ }
+ if (await env.page.isVisible("#view-send")) {
+ await env.page.click("#btn-send-back");
+ }
+ await visible(env.page, "#view-address");
+ await env.page.click("#btn-address-back");
+ await visible(env.page, "#view-main");
+ await openAddressDetail(env.page);
+ }
+});
+
// ------------------------------ the wait for a receipt ending in error (#315)
//
// README.md (WaitTx) documents two ways the wait ends on the error screen: a