From 6e31d3768df23ad953fa28bdbe1725ba8e1eedb7 Mon Sep 17 00:00:00 2001
From: clawbot <35+clawbot@noreply.example.org>
Date: Mon, 19 Oct 2026 15:33:23 +0000
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 | 8 ++-
tests/e2e/run.js | 120 ++++++++++++++++++++++++++++++++++-
5 files changed, 150 insertions(+), 18 deletions(-)
diff --git a/README.md b/README.md
index c325f27..74ee01b 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 covers the same
+way 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..00844da 100644
--- a/tests/addressKey.test.js
+++ b/tests/addressKey.test.js
@@ -442,12 +442,14 @@ 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("");
});
});
diff --git a/tests/e2e/run.js b/tests/e2e/run.js
index 56e2e4e..1b1e34f 100644
--- a/tests/e2e/run.js
+++ b/tests/e2e/run.js
@@ -2266,7 +2266,12 @@ async function confirmState(page) {
amountFeeError: shown("confirm-amount-fee-error"),
gasError: shown("confirm-gas-error"),
feeUnknownError: shown("confirm-fee-unknown-error"),
- // Its sentence is wrapped across lines in index.html.
+ // Their sentences are wrapped across lines in index.html.
+ fromAddressError: shown("confirm-from-address-error")
+ ? el("confirm-from-address-error")
+ .textContent.replace(/\s+/g, " ")
+ .trim()
+ : "",
feeBoundError: shown("confirm-fee-bound-error")
? el("confirm-fee-bound-error")
.textContent.replace(/\s+/g, " ")
@@ -3153,6 +3158,119 @@ 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);
+ const before = await confirmState(env.page);
+ assert(
+ !before.sendDisabled && before.fromAddressError === "",
+ "the screen is not ready to send: " + JSON.stringify(before),
+ );
+
+ const sent = env.routeOpts.broadcastTransactions.length;
+ await fillPasswordAndSend(env.page);
+ // The refusal shown anywhere on the screen, not only in its own box:
+ // shown on the password error line instead, it fails on the moves
+ // checked below rather than on this wait.
+ await env.page
+ .locator("#view-confirm-tx")
+ .getByText("does not hold the From address above")
+ .filter({ visible: true })
+ .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",
+ );
+ assert(
+ after.fromAddressError ===
+ "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(after.fromAddressError),
+ );
+ } 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