Compare commits
2
Commits
a314734acc
...
6032f542ee
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6032f542ee | ||
|
|
21d5e42528 |
@@ -786,6 +786,7 @@ Shown after a broadcast. The screen holds:
|
||||
|
||||
- the heading "Transaction Broadcast", with no "Back" button;
|
||||
- the amount and symbol;
|
||||
- each `Moved from the sender` amount of a swap;
|
||||
- the recipient;
|
||||
- the transaction hash, with an explorer link;
|
||||
- the counter "Waiting for confirmation... Ns".
|
||||
@@ -823,7 +824,9 @@ Shown when signing or a broadcast fails, or when a wait ends without a receipt.
|
||||
The screen holds:
|
||||
|
||||
- the heading "Transaction Failed";
|
||||
- the amount and the recipient;
|
||||
- the amount;
|
||||
- each `Moved from the sender` amount of a swap;
|
||||
- the recipient;
|
||||
- the error message, in a dashed box;
|
||||
- the transaction hash, when one exists;
|
||||
- a "Done" button that behaves as on SuccessTx.
|
||||
|
||||
@@ -45,6 +45,11 @@ function renderTop10() {
|
||||
el.querySelectorAll(".settings-addtoken-quick:not([disabled])").forEach(
|
||||
(btn) => {
|
||||
btn.addEventListener("click", async () => {
|
||||
// The button stays enabled until the first click's save ends.
|
||||
if (isTracked(btn.dataset.address)) {
|
||||
showFlash("Already tracked.");
|
||||
return;
|
||||
}
|
||||
const token = {
|
||||
address: btn.dataset.address,
|
||||
symbol: btn.dataset.symbol,
|
||||
|
||||
@@ -73,6 +73,29 @@ function toAddressHtml(address, chainId) {
|
||||
return renderAddressHtml(address, { title, chainId });
|
||||
}
|
||||
|
||||
// The amount of each `Moved from the sender` line of a decoded swap, for the
|
||||
// wait and error screens to show under the Amount figure. That figure can leave
|
||||
// these amounts out, so alone it can read as less than can have left the
|
||||
// wallet. `decoded` can come back from storage in any shape, so this shows only
|
||||
// the lines whose value is text.
|
||||
function movedFromSenderHtml(decoded) {
|
||||
if (!decoded || !Array.isArray(decoded.details)) return "";
|
||||
let html = "";
|
||||
for (const d of decoded.details) {
|
||||
if (
|
||||
!d ||
|
||||
d.label !== "Moved from the sender" ||
|
||||
typeof d.value !== "string"
|
||||
)
|
||||
continue;
|
||||
html += `<div class="mt-2">`;
|
||||
html += `<div class="text-xs text-muted mb-1">${escapeHtml(d.label)}</div>`;
|
||||
html += `<div class="font-bold">${escapeHtml(d.value)}</div>`;
|
||||
html += `</div>`;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
function txHashHtml(hash, chainId) {
|
||||
const link = explorerUrl("tx", hash, chainId);
|
||||
return copyableHtml(hash, "break-all") + etherscanLinkHtml(link);
|
||||
@@ -109,10 +132,9 @@ function startWait(txInfo, txHash, broadcastTime, pollNow) {
|
||||
}
|
||||
$("wait-tx-summary").textContent =
|
||||
txInfo.amount + (symbol ? " " + symbol : "");
|
||||
$("wait-tx-token-note").innerHTML = blockscoutOffTokenNoteHtml(
|
||||
txInfo.token,
|
||||
txInfo.chainId,
|
||||
);
|
||||
$("wait-tx-token-note").innerHTML =
|
||||
blockscoutOffTokenNoteHtml(txInfo.token, txInfo.chainId) +
|
||||
movedFromSenderHtml(txInfo.decoded);
|
||||
$("wait-tx-to").innerHTML = toAddressHtml(txInfo.to, txInfo.chainId);
|
||||
$("wait-tx-hash").innerHTML = txHashHtml(txHash, txInfo.chainId);
|
||||
attachCopyHandlers("view-wait-tx");
|
||||
@@ -223,7 +245,7 @@ function showWait(txInfo, txHash) {
|
||||
// to name a network. txInfo.token and txInfo.tokenSymbol are not checked here.
|
||||
// blockscoutOffTokenNoteHtml() shows no line for a token that is not text.
|
||||
// Everything else only compares and coalesces them, and tokenSymbol is null for
|
||||
// ETH.
|
||||
// ETH. txInfo.decoded is not checked either: see movedFromSenderHtml().
|
||||
function restoreWait() {
|
||||
const d = state.viewData;
|
||||
if (!d || !d.pendingWait) return false;
|
||||
@@ -356,6 +378,7 @@ function showError(txInfo, txHash, message) {
|
||||
to: txInfo.to,
|
||||
hash: txHash || null,
|
||||
message: message,
|
||||
decoded: txInfo.decoded || null,
|
||||
chainId: txInfo.chainId,
|
||||
};
|
||||
renderError();
|
||||
@@ -366,10 +389,9 @@ function renderError() {
|
||||
if (!d || !d.message) return;
|
||||
$("error-tx-summary").textContent =
|
||||
d.amount + (d.symbol ? " " + d.symbol : "");
|
||||
$("error-tx-token-note").innerHTML = blockscoutOffTokenNoteHtml(
|
||||
d.token,
|
||||
d.chainId,
|
||||
);
|
||||
$("error-tx-token-note").innerHTML =
|
||||
blockscoutOffTokenNoteHtml(d.token, d.chainId) +
|
||||
movedFromSenderHtml(d.decoded);
|
||||
$("error-tx-to").innerHTML = toAddressHtml(d.to, d.chainId);
|
||||
$("error-tx-message").textContent = d.message;
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/603). An answer while AddToken
|
||||
// still shows goes to AddressDetail. On either add-token screen, an answer for a
|
||||
// token tracked while the lookup waited tracks nothing
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/727). Driven through the real
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/727). A second click on a "Top
|
||||
// tokens:" button tracks nothing
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/729). Driven through the real
|
||||
// popup over a stored profile.
|
||||
|
||||
const {
|
||||
@@ -197,6 +199,8 @@ describe.each([
|
||||
await answerSecondClick();
|
||||
expect(boot.text("flash-msg")).toBe(alreadyTrackedFlash);
|
||||
expect(boot.storage.read("autistmask").trackedTokens).toEqual([TOKEN]);
|
||||
const { state } = require("../src/shared/state");
|
||||
expect(state.trackedTokens).toEqual([TOKEN]);
|
||||
});
|
||||
|
||||
test("an answer for a token tracked while the lookup waited clears the status line", async () => {
|
||||
@@ -213,3 +217,44 @@ describe.each([
|
||||
expect(state.trackedTokens).toEqual([TOKEN]);
|
||||
});
|
||||
});
|
||||
|
||||
test('a double click on a "Top tokens:" button tracks the token once', async () => {
|
||||
const boot = await bootPopup(
|
||||
unversionedValidProfile({
|
||||
currentView: "address",
|
||||
selectedWallet: 0,
|
||||
selectedAddress: 0,
|
||||
}),
|
||||
);
|
||||
expect(boot.pageErrors).toEqual([]);
|
||||
// The page stub does not turn the markup the screen draws into elements.
|
||||
// So the list hands the screen a stand-in for TOKEN's button.
|
||||
let press;
|
||||
boot.node("settings-addtoken-top10").querySelectorAll = () => [
|
||||
{
|
||||
dataset: {
|
||||
address: TOKEN.address,
|
||||
symbol: TOKEN.symbol,
|
||||
decimals: String(TOKEN.decimals),
|
||||
name: TOKEN.name,
|
||||
},
|
||||
addEventListener: (name, fn) => {
|
||||
press = fn;
|
||||
},
|
||||
},
|
||||
];
|
||||
await boot.click("btn-settings");
|
||||
await boot.click("btn-settings-add-token");
|
||||
|
||||
const firstClick = press();
|
||||
const secondClick = press();
|
||||
const { state } = require("../src/shared/state");
|
||||
expect(state.trackedTokens).toEqual([TOKEN]);
|
||||
// Read before the first click's save ends, when "Added TST" replaces it.
|
||||
expect(boot.text("flash-msg")).toBe("Already tracked.");
|
||||
|
||||
await Promise.all([firstClick, secondClick]);
|
||||
await boot.settle();
|
||||
expect(state.trackedTokens).toEqual([TOKEN]);
|
||||
expect(boot.storage.read("autistmask").trackedTokens).toEqual([TOKEN]);
|
||||
});
|
||||
|
||||
@@ -340,3 +340,75 @@ describe.each([
|
||||
expect(node("error-tx-summary").textContent).toBe("1.0000 ETH");
|
||||
});
|
||||
});
|
||||
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/722: a WRAP_ETH of 1 ETH into the
|
||||
// router, an UNWRAP_WETH into the router, then a V4 swap of 1 ETH the user
|
||||
// settles. The Amount line reads 1.0000 ETH, though up to 2 ETH can leave. The
|
||||
// rest is on the swap's own Moved from the sender line.
|
||||
describe("a swap with a Moved from the sender line of its own", () => {
|
||||
const ADDRESS_THIS = "0x0000000000000000000000000000000000000002";
|
||||
const TWO_ETH = 2n * ONE_ETH;
|
||||
const SEND_FAILS = {
|
||||
error: "The transaction could not be sent.",
|
||||
retryable: false,
|
||||
};
|
||||
const data = routerIface.encodeFunctionData("execute", [
|
||||
"0x0b0c10",
|
||||
[
|
||||
// WRAP_ETH and UNWRAP_WETH have the same layout.
|
||||
coder.encode(["address", "uint256"], [ADDRESS_THIS, ONE_ETH]),
|
||||
coder.encode(["address", "uint256"], [ADDRESS_THIS, 0n]),
|
||||
coder.encode(
|
||||
["bytes", "bytes[]"],
|
||||
[
|
||||
new Uint8Array([SWAP_EXACT_IN_SINGLE, SETTLE, TAKE_ALL]),
|
||||
[
|
||||
coder.encode(
|
||||
[`tuple(${POOL_KEY},bool,uint128,uint128,bytes)`],
|
||||
[[ETH_USDC_POOL, true, ONE_ETH, 990000n, "0x"]],
|
||||
),
|
||||
// The user pays this one, not the router.
|
||||
coder.encode(
|
||||
["address", "uint256", "bool"],
|
||||
[ZERO, 0n, true],
|
||||
),
|
||||
takeAllUsdc,
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
9999999999n,
|
||||
]);
|
||||
|
||||
function expectMovedLine(id) {
|
||||
const html = node(id).innerHTML;
|
||||
expect(html).toContain("Moved from the sender");
|
||||
expect(html).toContain("Up to 1.0000 ETH");
|
||||
}
|
||||
|
||||
test("the wait screen shows that line's amount under the Amount figure", async () => {
|
||||
await approveSwap(data, { txHash: TX_HASH }, TWO_ETH);
|
||||
expect(node("wait-tx-summary").textContent).toBe("1.0000 ETH");
|
||||
expectMovedLine("wait-tx-token-note");
|
||||
});
|
||||
|
||||
test("the error screen shows that line's amount under the Amount figure", async () => {
|
||||
await approveSwap(data, SEND_FAILS, TWO_ETH);
|
||||
expect(node("error-tx-summary").textContent).toBe("1.0000 ETH");
|
||||
expectMovedLine("error-tx-token-note");
|
||||
});
|
||||
|
||||
test("a reopened popup's wait screen still shows it", async () => {
|
||||
await approveSwap(data, { txHash: TX_HASH }, TWO_ETH);
|
||||
node("wait-tx-token-note").innerHTML = "";
|
||||
txStatus.restoreWait();
|
||||
expectMovedLine("wait-tx-token-note");
|
||||
});
|
||||
|
||||
test("a reopened popup's error screen still shows it", async () => {
|
||||
await approveSwap(data, SEND_FAILS, TWO_ETH);
|
||||
node("error-tx-token-note").innerHTML = "";
|
||||
txStatus.renderError();
|
||||
expectMovedLine("error-tx-token-note");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -672,6 +672,86 @@ describe("SuccessTx and ErrorTx for a Sepolia transaction with mainnet active",
|
||||
});
|
||||
});
|
||||
|
||||
describe("the Moved from the sender lines of a swap on WaitTx and ErrorTx", () => {
|
||||
const moved = (value) => ({ label: "Moved from the sender", value });
|
||||
const decoded = {
|
||||
details: [
|
||||
moved("Up to 1.0000 ETH"),
|
||||
{ label: "Token moved", value: "ETH" },
|
||||
moved("<b>400.0000</b> DAI"),
|
||||
],
|
||||
};
|
||||
|
||||
function expectEveryLine(id) {
|
||||
const html = getElement(id).innerHTML;
|
||||
expect(html).toContain("Up to 1.0000 ETH");
|
||||
expect(html).toContain("<b>400.0000</b> DAI");
|
||||
expect(html).not.toContain("Token moved");
|
||||
}
|
||||
|
||||
test("WaitTx shows each line's amount, escaped", () => {
|
||||
mockReceiptLookup.mockResolvedValue(null);
|
||||
txStatus.showWait({ ...TX_INFO, decoded }, TX_HASH);
|
||||
expectEveryLine("wait-tx-token-note");
|
||||
});
|
||||
|
||||
test("ErrorTx shows each line's amount, escaped", () => {
|
||||
txStatus.showError({ ...TX_INFO, decoded }, TX_HASH, "It failed.");
|
||||
expectEveryLine("error-tx-token-note");
|
||||
});
|
||||
|
||||
test("a wait that ends in ErrorTx keeps them", async () => {
|
||||
mockReceiptLookup.mockResolvedValue(null);
|
||||
txStatus.showWait({ ...TX_INFO, decoded }, TX_HASH);
|
||||
await jest.advanceTimersByTimeAsync(60000);
|
||||
expect(state.currentView).toBe("error-tx");
|
||||
expectEveryLine("error-tx-token-note");
|
||||
});
|
||||
|
||||
// A saved record comes back from storage, in whatever shape it is in. A
|
||||
// throw here would leave the popup's restore unfinished.
|
||||
test("a saved record in another shape shows only the lines it can read", () => {
|
||||
mockReceiptLookup.mockResolvedValue(null);
|
||||
function noteOnReopen(saved) {
|
||||
state.viewData = {
|
||||
pendingWait: {
|
||||
hash: TX_HASH,
|
||||
txInfo: { ...TX_INFO, decoded: saved },
|
||||
broadcastTime: Date.now(),
|
||||
},
|
||||
};
|
||||
expect(txStatus.restoreWait()).toBe(true);
|
||||
const waitNote = getElement("wait-tx-token-note").innerHTML;
|
||||
txStatus.endWait();
|
||||
|
||||
state.viewData = {
|
||||
message: "It failed.",
|
||||
to: RECIPIENT,
|
||||
chainId: TX_INFO.chainId,
|
||||
decoded: saved,
|
||||
};
|
||||
txStatus.renderError();
|
||||
expect(getElement("error-tx-token-note").innerHTML).toBe(waitNote);
|
||||
return waitNote;
|
||||
}
|
||||
|
||||
for (const saved of [
|
||||
7,
|
||||
"text",
|
||||
{ details: 7 },
|
||||
{ details: [null] },
|
||||
{ details: [{ label: "Moved from the sender" }] },
|
||||
{ details: [moved(7)] },
|
||||
{ details: [moved({ toString: 1 })] },
|
||||
]) {
|
||||
expect(noteOnReopen(saved)).toBe("");
|
||||
}
|
||||
expect(
|
||||
noteOnReopen({ details: [null, moved("Up to 1.0000 ETH")] }),
|
||||
).toContain("Up to 1.0000 ETH");
|
||||
});
|
||||
});
|
||||
|
||||
describe("wait-tx is a view the popup may reopen onto", () => {
|
||||
// The resume feature is wired through RESTORABLE_VIEWS: restoreView()
|
||||
// refuses any view not in the set, so dropping "wait-tx" from it kills
|
||||
|
||||
Reference in New Issue
Block a user