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