Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39729465d0 | ||
|
|
bec20aa2bb |
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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>`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user