Compare commits

..
2 Commits
Author SHA1 Message Date
sneak 39729465d0 fix: decode Uniswap V2 exact-out swaps, input amount shown as a maximum (closes #283)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
decode() had no arm for Universal Router command 0x09, so the approval
screen showed no token or amount for it. It now takes the path's first
token and amountInMax as the input side, the last token and amountOut as
the output side. With such a step, the Amount figure reads "Up to
<amount>" whichever step set it, on the approval, wait, success and error
screens, unless it reads "Unlimited", as an unbounded PERMIT2_PERMIT does,
or "All available (V4 open delta)". In every swap, UNWRAP_WETH makes
Token Out ETH only when the output side is WETH, on mainnet or Sepolia, or
no step set it; otherwise the output keeps its own token and figure.
decodeV2SwapExactOut() loses its eslint-disable comment.

Model: opus-5-5
2026-10-04 10:27:43 +00:00
clawbot bec20aa2bb fix: say a contract creation has no recipient instead of a blank line (closes #250)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
A transaction with no `to` showed a blank address, with a colour dot whose
colour was `undefined`, on the wait, success and error screens, the
transaction detail view and the history rows on Home, AddressDetail and
AddressToken. The approval screen showed "(contract creation)".

All of them now say "This transaction creates a new contract. It has no
recipient." The three history lists draw a row's counterparty lines through
one helper in helpers.js. A transaction with a real `to` is unchanged. The
new test drives each screen and list both ways.

Model: opus-5-5
2026-10-04 12:24:39 +02:00
11 changed files with 414 additions and 26 deletions
+14 -7
View File
@@ -1272,7 +1272,10 @@ view would leave a wallet one click from deletion.
of every wallet, deduplicated by hash and filtered. Each row is three of every wallet, deduplicated by hash and filtered. Each row is three
lines: age and direction, then the counterparty's colour dot (with our own lines: age and direction, then the counterparty's colour dot (with our own
name for it, where it is one of our addresses) and the amount, then the name for it, where it is one of our addresses) and the amount, then the
counterparty's full address on a row of its own counterparty's full address on a row of its own. A contract creation has
no counterparty: its second line is the amount alone and its third line
says "This transaction creates a new contract. It has no recipient." The
transaction lists on AddressDetail and AddressToken draw the same rows
- "Add additional wallet..." link at bottom - "Add additional wallet..." link at bottom
- **Transitions**: - **Transitions**:
- Tap address row → sets the active address and broadcasts - Tap address row → sets the active address and broadcasts
@@ -1465,7 +1468,9 @@ view would leave a wallet one click from deletion.
- **Elements**: - **Elements**:
- "Transaction Broadcast" heading (no back button — tx is irreversible) - "Transaction Broadcast" heading (no back button — tx is irreversible)
- Amount + symbol - Amount + symbol
- To: color dot + full address + etherscan link - To: color dot + full address + etherscan link; for a contract creation,
which has no recipient, "This transaction creates a new contract. It has
no recipient." instead
- Transaction hash: full hash (tap to copy) + etherscan link - Transaction hash: full hash (tap to copy) + etherscan link
- Count-up timer: "Waiting for confirmation... Ns" - Count-up timer: "Waiting for confirmation... Ns"
- **Behavior**: Polls `getTransactionReceipt` every 10 seconds. The wait is - **Behavior**: Polls `getTransactionReceipt` every 10 seconds. The wait is
@@ -1494,7 +1499,8 @@ view would leave a wallet one click from deletion.
- Decoded action well (shown when the transaction carried recognized - Decoded action well (shown when the transaction carried recognized
calldata; the top-level Amount and To are hidden in that case) calldata; the top-level Amount and To are hidden in that case)
- Amount + symbol - Amount + symbol
- To: color dot + full address + etherscan link - To: color dot + full address + etherscan link, or for a contract creation
the same sentence as on WaitTx
- Block number - Block number
- Transaction hash: full hash (tap to copy) + etherscan link - Transaction hash: full hash (tap to copy) + etherscan link
- "Done" button - "Done" button
@@ -1509,7 +1515,8 @@ view would leave a wallet one click from deletion.
- **Elements**: - **Elements**:
- "Transaction Failed" heading - "Transaction Failed" heading
- Amount + symbol - Amount + symbol
- To: color dot + full address + etherscan link - To: color dot + full address + etherscan link, or for a contract creation
the same sentence as on WaitTx
- Error message (dashed border box) - Error message (dashed border box)
- Transaction hash section (hidden if broadcast failed before getting hash): - Transaction hash section (hidden if broadcast failed before getting hash):
full hash (tap to copy) + etherscan link full hash (tap to copy) + etherscan link
@@ -1547,7 +1554,7 @@ view would leave a wallet one click from deletion.
- From: blockie + color dot + full address (tap to copy) + etherscan link; - From: blockie + color dot + full address (tap to copy) + etherscan link;
ENS name if available ENS name if available
- To: blockie + color dot + full address (tap to copy) + etherscan link; ENS - To: blockie + color dot + full address (tap to copy) + etherscan link; ENS
name if available name if available. For a contract creation, the same sentence as on WaitTx
- Time: ISO datetime + relative age in parentheses - Time: ISO datetime + relative age in parentheses
- Block: block number (tap to copy) + etherscan block link - Block: block number (tap to copy) + etherscan block link
- Amount: value + symbol (bold) - Amount: value + symbol (bold)
@@ -1858,8 +1865,8 @@ view would leave a wallet one click from deletion.
- Decoded action (if calldata is recognized): action name, token details, - Decoded action (if calldata is recognized): action name, token details,
amounts, steps, deadline (see Transaction Decoding) amounts, steps, deadline (see Transaction Decoding)
- From: color dot + full address + etherscan link - From: color dot + full address + etherscan link
- Contract: color dot + full address + etherscan link (or "contract - Contract: color dot + full address + etherscan link, token symbol label if
creation"), token symbol label if known known; for a contract creation, the same sentence as on WaitTx
- Value: amount in ETH (4 decimal places, USD in parentheses) - Value: amount in ETH (4 decimal places, USD in parentheses)
- Network fee (max): gas limit × fee per gas in ETH (4 decimal places, USD - Network fee (max): gas limit × fee per gas in ETH (4 decimal places, USD
in parentheses), with the gas limit and the fee per gas in gwei below it in parentheses), with the gas limit and the fee per gas in gwei below it
+8
View File
@@ -60,6 +60,14 @@ but the review is broader than any of them.
`Token Out` and `Min. received` keep the output side's own token and figure. `Token Out` and `Min. received` keep the output side's own token and figure.
V3 exact-out (`0x01`) is still not decoded. V3 exact-out (`0x01`) is still not decoded.
- 2026-10-04: A transaction with no `to` says "This transaction creates a new
contract. It has no recipient." on its recipient line and in its transaction
history row ([#250](https://git.eeqj.de/sneak/AutistMask/issues/250)). The
wait, success and error screens, the transaction detail view and the history
rows on Home, AddressDetail and AddressToken showed a blank address there,
with a colour dot whose colour was `undefined`; the approval screen showed
"(contract creation)". A transaction with a real `to` is unchanged.
- 2026-10-04: The Send and confirmation screens no longer show an ETH balance, a - 2026-10-04: The Send and confirmation screens no longer show an ETH balance, a
token balance or a network fee below 0.000001 as zero token balance or a network fee below 0.000001 as zero
([#343](https://git.eeqj.de/sneak/AutistMask/issues/343)). The stored balances ([#343](https://git.eeqj.de/sneak/AutistMask/issues/343)). The stored balances
+2 -5
View File
@@ -3,7 +3,7 @@ const {
showView, showView,
showFlash, showFlash,
balanceLinesForAddress, balanceLinesForAddress,
addressDotHtml, txCounterpartyHtml,
addressTitle, addressTitle,
escapeHtml, escapeHtml,
displaySymbol, displaySymbol,
@@ -233,16 +233,13 @@ function renderTransactions(txs) {
// is shown whole; the title or ENS name, where there is one, names // is shown whole; the title or ENS name, where there is one, names
// it on the line above rather than replacing it. // it on the line above rather than replacing it.
const nameStr = escapeHtml(title || ensName || ""); const nameStr = escapeHtml(title || ensName || "");
const addrStr = escapeHtml(counterparty);
const dot = addressDotHtml(counterparty);
const err = tx.isError ? " (failed)" : ""; const err = tx.isError ? " (failed)" : "";
const opacity = tx.isError ? " opacity:0.5;" : ""; const opacity = tx.isError ? " opacity:0.5;" : "";
const ago = escapeHtml(timeAgo(tx.timestamp)); const ago = escapeHtml(timeAgo(tx.timestamp));
const iso = escapeHtml(isoDate(tx.timestamp)); const iso = escapeHtml(isoDate(tx.timestamp));
html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`; html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`;
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`; html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
html += `<div class="flex justify-between"><span class="flex items-center">${dot}${nameStr}</span><span>${amountStr}</span></div>`; html += txCounterpartyHtml(counterparty, nameStr, amountStr);
html += `<div class="am-address">${addrStr}</div>`;
html += `</div>`; html += `</div>`;
i++; i++;
} }
+2 -5
View File
@@ -6,7 +6,7 @@ const {
showView, showView,
showFlash, showFlash,
flashCopyFeedback, flashCopyFeedback,
addressDotHtml, txCounterpartyHtml,
addressTitle, addressTitle,
escapeHtml, escapeHtml,
displaySymbol, displaySymbol,
@@ -309,16 +309,13 @@ function renderTransactions(txs) {
// is shown whole; the title or ENS name, where there is one, names // is shown whole; the title or ENS name, where there is one, names
// it on the line above rather than replacing it. // it on the line above rather than replacing it.
const nameStr = escapeHtml(title || ensName || ""); const nameStr = escapeHtml(title || ensName || "");
const addrStr = escapeHtml(counterparty);
const dot = addressDotHtml(counterparty);
const err = tx.isError ? " (failed)" : ""; const err = tx.isError ? " (failed)" : "";
const opacity = tx.isError ? " opacity:0.5;" : ""; const opacity = tx.isError ? " opacity:0.5;" : "";
const ago = escapeHtml(timeAgo(tx.timestamp)); const ago = escapeHtml(timeAgo(tx.timestamp));
const iso = escapeHtml(isoDate(tx.timestamp)); const iso = escapeHtml(isoDate(tx.timestamp));
html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`; html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`;
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`; html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
html += `<div class="flex justify-between"><span class="flex items-center">${dot}${nameStr}</span><span>${amountStr}</span></div>`; html += txCounterpartyHtml(counterparty, nameStr, amountStr);
html += `<div class="am-address">${addrStr}</div>`;
html += `</div>`; html += `</div>`;
i++; i++;
} }
+2 -1
View File
@@ -1,6 +1,7 @@
const { const {
$, $,
addressTitle, addressTitle,
CONTRACT_CREATION_TEXT,
escapeHtml, escapeHtml,
showView, showView,
showError, showError,
@@ -318,7 +319,7 @@ function showTxApproval(details) {
toHtml += approvalAddressHtml(toAddr); toHtml += approvalAddressHtml(toAddr);
$("approve-tx-to").innerHTML = toHtml; $("approve-tx-to").innerHTML = toHtml;
} else { } else {
$("approve-tx-to").innerHTML = escapeHtml("(contract creation)"); $("approve-tx-to").innerHTML = escapeHtml(CONTRACT_CREATION_TEXT);
} }
const ethValueFormatted = formatTxValue( const ethValueFormatted = formatTxValue(
+25
View File
@@ -431,6 +431,29 @@ function addressTitle(address, wallets) {
return null; return null;
} }
// What every recipient line and history row says for a transaction with no
// `to`. Such a transaction creates a contract, so there is no address to show,
// and a blank line on these screens reads as a rendering fault.
const CONTRACT_CREATION_TEXT =
"This transaction creates a new contract. It has no recipient.";
// The last two lines of a transaction history row: the counterparty's colour
// dot and name beside the amount, then its full address. A contract creation
// the user sent has no counterparty (its `to` is ""), so its row has the
// amount alone and the contract creation sentence in place of the address.
function txCounterpartyHtml(address, nameHtml, amountHtml) {
if (!address) {
return (
`<div class="flex justify-between"><span></span><span>${amountHtml}</span></div>` +
`<div>${escapeHtml(CONTRACT_CREATION_TEXT)}</div>`
);
}
return (
`<div class="flex justify-between"><span class="flex items-center">${addressDotHtml(address)}${nameHtml}</span><span>${amountHtml}</span></div>` +
`<div class="am-address">${escapeHtml(address)}</div>`
);
}
// Render an address with color dot, optional ENS name, optional title, // Render an address with color dot, optional ENS name, optional title,
// and optional truncation. Title and ENS are shown as bold labels above // and optional truncation. Title and ENS are shown as bold labels above
// the full address. // the full address.
@@ -643,6 +666,8 @@ module.exports = {
escapeHtml, escapeHtml,
displaySymbol, displaySymbol,
addressTitle, addressTitle,
CONTRACT_CREATION_TEXT,
txCounterpartyHtml,
formatAddressHtml, formatAddressHtml,
renderAddressHtml, renderAddressHtml,
copyableHtml, copyableHtml,
+2 -4
View File
@@ -6,6 +6,7 @@ const {
isoDate, isoDate,
timeAgo, timeAgo,
addressDotHtml, addressDotHtml,
txCounterpartyHtml,
addressTitle, addressTitle,
escapeHtml, escapeHtml,
displaySymbol, displaySymbol,
@@ -122,16 +123,13 @@ function renderHomeTxList(ctx) {
// names it on the line above rather than replacing it. // names it on the line above rather than replacing it.
const title = addressTitle(counterparty, state.wallets); const title = addressTitle(counterparty, state.wallets);
const titleStr = title ? escapeHtml(title) : ""; const titleStr = title ? escapeHtml(title) : "";
const addrStr = escapeHtml(counterparty);
const dot = addressDotHtml(counterparty);
const err = tx.isError ? " (failed)" : ""; const err = tx.isError ? " (failed)" : "";
const opacity = tx.isError ? " opacity:0.5;" : ""; const opacity = tx.isError ? " opacity:0.5;" : "";
const ago = escapeHtml(timeAgo(tx.timestamp)); const ago = escapeHtml(timeAgo(tx.timestamp));
const iso = escapeHtml(isoDate(tx.timestamp)); const iso = escapeHtml(isoDate(tx.timestamp));
html += `<div class="home-tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`; html += `<div class="home-tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`;
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`; html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
html += `<div class="flex justify-between"><span class="flex items-center">${dot}${titleStr}</span><span>${amountStr}</span></div>`; html += txCounterpartyHtml(counterparty, titleStr, amountStr);
html += `<div class="am-address">${addrStr}</div>`;
html += `</div>`; html += `</div>`;
i++; i++;
} }
+8 -2
View File
@@ -7,6 +7,7 @@ const {
showFlash, showFlash,
flashCopyFeedback, flashCopyFeedback,
addressTitle, addressTitle,
CONTRACT_CREATION_TEXT,
addressDotHtml, addressDotHtml,
escapeHtml, escapeHtml,
isoDate, isoDate,
@@ -94,13 +95,18 @@ function render() {
$("tx-detail-hash").innerHTML = txHashHtml(tx.hash); $("tx-detail-hash").innerHTML = txHashHtml(tx.hash);
const fromTitle = addressTitle(tx.from, state.wallets); const fromTitle = addressTitle(tx.from, state.wallets);
const toTitle = addressTitle(tx.to, state.wallets);
$("tx-detail-from").innerHTML = txAddressHtml( $("tx-detail-from").innerHTML = txAddressHtml(
tx.from, tx.from,
tx.fromEns, tx.fromEns,
fromTitle, fromTitle,
); );
$("tx-detail-to").innerHTML = txAddressHtml(tx.to, tx.toEns, toTitle); // A contract creation has no recipient: transactions.js gives it `to: ""`.
if (tx.to) {
const toTitle = addressTitle(tx.to, state.wallets);
$("tx-detail-to").innerHTML = txAddressHtml(tx.to, tx.toEns, toTitle);
} else {
$("tx-detail-to").innerHTML = escapeHtml(CONTRACT_CREATION_TEXT);
}
// Exact amount (full precision, copyable) // Exact amount (full precision, copyable)
const detailSym = displaySymbol(tx.symbol); const detailSym = displaySymbol(tx.symbol);
+7 -2
View File
@@ -4,6 +4,7 @@ const {
$, $,
showView, showView,
addressTitle, addressTitle,
CONTRACT_CREATION_TEXT,
escapeHtml, escapeHtml,
renderAddressHtml, renderAddressHtml,
attachCopyHandlers, attachCopyHandlers,
@@ -58,7 +59,10 @@ function endWait() {
} }
} }
// A contract creation reaches these screens with `to` as "" (approval.js
// writes `to: toAddr || ""`).
function toAddressHtml(address) { function toAddressHtml(address) {
if (!address) return escapeHtml(CONTRACT_CREATION_TEXT);
const title = addressTitle(address, state.wallets); const title = addressTitle(address, state.wallets);
return renderAddressHtml(address, { title }); return renderAddressHtml(address, { title });
} }
@@ -202,8 +206,9 @@ function restoreWait() {
if (!info || typeof info !== "object" || Array.isArray(info)) return false; if (!info || typeof info !== "object" || Array.isArray(info)) return false;
// A string is the whole requirement: the empty string is what a // A string is the whole requirement: the empty string is what a
// contract-deployment approval persists (approval.js writes `to: toAddr // contract-deployment approval persists (approval.js writes `to: toAddr
// || ""`), and both fields render harmlessly when empty, so refusing it // || ""`), an empty `to` renders as a contract creation and an empty
// would abandon a wait the live path itself created. // amount renders harmlessly, so refusing it would abandon a wait the live
// path itself created.
if (typeof info.to !== "string") return false; if (typeof info.to !== "string") return false;
if (typeof info.amount !== "string") return false; if (typeof info.amount !== "string") return false;
if (typeof w.broadcastTime !== "number" || !isFinite(w.broadcastTime)) { if (typeof w.broadcastTime !== "number" || !isFinite(w.broadcastTime)) {
+315
View File
@@ -0,0 +1,315 @@
// The recipient line of a contract creation
// (https://git.eeqj.de/sneak/AutistMask/issues/250).
//
// A transaction with no `to` creates a contract. The approval screen, the
// wait, success and error screens, the transaction detail view and the
// transaction history rows each say so in a sentence, where they used to show
// a blank line (an empty address, with a colour dot whose colour was
// `undefined`) or, on the approval screen, "(contract creation)". A
// transaction with a real `to` still shows that address.
//
// Driven against a minimal DOM stub in the shape
// tests/typedDataPermit.test.js uses.
jest.mock("../src/shared/log", () => ({
log: {
debugf: () => {},
infof: () => {},
warnf: () => {},
errorf: () => {},
},
// The transaction detail view fetches on-chain details after drawing; an
// answer that is not ok leaves the drawn lines as they are.
debugFetch: async () => ({ ok: false }),
setRuntimeDebug: () => {},
isDebug: () => false,
}));
// The wait screen polls for a receipt; this one never arrives.
jest.mock("../src/shared/balances", () => ({
getProvider: () => ({ getTransactionReceipt: () => new Promise(() => {}) }),
refreshBalances: () => {},
}));
// The history lists ask the explorer for their transactions and resolve ENS
// names for them; here the explorer answers with mockHistory and no name
// resolves.
let mockHistory = [];
jest.mock("../src/shared/transactions", () => ({
...jest.requireActual("../src/shared/transactions"),
fetchRecentTransactions: async () => mockHistory,
}));
jest.mock("../src/shared/ens", () => ({
...jest.requireActual("../src/shared/ens"),
resolveEnsNames: async () => new Map(),
}));
globalThis.chrome = {
storage: { local: { get: async () => ({}), set: async () => {} } },
};
const { state } = require("../src/shared/state");
const approval = require("../src/popup/views/approval");
const txStatus = require("../src/popup/views/txStatus");
const transactionDetail = require("../src/popup/views/transactionDetail");
const home = require("../src/popup/views/home");
const addressDetail = require("../src/popup/views/addressDetail");
const addressToken = require("../src/popup/views/addressToken");
const SENTENCE =
"This transaction creates a new contract. It has no recipient.";
const FROM = "0x0000000000000000000000000000000000000a11";
const RECIPIENT = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
const TX_HASH =
"0x85215772ed26ea8b39c2b3b18779030487efbe0b5fd7e882592b2f62b837be84";
// Init code for a contract creation's data.
const INIT_CODE = "0x600160005500";
function makeElement(id) {
const classes = new Set();
const el = {
id,
textContent: "",
value: "",
innerHTML: "",
disabled: false,
style: {},
dataset: {},
classList: {
add: (...names) => names.forEach((n) => classes.add(n)),
remove: (...names) => names.forEach((n) => classes.delete(n)),
contains: (n) => classes.has(n),
toggle: (n, force) => {
const on = force === undefined ? !classes.has(n) : force;
if (on) classes.add(n);
else classes.delete(n);
return on;
},
},
addEventListener: () => {},
querySelectorAll: () => [],
appendChild: () => {},
};
// Views reach for .parentElement to hide whole sections.
Object.defineProperty(el, "parentElement", {
get: () => node(id + "-parent"),
});
return el;
}
function makeDocument() {
const els = new Map();
return {
getElementById(id) {
// The debug banner is created on demand by helpers.js; absent
// is the state a non-debug, non-testnet popup is in.
if (id === "debug-banner") return null;
if (!els.has(id)) els.set(id, makeElement(id));
return els.get(id);
},
createElement: () => makeElement("created"),
body: { prepend: () => {} },
};
}
function node(id) {
return globalThis.document.getElementById(id);
}
// The line a transaction with a real `to` shows: that address, and nothing
// left over from an empty one.
function expectAddressLine(html) {
expect(html).toContain(RECIPIENT);
expect(html).not.toContain(SENTENCE);
expect(html).not.toContain("undefined");
}
beforeEach(() => {
globalThis.document = makeDocument();
globalThis.window = { location: { search: "" } };
state.wallets = [];
state.trackedTokens = [];
state.viewData = {};
state.viewStack = [];
state.currentView = null;
txStatus.init({ doRefreshAndRender: () => {} });
});
afterEach(() => {
txStatus.endWait();
});
// Open the transaction approval screen the way the popup does: the background
// hands over the populated transaction and show() draws it.
async function openTxApproval(to, data) {
globalThis.chrome.runtime = {
connect: () => ({ postMessage: () => {} }),
sendMessage: (msg, reply) => {
if (!reply) return;
if (msg.type !== "AUTISTMASK_GET_APPROVAL") return reply(null);
reply({
type: "tx",
hostname: "dapp.example",
isPhishingDomain: false,
approvedFrom: FROM,
approvedTx: {
type: 2,
from: FROM,
chainId: "0x1",
nonce: "0x7",
gasLimit: "0x5208",
maxPriorityFeePerGas: "0x3b9aca00",
maxFeePerGas: "0x77359400",
to,
value: "0x0",
data,
accessList: [],
},
});
},
};
approval.init({});
await approval.show(1);
}
describe("the transaction approval screen", () => {
test("a contract creation says so instead of naming a contract", async () => {
await openTxApproval(null, INIT_CODE);
expect(node("approve-tx-to").innerHTML).toBe(SENTENCE);
});
test("a transaction with a recipient shows its address", async () => {
await openTxApproval(RECIPIENT, "0x");
expectAddressLine(node("approve-tx-to").innerHTML);
});
});
// approval.js carries a contract creation to these screens with `to` as "".
describe("the wait, success and error screens", () => {
const creation = {
to: "",
amount: "0.0000",
token: "ETH",
tokenSymbol: null,
};
const transfer = { ...creation, to: RECIPIENT };
test("a contract creation says so on the wait screen", () => {
txStatus.showWait(creation, TX_HASH);
expect(node("wait-tx-to").innerHTML).toBe(SENTENCE);
});
test("a transaction with a recipient shows its address on the wait screen", () => {
txStatus.showWait(transfer, TX_HASH);
expectAddressLine(node("wait-tx-to").innerHTML);
});
test("a contract creation says so on the success and error screens", () => {
state.viewData = {
amount: "0.0000",
symbol: "ETH",
to: "",
hash: TX_HASH,
blockNumber: 1,
};
txStatus.renderSuccess();
expect(node("success-tx-to").innerHTML).toBe(SENTENCE);
txStatus.showError(creation, TX_HASH, "The transaction failed.");
expect(node("error-tx-to").innerHTML).toBe(SENTENCE);
});
test("a transaction with a recipient shows its address on the success and error screens", () => {
state.viewData = {
amount: "0.0050",
symbol: "ETH",
to: RECIPIENT,
hash: TX_HASH,
blockNumber: 1,
};
txStatus.renderSuccess();
expectAddressLine(node("success-tx-to").innerHTML);
txStatus.showError(transfer, TX_HASH, "The transaction failed.");
expectAddressLine(node("error-tx-to").innerHTML);
});
});
// A transaction FROM sent, as the history lists hold it. The explorer reports a
// contract creation with no `to`, which src/shared/transactions.js turns into
// `to: ""`.
function historyTx(to) {
return {
hash: TX_HASH,
from: FROM,
to,
value: "0.0000",
exactValue: "0.0",
rawAmount: "0",
rawUnit: "wei",
symbol: "ETH",
timestamp: 1790000000,
isError: false,
directionLabel: "Sent",
direction: "sent",
contractAddress: null,
};
}
// The detail view is opened with the transaction a history row holds.
describe("the transaction detail view", () => {
test("a contract creation says so", () => {
transactionDetail.show(historyTx(""));
expect(node("tx-detail-to").innerHTML).toBe(SENTENCE);
expect(node("tx-detail-type").textContent).toBe("Contract Creation");
});
test("a transaction with a recipient shows its address", () => {
transactionDetail.show(historyTx(RECIPIENT));
expectAddressLine(node("tx-detail-to").innerHTML);
});
});
// The same rows are drawn on Home, AddressDetail and AddressToken (for ETH).
describe.each([
["Home", "home-tx-list", () => home.render({})],
["AddressDetail", "tx-list", () => addressDetail.show()],
["AddressToken", "address-token-tx-list", () => addressToken.show()],
])("the transaction history on %s", (_name, listId, open) => {
async function rowsFor(tx) {
mockHistory = [tx];
open();
// The list is drawn once the history has been fetched.
await new Promise((resolve) => setTimeout(resolve, 0));
return node(listId).innerHTML;
}
beforeEach(() => {
state.wallets = [
{
name: "Main",
type: "key",
addresses: [{ address: FROM, balance: "0.0000" }],
},
];
state.selectedWallet = 0;
state.selectedAddress = 0;
state.selectedToken = "ETH";
});
test("a contract creation's row says so, with no colour dot and no address line", async () => {
const html = await rowsFor(historyTx(""));
expect(html).toContain(SENTENCE);
expect(html).not.toContain("background:");
expect(html).not.toContain("am-address");
expect(html).not.toContain("undefined");
});
test("a transaction with a recipient shows its colour dot and address", async () => {
const html = await rowsFor(historyTx(RECIPIENT));
expectAddressLine(html);
expect(html).toContain("background:#");
expect(html).toContain(`<div class="am-address">${RECIPIENT}</div>`);
});
});
+29
View File
@@ -431,6 +431,35 @@ describe("uniswap decoder", () => {
expect(detail(result, "Min. received").value).toBe("0.0005 ETH"); expect(detail(result, "Min. received").value).toBe("0.0005 ETH");
}); });
// A step that states a Min. received figure but names no output token has
// set the output side, so UNWRAP_WETH does not make it ETH: nothing says
// the figure is counted in WETH.
test("a step with a minimum but no output token, then UNWRAP_WETH, names no token", () => {
const data = buildExecute(
solidityPacked(["uint8", "uint8"], [0x10, 0x0c]),
[
encodeV4Swap(new Uint8Array([V4_SWAP_EXACT_IN]), [
encodeV4ExactIn(
USDC_ADDR,
[], // no path: this step names no output currency
1000000000n, // 1,000 USDC
400000000000000000n, // amountOutMin
),
]),
encodeWrapEth(USER_ADDR, 0n), // UNWRAP_WETH
],
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
expect(detail(result, "Token Out").value).toBe(
"Unknown (not named in the calldata)",
);
expect(detail(result, "Min. received").value).toBe(
"400000000000000000 base units (decimals unknown)",
);
});
test("decodes V3_SWAP_EXACT_IN with known tokens", () => { test("decodes V3_SWAP_EXACT_IN with known tokens", () => {
const data = buildExecute( const data = buildExecute(
"0x00", // V3_SWAP_EXACT_IN "0x00", // V3_SWAP_EXACT_IN