Compare commits
2
Commits
172f2c8a87
...
8f940b6481
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f940b6481 | ||
|
|
e46d894527 |
@@ -1891,14 +1891,17 @@ view would leave a wallet one click from deletion.
|
|||||||
no recipient." instead
|
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 screen
|
||||||
persisted: closing and reopening the popup resumes the poll, with the elapsed
|
describes the transaction on the network it was sent on, whichever network is
|
||||||
counter and the timeout deadline still measured from the original broadcast. A
|
active: the receipt is asked of that network's RPC endpoint, the one Settings
|
||||||
lookup that fails is retried on the next tick rather than counted as a missing
|
has for it, and the To and hash links open that network's explorer. The wait
|
||||||
receipt, because a failed lookup says nothing about the transaction; but six
|
is persisted: closing and reopening the popup resumes the poll, with the
|
||||||
failures in a row (60 seconds at the poll cadence) end the wait, so an RPC
|
elapsed counter and the timeout deadline still measured from the original
|
||||||
that never answers cannot leave it running indefinitely. Any lookup that
|
broadcast. A lookup that fails is retried on the next tick rather than counted
|
||||||
answers resets that count.
|
as a missing receipt, because a failed lookup says nothing about the
|
||||||
|
transaction; but six failures in a row (60 seconds at the poll cadence) end
|
||||||
|
the wait, so an RPC that never answers cannot leave it running indefinitely.
|
||||||
|
Any lookup that answers resets that count.
|
||||||
- **Transitions**:
|
- **Transitions**:
|
||||||
- Receipt found → **SuccessTx**
|
- Receipt found → **SuccessTx**
|
||||||
- A lookup that answers "no receipt" 60 seconds or more after broadcast →
|
- A lookup that answers "no receipt" 60 seconds or more after broadcast →
|
||||||
@@ -1963,7 +1966,8 @@ view would leave a wallet one click from deletion.
|
|||||||
#### TransactionDetail (`transaction`)
|
#### TransactionDetail (`transaction`)
|
||||||
|
|
||||||
- **When**: User tapped a transaction row on Home, AddressDetail, or
|
- **When**: User tapped a transaction row on Home, AddressDetail, or
|
||||||
AddressToken.
|
AddressToken. Its etherscan links open the explorer of the network the
|
||||||
|
transaction was sent on, whichever network is active.
|
||||||
- **Elements** (grouped into logical blocks using light well containers; field
|
- **Elements** (grouped into logical blocks using light well containers; field
|
||||||
labels are self-explanatory so groups have no headings):
|
labels are self-explanatory so groups have no headings):
|
||||||
- "Transaction" heading, "Back" button
|
- "Transaction" heading, "Back" button
|
||||||
@@ -2364,9 +2368,10 @@ view would leave a wallet one click from deletion.
|
|||||||
- Site origin (bold, scheme and port included) + "wants to send a
|
- Site origin (bold, scheme and port included) + "wants to send a
|
||||||
transaction"
|
transaction"
|
||||||
- Decoded action (if calldata is recognized): action name, token details,
|
- Decoded action (if calldata is recognized): action name, token details,
|
||||||
amounts, steps, deadline, and unless a swap pays only the From address, a
|
amounts, what each `PERMIT2_TRANSFER_FROM` moves and where to, steps,
|
||||||
`Paid to` line for each other address it pays, or saying that a token
|
deadline, and unless a swap pays only the From address, a `Paid to` line
|
||||||
stays in the router or who it pays is unknown (see Transaction Decoding)
|
for each other address it pays, or saying that a token stays in the router
|
||||||
|
or who it pays is unknown (see Transaction Decoding)
|
||||||
- From: color dot + full address + etherscan link
|
- From: color dot + full address + etherscan link
|
||||||
- Contract: color dot + full address + etherscan link, token symbol label if
|
- Contract: color dot + full address + etherscan link, token symbol label if
|
||||||
known; for a contract creation, the same sentence as on WaitTx
|
known; for a contract creation, the same sentence as on WaitTx
|
||||||
@@ -3116,9 +3121,19 @@ Currently supported:
|
|||||||
name); and one more reads `Unknown (could not be decoded)` when a step that
|
name); and one more reads `Unknown (could not be decoded)` when a step that
|
||||||
may pay an address went unread. When no step pays anyone, a single line reads
|
may pay an address went unread. When no step pays anyone, a single line reads
|
||||||
`Unknown (not named in the calldata)`. Wherever there is a `Paid to` line, the
|
`Unknown (not named in the calldata)`. Wherever there is a `Paid to` line, the
|
||||||
minimum reads `Min. paid out`. A step goes unread when its recipient does not
|
minimum reads `Min. paid out`. After the `Amount` line (after `Token In` when
|
||||||
decode as an address, or when it is a command the decoder does not read: every
|
there is none), each `PERMIT2_TRANSFER_FROM` but one into the router, in step
|
||||||
command other than `PERMIT2_PERMIT`, `PERMIT2_TRANSFER_FROM`,
|
order, gives three lines: `Moved from the sender`, the amount it moves, for
|
||||||
|
example `1000000.0000 USDT`; `Token moved`, giving the token's address as the
|
||||||
|
`Token In` line does; and `Moved to`, which reads
|
||||||
|
`The V2 pair the next swap spends` for a hop and `The sender` for `address(1)`
|
||||||
|
on every router but `UniversalRouterV1`, and otherwise gives the address. A
|
||||||
|
move into the router needs none of them, because its `Paid to` line and
|
||||||
|
`Token moved into the router` line already give the amount, the token and
|
||||||
|
where it goes, and so its amount is shown once. These lines change neither the
|
||||||
|
`Paid to` lines nor the minimum's label. A step goes unread when its recipient
|
||||||
|
does not decode as an address, or when it is a command the decoder does not
|
||||||
|
read: every command other than `PERMIT2_PERMIT`, `PERMIT2_TRANSFER_FROM`,
|
||||||
`V2_SWAP_EXACT_IN`, `V2_SWAP_EXACT_OUT`, `V3_SWAP_EXACT_IN`, `V4_SWAP`,
|
`V2_SWAP_EXACT_IN`, `V2_SWAP_EXACT_OUT`, `V3_SWAP_EXACT_IN`, `V4_SWAP`,
|
||||||
`SWEEP`, `TRANSFER`, `PAY_PORTION`, `PAY_PORTION_FULL_PRECISION`,
|
`SWEEP`, `TRANSFER`, `PAY_PORTION`, `PAY_PORTION_FULL_PRECISION`,
|
||||||
`ACROSS_V4_DEPOSIT_V3`, `WRAP_ETH`, `UNWRAP_WETH` and `BALANCE_CHECK_ERC20`
|
`ACROSS_V4_DEPOSIT_V3`, `WRAP_ETH`, `UNWRAP_WETH` and `BALANCE_CHECK_ERC20`
|
||||||
|
|||||||
@@ -145,12 +145,15 @@ function isRenderablePendingTx(value) {
|
|||||||
|
|
||||||
// The stored transaction transactionDetail.render() shows. contractAddress is
|
// The stored transaction transactionDetail.render() shows. contractAddress is
|
||||||
// optional on an ETH transfer, and reaches addressDotHtml() when it is there.
|
// optional on an ETH transfer, and reaches addressDotHtml() when it is there.
|
||||||
|
// `chainId` picks the network whose explorer the links open, so when it is
|
||||||
|
// there it has to name one.
|
||||||
function isRenderableTx(value) {
|
function isRenderableTx(value) {
|
||||||
return (
|
return (
|
||||||
isRecord(value) &&
|
isRecord(value) &&
|
||||||
isAddressText(value.from) &&
|
isAddressText(value.from) &&
|
||||||
isAddressText(value.to) &&
|
isAddressText(value.to) &&
|
||||||
(!value.contractAddress || isAddressText(value.contractAddress))
|
(!value.contractAddress || isAddressText(value.contractAddress)) &&
|
||||||
|
(!value.chainId || SUPPORTED_CHAIN_IDS.has(value.chainId))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,12 +23,10 @@ const {
|
|||||||
goBack,
|
goBack,
|
||||||
onViewLeave,
|
onViewLeave,
|
||||||
formatFee,
|
formatFee,
|
||||||
|
reviewedNetworkProvider,
|
||||||
} = require("./helpers");
|
} = require("./helpers");
|
||||||
const { state, currentNetwork } = require("../../shared/state");
|
const { state, currentNetwork } = require("../../shared/state");
|
||||||
const {
|
const { nativeCurrencyByChainId } = require("../../shared/networks");
|
||||||
networkByChainId,
|
|
||||||
nativeCurrencyByChainId,
|
|
||||||
} = require("../../shared/networks");
|
|
||||||
const { getSignerForAddress } = require("../../shared/wallet");
|
const { getSignerForAddress } = require("../../shared/wallet");
|
||||||
const { walletDefect } = require("../../shared/walletDefects");
|
const { walletDefect } = require("../../shared/walletDefects");
|
||||||
const { decryptWithPassword } = require("../../shared/vault");
|
const { decryptWithPassword } = require("../../shared/vault");
|
||||||
@@ -458,20 +456,6 @@ function setVisible(id, visible) {
|
|||||||
$(id).style.visibility = visible ? "visible" : "hidden";
|
$(id).style.visibility = visible ? "visible" : "hidden";
|
||||||
}
|
}
|
||||||
|
|
||||||
// The node of the network this transaction was reviewed for, which need not be
|
|
||||||
// the active one: the popup can reopen onto this screen after Settings switched
|
|
||||||
// the network. Its RPC URL is the one Settings has for that network: the
|
|
||||||
// active network's is state.rpcUrl, any other's the one remembered for it (see
|
|
||||||
// applyChainSwitchFields()).
|
|
||||||
function reviewedNetworkProvider(txInfo) {
|
|
||||||
const net = networkByChainId(txInfo.chainId);
|
|
||||||
if (net.id === state.networkId) {
|
|
||||||
return getProvider(state.rpcUrl, net.id);
|
|
||||||
}
|
|
||||||
const remembered = state.networkEndpoints[net.id] || {};
|
|
||||||
return getProvider(remembered.rpcUrl || net.defaultRpcUrl, net.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function estimateGas(txInfo) {
|
async function estimateGas(txInfo) {
|
||||||
try {
|
try {
|
||||||
const provider = reviewedNetworkProvider(txInfo);
|
const provider = reviewedNetworkProvider(txInfo);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const {
|
|||||||
isBelowOneMillionth,
|
isBelowOneMillionth,
|
||||||
} = require("../../shared/amountDisplay");
|
} = require("../../shared/amountDisplay");
|
||||||
const { resolveTokenSymbol } = require("../../shared/approvalAmount");
|
const { resolveTokenSymbol } = require("../../shared/approvalAmount");
|
||||||
|
const { getProvider } = require("../../shared/balances");
|
||||||
const { DEBUG } = require("../../shared/constants");
|
const { DEBUG } = require("../../shared/constants");
|
||||||
const { escapeHtml } = require("../../shared/html");
|
const { escapeHtml } = require("../../shared/html");
|
||||||
const { isDebug } = require("../../shared/log");
|
const { isDebug } = require("../../shared/log");
|
||||||
@@ -607,6 +608,20 @@ const EXT_ICON =
|
|||||||
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
|
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
|
||||||
`</svg></span>`;
|
`</svg></span>`;
|
||||||
|
|
||||||
|
// The node of the network a transaction was reviewed for, the one its chain id
|
||||||
|
// names, which need not be the active one: the popup can reopen onto ConfirmTx
|
||||||
|
// or WaitTx after Settings switched the network. Its RPC URL is the one
|
||||||
|
// Settings has for that network: the active network's is state.rpcUrl, any
|
||||||
|
// other's the one remembered for it (see applyChainSwitchFields()).
|
||||||
|
function reviewedNetworkProvider(txInfo) {
|
||||||
|
const net = networkByChainId(txInfo.chainId);
|
||||||
|
if (net.id === state.networkId) {
|
||||||
|
return getProvider(state.rpcUrl, net.id);
|
||||||
|
}
|
||||||
|
const remembered = state.networkEndpoints[net.id] || {};
|
||||||
|
return getProvider(remembered.rpcUrl || net.defaultRpcUrl, net.id);
|
||||||
|
}
|
||||||
|
|
||||||
// Block-explorer URLs. The origin is a per-network constant from
|
// Block-explorer URLs. The origin is a per-network constant from
|
||||||
// src/shared/networks.js, the explorer of the network with `chainId`, or of
|
// src/shared/networks.js, the explorer of the network with `chainId`, or of
|
||||||
// the active network when no chain id is given; only the path segment is
|
// the active network when no chain id is given; only the path segment is
|
||||||
@@ -762,6 +777,7 @@ module.exports = {
|
|||||||
attachCopyHandlers,
|
attachCopyHandlers,
|
||||||
etherscanLinkHtml,
|
etherscanLinkHtml,
|
||||||
explorerUrl,
|
explorerUrl,
|
||||||
|
reviewedNetworkProvider,
|
||||||
EXT_ICON,
|
EXT_ICON,
|
||||||
truncateMiddle,
|
truncateMiddle,
|
||||||
isoDate,
|
isoDate,
|
||||||
|
|||||||
@@ -48,16 +48,16 @@ function getTransactionType(tx) {
|
|||||||
return "Native " + nativeCurrencyByChainId(tx.chainId) + " Transfer";
|
return "Native " + nativeCurrencyByChainId(tx.chainId) + " Transfer";
|
||||||
}
|
}
|
||||||
|
|
||||||
function txAddressHtml(address, ensName, title) {
|
function txAddressHtml(address, ensName, title, chainId) {
|
||||||
const blockie = blockieHtml(address);
|
const blockie = blockieHtml(address);
|
||||||
return (
|
return (
|
||||||
`<div class="mb-1">${blockie}</div>` +
|
`<div class="mb-1">${blockie}</div>` +
|
||||||
renderAddressHtml(address, { title, ensName })
|
renderAddressHtml(address, { title, ensName, chainId })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function txHashHtml(hash) {
|
function txHashHtml(hash, chainId) {
|
||||||
const link = explorerUrl("tx", hash);
|
const link = explorerUrl("tx", hash, chainId);
|
||||||
const extLink = etherscanLinkHtml(link);
|
const extLink = etherscanLinkHtml(link);
|
||||||
return copyableHtml(hash, "break-all") + extLink;
|
return copyableHtml(hash, "break-all") + extLink;
|
||||||
}
|
}
|
||||||
@@ -83,10 +83,10 @@ function show(tx) {
|
|||||||
method: tx.method || null,
|
method: tx.method || null,
|
||||||
contractAddress: tx.contractAddress || null,
|
contractAddress: tx.contractAddress || null,
|
||||||
// The network the history entry was read from. The type line and
|
// The network the history entry was read from. The type line and
|
||||||
// the fee are in its native currency, not the active network's:
|
// the fee are in its native currency, not the active network's,
|
||||||
// the active network can change, in Settings or when the user
|
// and the links open its explorer: the active network can change,
|
||||||
// approves a site's request, before a later popup shows this
|
// in Settings or when the user approves a site's request, before
|
||||||
// screen again.
|
// a later popup shows this screen again.
|
||||||
chainId: tx.chainId,
|
chainId: tx.chainId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -96,18 +96,24 @@ function show(tx) {
|
|||||||
function render() {
|
function render() {
|
||||||
const tx = state.viewData.tx;
|
const tx = state.viewData.tx;
|
||||||
if (!tx) return;
|
if (!tx) return;
|
||||||
$("tx-detail-hash").innerHTML = txHashHtml(tx.hash);
|
$("tx-detail-hash").innerHTML = txHashHtml(tx.hash, tx.chainId);
|
||||||
|
|
||||||
const fromTitle = addressTitle(tx.from, state.wallets);
|
const fromTitle = addressTitle(tx.from, state.wallets);
|
||||||
$("tx-detail-from").innerHTML = txAddressHtml(
|
$("tx-detail-from").innerHTML = txAddressHtml(
|
||||||
tx.from,
|
tx.from,
|
||||||
tx.fromEns,
|
tx.fromEns,
|
||||||
fromTitle,
|
fromTitle,
|
||||||
|
tx.chainId,
|
||||||
);
|
);
|
||||||
// A contract creation has no recipient: transactions.js gives it `to: ""`.
|
// A contract creation has no recipient: transactions.js gives it `to: ""`.
|
||||||
if (tx.to) {
|
if (tx.to) {
|
||||||
const toTitle = addressTitle(tx.to, state.wallets);
|
const toTitle = addressTitle(tx.to, state.wallets);
|
||||||
$("tx-detail-to").innerHTML = txAddressHtml(tx.to, tx.toEns, toTitle);
|
$("tx-detail-to").innerHTML = txAddressHtml(
|
||||||
|
tx.to,
|
||||||
|
tx.toEns,
|
||||||
|
toTitle,
|
||||||
|
tx.chainId,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
$("tx-detail-to").innerHTML = escapeHtml(CONTRACT_CREATION_TEXT);
|
$("tx-detail-to").innerHTML = escapeHtml(CONTRACT_CREATION_TEXT);
|
||||||
}
|
}
|
||||||
@@ -146,7 +152,7 @@ function render() {
|
|||||||
if (tokenContractSection && tokenContractEl) {
|
if (tokenContractSection && tokenContractEl) {
|
||||||
if (tx.contractAddress) {
|
if (tx.contractAddress) {
|
||||||
const dot = addressDotHtml(tx.contractAddress);
|
const dot = addressDotHtml(tx.contractAddress);
|
||||||
const link = explorerUrl("token", tx.contractAddress);
|
const link = explorerUrl("token", tx.contractAddress, tx.chainId);
|
||||||
// Hand-rolled rather than renderAddressHtml() because the
|
// Hand-rolled rather than renderAddressHtml() because the
|
||||||
// link goes to the explorer's /token/ page, not /address/.
|
// link goes to the explorer's /token/ page, not /address/.
|
||||||
// Same two-row shape though: dot and link on the strip, the
|
// Same two-row shape though: dot and link on the strip, the
|
||||||
@@ -204,7 +210,11 @@ function showDetailField(sectionId, contentId, value) {
|
|||||||
function populateOnChainDetails(txData, chainId) {
|
function populateOnChainDetails(txData, chainId) {
|
||||||
// Block number
|
// Block number
|
||||||
if (txData.block_number != null) {
|
if (txData.block_number != null) {
|
||||||
const blockLink = explorerUrl("block", String(txData.block_number));
|
const blockLink = explorerUrl(
|
||||||
|
"block",
|
||||||
|
String(txData.block_number),
|
||||||
|
chainId,
|
||||||
|
);
|
||||||
const blockSection = $("tx-detail-block-section");
|
const blockSection = $("tx-detail-block-section");
|
||||||
const blockEl = $("tx-detail-block");
|
const blockEl = $("tx-detail-block");
|
||||||
if (blockSection && blockEl) {
|
if (blockSection && blockEl) {
|
||||||
@@ -340,9 +350,9 @@ async function loadFullTxDetails(txHash, chainId) {
|
|||||||
if (tokenSymbol) {
|
if (tokenSymbol) {
|
||||||
detailsHtml += `<div class="font-bold">${escapeHtml(displaySymbol(tokenSymbol))}</div>`;
|
detailsHtml += `<div class="font-bold">${escapeHtml(displaySymbol(tokenSymbol))}</div>`;
|
||||||
}
|
}
|
||||||
detailsHtml += renderAddressHtml(d.address);
|
detailsHtml += renderAddressHtml(d.address, { chainId });
|
||||||
} else if (d.address) {
|
} else if (d.address) {
|
||||||
detailsHtml += renderAddressHtml(d.address);
|
detailsHtml += renderAddressHtml(d.address, { chainId });
|
||||||
} else {
|
} else {
|
||||||
detailsHtml += `<div class="font-bold">${escapeHtml(d.value)}</div>`;
|
detailsHtml += `<div class="font-bold">${escapeHtml(d.value)}</div>`;
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-17
@@ -14,10 +14,13 @@ const {
|
|||||||
displaySymbol,
|
displaySymbol,
|
||||||
clearViewStack,
|
clearViewStack,
|
||||||
tokenLabel,
|
tokenLabel,
|
||||||
|
reviewedNetworkProvider,
|
||||||
} = require("./helpers");
|
} = require("./helpers");
|
||||||
const { state } = require("../../shared/state");
|
const { state } = require("../../shared/state");
|
||||||
const { nativeCurrencyByChainId } = require("../../shared/networks");
|
const {
|
||||||
const { getProvider } = require("../../shared/balances");
|
SUPPORTED_CHAIN_IDS,
|
||||||
|
nativeCurrencyByChainId,
|
||||||
|
} = require("../../shared/networks");
|
||||||
const { log } = require("../../shared/log");
|
const { log } = require("../../shared/log");
|
||||||
|
|
||||||
// Receipt poll cadence and the deadline after which the wait is reported as
|
// Receipt poll cadence and the deadline after which the wait is reported as
|
||||||
@@ -61,15 +64,16 @@ function endWait() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A contract creation reaches these screens with `to` as "" (approval.js
|
// A contract creation reaches these screens with `to` as "" (approval.js
|
||||||
// writes `to: toAddr || ""`).
|
// writes `to: toAddr || ""`). The link opens the explorer of the network with
|
||||||
function toAddressHtml(address) {
|
// `chainId`, or of the active network when none is given.
|
||||||
|
function toAddressHtml(address, chainId) {
|
||||||
if (!address) return escapeHtml(CONTRACT_CREATION_TEXT);
|
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, chainId });
|
||||||
}
|
}
|
||||||
|
|
||||||
function txHashHtml(hash) {
|
function txHashHtml(hash, chainId) {
|
||||||
const link = explorerUrl("tx", hash);
|
const link = explorerUrl("tx", hash, chainId);
|
||||||
return copyableHtml(hash, "break-all") + etherscanLinkHtml(link);
|
return copyableHtml(hash, "break-all") + etherscanLinkHtml(link);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,9 +95,11 @@ function startWait(txInfo, txHash, broadcastTime, pollNow) {
|
|||||||
// native currency of txInfo.chainId, the network the transaction was sent
|
// native currency of txInfo.chainId, the network the transaction was sent
|
||||||
// on, not the active network's: the active network can change, in
|
// on, not the active network's: the active network can change, in
|
||||||
// Settings or when the user approves a site's request, while this screen
|
// Settings or when the user approves a site's request, while this screen
|
||||||
// is open or before a later popup resumes it. A null token is a swap whose
|
// is open or before a later popup resumes it. This screen's links open
|
||||||
// amount was not decoded (see showTxApproval() in approval.js): the amount
|
// that network's explorer, and its receipt is asked of that network's
|
||||||
// says so, and no symbol follows it.
|
// node, for the same reason. A null token is a swap whose amount was not
|
||||||
|
// decoded (see showTxApproval() in approval.js): the amount says so, and
|
||||||
|
// no symbol follows it.
|
||||||
let symbol = "";
|
let symbol = "";
|
||||||
if (txInfo.token === "ETH") {
|
if (txInfo.token === "ETH") {
|
||||||
symbol = nativeCurrencyByChainId(txInfo.chainId);
|
symbol = nativeCurrencyByChainId(txInfo.chainId);
|
||||||
@@ -102,8 +108,8 @@ function startWait(txInfo, txHash, broadcastTime, pollNow) {
|
|||||||
}
|
}
|
||||||
$("wait-tx-summary").textContent =
|
$("wait-tx-summary").textContent =
|
||||||
txInfo.amount + (symbol ? " " + symbol : "");
|
txInfo.amount + (symbol ? " " + symbol : "");
|
||||||
$("wait-tx-to").innerHTML = toAddressHtml(txInfo.to);
|
$("wait-tx-to").innerHTML = toAddressHtml(txInfo.to, txInfo.chainId);
|
||||||
$("wait-tx-hash").innerHTML = txHashHtml(txHash);
|
$("wait-tx-hash").innerHTML = txHashHtml(txHash, txInfo.chainId);
|
||||||
attachCopyHandlers("view-wait-tx");
|
attachCopyHandlers("view-wait-tx");
|
||||||
|
|
||||||
// Persisted so closing and reopening the popup resumes this wait
|
// Persisted so closing and reopening the popup resumes this wait
|
||||||
@@ -128,7 +134,7 @@ function startWait(txInfo, txHash, broadcastTime, pollNow) {
|
|||||||
renderElapsed();
|
renderElapsed();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
const provider = getProvider(state.rpcUrl, state.networkId);
|
const provider = reviewedNetworkProvider(txInfo);
|
||||||
let consecutiveFailures = 0;
|
let consecutiveFailures = 0;
|
||||||
|
|
||||||
async function poll() {
|
async function poll() {
|
||||||
@@ -207,10 +213,11 @@ function showWait(txInfo, txHash) {
|
|||||||
// an object merely missing one of them throws a TypeError out of
|
// an object merely missing one of them throws a TypeError out of
|
||||||
// restoreView() — which init() does not guard, skipping the rest of popup
|
// restoreView() — which init() does not guard, skipping the rest of popup
|
||||||
// init and leaving wait-tx on screen with no back control. A non-numeric
|
// init and leaving wait-tx on screen with no back control. A non-numeric
|
||||||
// broadcastTime leaves an unexitable wait counting "NaNs". txInfo.token,
|
// broadcastTime leaves an unexitable wait counting "NaNs". txInfo.chainId picks
|
||||||
// txInfo.tokenSymbol and txInfo.chainId are deliberately unchecked: they are
|
// the node the receipt is asked of and the explorer the links open, so it has
|
||||||
// compared and coalesced rather than dereferenced, and tokenSymbol is null for
|
// to name a network. txInfo.token and txInfo.tokenSymbol are deliberately
|
||||||
// ETH.
|
// unchecked: they are compared and coalesced rather than dereferenced, and
|
||||||
|
// tokenSymbol is null for ETH.
|
||||||
function restoreWait() {
|
function restoreWait() {
|
||||||
const d = state.viewData;
|
const d = state.viewData;
|
||||||
if (!d || !d.pendingWait) return false;
|
if (!d || !d.pendingWait) return false;
|
||||||
@@ -226,6 +233,7 @@ function restoreWait() {
|
|||||||
// path itself created.
|
// 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 (!SUPPORTED_CHAIN_IDS.has(info.chainId)) return false;
|
||||||
if (typeof w.broadcastTime !== "number" || !isFinite(w.broadcastTime)) {
|
if (typeof w.broadcastTime !== "number" || !isFinite(w.broadcastTime)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
+66
-21
@@ -488,6 +488,21 @@ function amountText(raw, info) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The line giving a moved token under `label`, in the shape of the Token In
|
||||||
|
// line, so the screens show it as they show that one. ETH has no address.
|
||||||
|
function tokenMovedLine(label, token, sources) {
|
||||||
|
const info = tokenInfo(token, sources);
|
||||||
|
if (!present(info.address)) {
|
||||||
|
return { label, value: info.symbol };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
value: info.symbol ? info.symbol + " (" + token + ")" : token,
|
||||||
|
address: token,
|
||||||
|
isToken: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Decode PERMIT2_PERMIT (command 0x0a) input bytes.
|
// Decode PERMIT2_PERMIT (command 0x0a) input bytes.
|
||||||
// ABI: ((address token, uint160 amount, uint48 expiration, uint48 nonce),
|
// ABI: ((address token, uint160 amount, uint48 expiration, uint48 nonce),
|
||||||
// address spender, uint256 sigDeadline), bytes signature
|
// address spender, uint256 sigDeadline), bytes signature
|
||||||
@@ -1171,6 +1186,13 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// router, as { token, amount }. Later steps may leave part of it
|
// router, as { token, amount }. Later steps may leave part of it
|
||||||
// there: nothing here checks that they spend all of it.
|
// there: nothing here checks that they spend all of it.
|
||||||
const movedIntoRouter = [];
|
const movedIntoRouter = [];
|
||||||
|
// What each PERMIT2_TRANSFER_FROM not into the router moves from the
|
||||||
|
// sender, in step order, whoever counts as paid, as
|
||||||
|
// { token, recipient, amount, to }: `to` says where it goes, the
|
||||||
|
// sender or the V2 pair the next swap spends, and is null when it
|
||||||
|
// goes to `recipient` as written. A move into the router is in
|
||||||
|
// `movedIntoRouter`, whose lines give its amount already.
|
||||||
|
const movedFromSender = [];
|
||||||
// Each token a step paid to the router that no later step paid out
|
// Each token a step paid to the router that no later step paid out
|
||||||
// or spent in full, by its address, lowercased, ETH as the zero
|
// or spent in full, by its address, lowercased, ETH as the zero
|
||||||
// address. The router is paid what a swap step, V4 TAKE or
|
// address. The router is paid what a swap step, V4 TAKE or
|
||||||
@@ -1302,10 +1324,23 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// pays address(1) and address(2) as written (see
|
// pays address(1) and address(2) as written (see
|
||||||
// decodePermit2TransferFrom()).
|
// decodePermit2TransferFrom()).
|
||||||
addPaidElsewhere(p.recipient);
|
addPaidElsewhere(p.recipient);
|
||||||
|
movedFromSender.push({ ...p, to: null });
|
||||||
} else if (isRouter(p.recipient)) {
|
} else if (isRouter(p.recipient)) {
|
||||||
movedIntoRouter.push(p);
|
movedIntoRouter.push(p);
|
||||||
} else if (!isHop(i, p.token, p.recipient)) {
|
} else if (isHop(i, p.token, p.recipient)) {
|
||||||
|
movedFromSender.push({
|
||||||
|
...p,
|
||||||
|
to: "The V2 pair the next swap spends",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
addPayee(p.recipient);
|
addPayee(p.recipient);
|
||||||
|
movedFromSender.push({
|
||||||
|
...p,
|
||||||
|
to:
|
||||||
|
p.recipient.toLowerCase() === MSG_SENDER
|
||||||
|
? "The sender"
|
||||||
|
: null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (command === "BALANCE_CHECK_ERC20") {
|
} else if (command === "BALANCE_CHECK_ERC20") {
|
||||||
const b = decodeBalanceCheck(inputs[i]);
|
const b = decodeBalanceCheck(inputs[i]);
|
||||||
@@ -1675,6 +1710,26 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// What each PERMIT2_TRANSFER_FROM moves: its amount, its token and
|
||||||
|
// where it goes, an address on a line of its own.
|
||||||
|
for (const m of movedFromSender) {
|
||||||
|
details.push({
|
||||||
|
label: "Moved from the sender",
|
||||||
|
value: amountText(m.amount, tokenInfo(m.token, sources))
|
||||||
|
.display,
|
||||||
|
});
|
||||||
|
details.push(tokenMovedLine("Token moved", m.token, sources));
|
||||||
|
details.push(
|
||||||
|
present(m.to)
|
||||||
|
? { label: "Moved to", value: m.to }
|
||||||
|
: {
|
||||||
|
label: "Moved to",
|
||||||
|
value: m.recipient,
|
||||||
|
address: m.recipient,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Keyed on the address, not the symbol: a token absent from the
|
// Keyed on the address, not the symbol: a token absent from the
|
||||||
// bundled list has no symbol, and gating the line on one dropped it
|
// bundled list has no symbol, and gating the line on one dropped it
|
||||||
// entirely, leaving a Min. received figure with nothing saying what is
|
// entirely, leaving a Min. received figure with nothing saying what is
|
||||||
@@ -1723,24 +1778,6 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
if (bridgeDeposit) {
|
if (bridgeDeposit) {
|
||||||
paidTo.push({ label: "Paid to", value: PAID_TO_BRIDGE });
|
paidTo.push({ label: "Paid to", value: PAID_TO_BRIDGE });
|
||||||
}
|
}
|
||||||
// The line giving a token in the router, in the shape of the Token In
|
|
||||||
// line, so the screens show it as they show that one. ETH has no
|
|
||||||
// address.
|
|
||||||
const tokenMovedLine = (token) => {
|
|
||||||
const info = tokenInfo(token, sources);
|
|
||||||
if (!present(info.address)) {
|
|
||||||
return {
|
|
||||||
label: "Token moved into the router",
|
|
||||||
value: info.symbol,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
label: "Token moved into the router",
|
|
||||||
value: info.symbol ? info.symbol + " (" + token + ")" : token,
|
|
||||||
address: token,
|
|
||||||
isToken: true,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
for (const m of movedIntoRouter) {
|
for (const m of movedIntoRouter) {
|
||||||
const info = tokenInfo(m.token, sources);
|
const info = tokenInfo(m.token, sources);
|
||||||
paidTo.push({
|
paidTo.push({
|
||||||
@@ -1750,7 +1787,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
amountText(m.amount, info).display +
|
amountText(m.amount, info).display +
|
||||||
" from the sender (later steps may not spend all of it)",
|
" from the sender (later steps may not spend all of it)",
|
||||||
});
|
});
|
||||||
paidTo.push(tokenMovedLine(m.token));
|
paidTo.push(
|
||||||
|
tokenMovedLine("Token moved into the router", m.token, sources),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (outputHeld) {
|
if (outputHeld) {
|
||||||
paidTo.push({ label: "Paid to", value: STAYS_IN_ROUTER });
|
paidTo.push({ label: "Paid to", value: STAYS_IN_ROUTER });
|
||||||
@@ -1764,7 +1803,13 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
paidTo.push({ label: "Paid to", value: STAYS_IN_ROUTER });
|
paidTo.push({ label: "Paid to", value: STAYS_IN_ROUTER });
|
||||||
paidTo.push(tokenMovedLine(getAddress(token)));
|
paidTo.push(
|
||||||
|
tokenMovedLine(
|
||||||
|
"Token moved into the router",
|
||||||
|
getAddress(token),
|
||||||
|
sources,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (payeeUnread) {
|
if (payeeUnread) {
|
||||||
paidTo.push({ label: "Paid to", value: UNDECODED_CURRENCY });
|
paidTo.push({ label: "Paid to", value: UNDECODED_CURRENCY });
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ describe("the wait, success and error screens", () => {
|
|||||||
amount: "0.0000",
|
amount: "0.0000",
|
||||||
token: "ETH",
|
token: "ETH",
|
||||||
tokenSymbol: null,
|
tokenSymbol: null,
|
||||||
|
chainId: "0x1",
|
||||||
};
|
};
|
||||||
const transfer = { ...creation, to: RECIPIENT };
|
const transfer = { ...creation, to: RECIPIENT };
|
||||||
|
|
||||||
|
|||||||
@@ -459,6 +459,19 @@ const CONTRACT = [
|
|||||||
views: ["transaction"],
|
views: ["transaction"],
|
||||||
restored: false,
|
restored: false,
|
||||||
},
|
},
|
||||||
|
// A chain id naming no network has no explorer to link to.
|
||||||
|
{
|
||||||
|
value: {
|
||||||
|
tx: {
|
||||||
|
hash: "0x1",
|
||||||
|
from: ADDRESS,
|
||||||
|
to: ADDRESS,
|
||||||
|
chainId: "0x5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
views: ["transaction"],
|
||||||
|
restored: false,
|
||||||
|
},
|
||||||
// confirm-tx passes on `data.pendingTx`.
|
// confirm-tx passes on `data.pendingTx`.
|
||||||
{
|
{
|
||||||
value: { pendingTx: { amount: "1" } },
|
value: { pendingTx: { amount: "1" } },
|
||||||
@@ -821,7 +834,7 @@ const WELL_FORMED_DATA = {
|
|||||||
},
|
},
|
||||||
pendingWait: {
|
pendingWait: {
|
||||||
hash: "0x1",
|
hash: "0x1",
|
||||||
txInfo: { to: ADDRESS, amount: "1" },
|
txInfo: { to: ADDRESS, amount: "1", chainId: "0x1" },
|
||||||
broadcastTime: BROADCAST_TIME,
|
broadcastTime: BROADCAST_TIME,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -225,7 +225,12 @@ describe("a popup already open when the stored profile becomes unreadable", () =
|
|||||||
viewData: {
|
viewData: {
|
||||||
pendingWait: {
|
pendingWait: {
|
||||||
hash: "0x1",
|
hash: "0x1",
|
||||||
txInfo: { to: ADDRESS, amount: "1", token: "ETH" },
|
txInfo: {
|
||||||
|
to: ADDRESS,
|
||||||
|
amount: "1",
|
||||||
|
token: "ETH",
|
||||||
|
chainId: "0x1",
|
||||||
|
},
|
||||||
broadcastTime: Date.now(),
|
broadcastTime: Date.now(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
// tracks. It decodes against the address the explorer reports the transaction
|
// tracks. It decodes against the address the explorer reports the transaction
|
||||||
// was sent to.
|
// was sent to.
|
||||||
//
|
//
|
||||||
|
// Its links open the explorer of the network the transaction was sent on,
|
||||||
|
// whichever network is active
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/584).
|
||||||
|
//
|
||||||
// The row is the one the real history parser makes from the explorer's answer,
|
// The row is the one the real history parser makes from the explorer's answer,
|
||||||
// opened through transactionDetail.show(), which is what a click on the row
|
// opened through transactionDetail.show(), which is what a click on the row
|
||||||
// calls. Driven against a minimal DOM stub in the shape
|
// calls. Driven against a minimal DOM stub in the shape
|
||||||
@@ -35,6 +39,7 @@ globalThis.chrome = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { state } = require("../src/shared/state");
|
const { state } = require("../src/shared/state");
|
||||||
|
const { NETWORKS } = require("../src/shared/networks");
|
||||||
const { fetchRecentTransactions } = require("../src/shared/transactions");
|
const { fetchRecentTransactions } = require("../src/shared/transactions");
|
||||||
const transactionDetail = require("../src/popup/views/transactionDetail");
|
const transactionDetail = require("../src/popup/views/transactionDetail");
|
||||||
|
|
||||||
@@ -77,7 +82,11 @@ function mockExplorerAnswer(url) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (url === EXPLORER + "/transactions/" + TX_HASH) {
|
if (url === EXPLORER + "/transactions/" + TX_HASH) {
|
||||||
return { to: { hash: TOKEN }, raw_input: TRANSFER_DATA };
|
return {
|
||||||
|
to: { hash: TOKEN },
|
||||||
|
raw_input: TRANSFER_DATA,
|
||||||
|
block_number: 100,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return { items: [] };
|
return { items: [] };
|
||||||
}
|
}
|
||||||
@@ -154,7 +163,11 @@ beforeEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("a token transfer's decoded details name the token contract and the amount at its scale", async () => {
|
test("a token transfer's decoded details name the token contract and the amount at its scale", async () => {
|
||||||
const [row] = await fetchRecentTransactions(HOLDER, EXPLORER, 1);
|
const [row] = await fetchRecentTransactions(
|
||||||
|
HOLDER,
|
||||||
|
EXPLORER,
|
||||||
|
NETWORKS.mainnet.chainId,
|
||||||
|
);
|
||||||
expect(row.to).toBe(RECIPIENT);
|
expect(row.to).toBe(RECIPIENT);
|
||||||
expect(row.contractAddress).toBe(TOKEN.toLowerCase());
|
expect(row.contractAddress).toBe(TOKEN.toLowerCase());
|
||||||
|
|
||||||
@@ -174,3 +187,33 @@ test("a token transfer's decoded details name the token contract and the amount
|
|||||||
'<div class="font-bold">1234.5000 TDOL</div>',
|
'<div class="font-bold">1234.5000 TDOL</div>',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Mainnet is active and the row was read from Sepolia's explorer. The stub
|
||||||
|
// explorer answers whichever network asks, so every link on the screen is
|
||||||
|
// drawn.
|
||||||
|
test("with mainnet active, a Sepolia transaction's links open the Sepolia explorer", async () => {
|
||||||
|
state.networkId = "mainnet";
|
||||||
|
const [row] = await fetchRecentTransactions(
|
||||||
|
HOLDER,
|
||||||
|
EXPLORER,
|
||||||
|
NETWORKS.sepolia.chainId,
|
||||||
|
);
|
||||||
|
|
||||||
|
transactionDetail.show(row);
|
||||||
|
for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
|
const sepolia = "https://sepolia.etherscan.io";
|
||||||
|
const links = (id) =>
|
||||||
|
[...node(id).innerHTML.matchAll(/href="([^"]*)"/g)].map((m) => m[1]);
|
||||||
|
expect(links("tx-detail-hash")).toEqual([`${sepolia}/tx/${TX_HASH}`]);
|
||||||
|
expect(links("tx-detail-from")).toEqual([`${sepolia}/address/${HOLDER}`]);
|
||||||
|
expect(links("tx-detail-to")).toEqual([`${sepolia}/address/${RECIPIENT}`]);
|
||||||
|
expect(links("tx-detail-token-contract")).toEqual([
|
||||||
|
`${sepolia}/token/${TOKEN.toLowerCase()}`,
|
||||||
|
]);
|
||||||
|
expect(links("tx-detail-block")).toEqual([`${sepolia}/block/100`]);
|
||||||
|
expect(links("tx-detail-calldata-details")).toEqual([
|
||||||
|
`${sepolia}/address/${TOKEN}`,
|
||||||
|
`${sepolia}/address/${RECIPIENT}`,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|||||||
+70
-3
@@ -29,8 +29,13 @@ jest.mock("../src/shared/log", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const mockReceiptLookup = jest.fn();
|
const mockReceiptLookup = jest.fn();
|
||||||
|
// The RPC URL and network of every node a wait was given.
|
||||||
|
const mockNodes = [];
|
||||||
jest.mock("../src/shared/balances", () => ({
|
jest.mock("../src/shared/balances", () => ({
|
||||||
getProvider: () => ({ getTransactionReceipt: mockReceiptLookup }),
|
getProvider: (rpcUrl, networkId) => {
|
||||||
|
mockNodes.push({ rpcUrl, networkId });
|
||||||
|
return { getTransactionReceipt: mockReceiptLookup };
|
||||||
|
},
|
||||||
refreshBalances: jest.fn(),
|
refreshBalances: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -107,6 +112,7 @@ global.chrome = { storage };
|
|||||||
|
|
||||||
const txStatus = require("../src/popup/views/txStatus");
|
const txStatus = require("../src/popup/views/txStatus");
|
||||||
const { state } = require("../src/shared/state");
|
const { state } = require("../src/shared/state");
|
||||||
|
const { NETWORKS } = require("../src/shared/networks");
|
||||||
const { RESTORABLE_VIEWS } = require("../src/shared/restorableViews");
|
const { RESTORABLE_VIEWS } = require("../src/shared/restorableViews");
|
||||||
const { log } = require("../src/shared/log");
|
const { log } = require("../src/shared/log");
|
||||||
|
|
||||||
@@ -118,6 +124,7 @@ const TX_INFO = {
|
|||||||
amount: "0.0050",
|
amount: "0.0050",
|
||||||
token: "ETH",
|
token: "ETH",
|
||||||
tokenSymbol: null,
|
tokenSymbol: null,
|
||||||
|
chainId: NETWORKS.mainnet.chainId,
|
||||||
};
|
};
|
||||||
|
|
||||||
// True when a view element is not hidden.
|
// True when a view element is not hidden.
|
||||||
@@ -137,7 +144,11 @@ beforeEach(() => {
|
|||||||
jest.setSystemTime(new Date("2026-08-11T12:00:00Z"));
|
jest.setSystemTime(new Date("2026-08-11T12:00:00Z"));
|
||||||
elements.clear();
|
elements.clear();
|
||||||
mockReceiptLookup.mockReset();
|
mockReceiptLookup.mockReset();
|
||||||
|
mockNodes.length = 0;
|
||||||
log.errorf.mockClear();
|
log.errorf.mockClear();
|
||||||
|
state.networkId = "mainnet";
|
||||||
|
state.rpcUrl = NETWORKS.mainnet.defaultRpcUrl;
|
||||||
|
state.networkEndpoints = {};
|
||||||
state.wallets = [];
|
state.wallets = [];
|
||||||
state.viewData = {};
|
state.viewData = {};
|
||||||
state.viewStack = [];
|
state.viewStack = [];
|
||||||
@@ -349,14 +360,14 @@ describe("WaitTx persistence across popup close", () => {
|
|||||||
// out of restoreView().
|
// out of restoreView().
|
||||||
{
|
{
|
||||||
hash: TX_HASH,
|
hash: TX_HASH,
|
||||||
txInfo: { to: 42, amount: "0.0050" },
|
txInfo: { ...TX_INFO, to: 42 },
|
||||||
broadcastTime: Date.now(),
|
broadcastTime: Date.now(),
|
||||||
},
|
},
|
||||||
// Otherwise complete but an array: only Array.isArray() rejects
|
// Otherwise complete but an array: only Array.isArray() rejects
|
||||||
// it, since typeof [] is "object" and the fields are present.
|
// it, since typeof [] is "object" and the fields are present.
|
||||||
{
|
{
|
||||||
hash: TX_HASH,
|
hash: TX_HASH,
|
||||||
txInfo: Object.assign([], { to: RECIPIENT, amount: "0.0050" }),
|
txInfo: Object.assign([], TX_INFO),
|
||||||
broadcastTime: Date.now(),
|
broadcastTime: Date.now(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -364,6 +375,18 @@ describe("WaitTx persistence across popup close", () => {
|
|||||||
txInfo: { to: RECIPIENT },
|
txInfo: { to: RECIPIENT },
|
||||||
broadcastTime: Date.now(),
|
broadcastTime: Date.now(),
|
||||||
},
|
},
|
||||||
|
// Otherwise complete but for a chain id naming no network: there
|
||||||
|
// is no node to ask for the receipt, and no explorer to link to.
|
||||||
|
{
|
||||||
|
hash: TX_HASH,
|
||||||
|
txInfo: { ...TX_INFO, chainId: undefined },
|
||||||
|
broadcastTime: Date.now(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hash: TX_HASH,
|
||||||
|
txInfo: { ...TX_INFO, chainId: "0x5" },
|
||||||
|
broadcastTime: Date.now(),
|
||||||
|
},
|
||||||
]) {
|
]) {
|
||||||
state.viewData = { pendingWait: bad };
|
state.viewData = { pendingWait: bad };
|
||||||
expect(txStatus.restoreWait()).toBe(false);
|
expect(txStatus.restoreWait()).toBe(false);
|
||||||
@@ -499,6 +522,50 @@ describe("WaitTx when the popup closes", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The popup can reopen onto WaitTx after Settings switched the network. The
|
||||||
|
// wait asked the active network's node for the receipt, never found it, and
|
||||||
|
// reported a timeout, and its links went to the active network's explorer
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/584).
|
||||||
|
describe("WaitTx for a Sepolia transaction with mainnet active", () => {
|
||||||
|
const SEPOLIA_NODE = "https://sepolia.node.example.invalid";
|
||||||
|
const sepoliaTx = { ...TX_INFO, chainId: NETWORKS.sepolia.chainId };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
state.rpcUrl = "https://mainnet.node.example.invalid";
|
||||||
|
state.networkEndpoints = { sepolia: { rpcUrl: SEPOLIA_NODE } };
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the receipt is asked of the Sepolia node Settings has", async () => {
|
||||||
|
mockReceiptLookup.mockResolvedValue({ blockNumber: 21000000 });
|
||||||
|
state.viewData = {
|
||||||
|
pendingWait: {
|
||||||
|
txInfo: sepoliaTx,
|
||||||
|
hash: TX_HASH,
|
||||||
|
broadcastTime: Date.now(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(txStatus.restoreWait()).toBe(true);
|
||||||
|
await jest.advanceTimersByTimeAsync(0);
|
||||||
|
|
||||||
|
expect(mockNodes).toEqual([
|
||||||
|
{ rpcUrl: SEPOLIA_NODE, networkId: "sepolia" },
|
||||||
|
]);
|
||||||
|
expect(state.currentView).toBe("success-tx");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the recipient and hash link to the Sepolia explorer", () => {
|
||||||
|
mockReceiptLookup.mockResolvedValue(null);
|
||||||
|
txStatus.showWait(sepoliaTx, TX_HASH);
|
||||||
|
|
||||||
|
expect(getElement("wait-tx-to").innerHTML).toContain(
|
||||||
|
`href="https://sepolia.etherscan.io/address/${RECIPIENT}"`,
|
||||||
|
);
|
||||||
|
expect(getElement("wait-tx-hash").innerHTML).toContain(
|
||||||
|
`href="https://sepolia.etherscan.io/tx/${TX_HASH}"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("wait-tx is a view the popup may reopen onto", () => {
|
describe("wait-tx is a view the popup may reopen onto", () => {
|
||||||
// The resume feature is wired through RESTORABLE_VIEWS: restoreView()
|
// The resume feature is wired through RESTORABLE_VIEWS: restoreView()
|
||||||
// refuses any view not in the set, so dropping "wait-tx" from it kills
|
// refuses any view not in the set, so dropping "wait-tx" from it kills
|
||||||
|
|||||||
@@ -6,8 +6,10 @@
|
|||||||
// release runs for a command type
|
// release runs for a command type
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/592), the address whose
|
// (https://git.eeqj.de/sneak/AutistMask/issues/592), the address whose
|
||||||
// balance a BALANCE_CHECK_ERC20 checks
|
// balance a BALANCE_CHECK_ERC20 checks
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/600), and any token a swap
|
// (https://git.eeqj.de/sneak/AutistMask/issues/600), any token a swap
|
||||||
// leaves in the router (https://git.eeqj.de/sneak/AutistMask/issues/606).
|
// leaves in the router (https://git.eeqj.de/sneak/AutistMask/issues/606), and
|
||||||
|
// what each PERMIT2_TRANSFER_FROM moves
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/611).
|
||||||
//
|
//
|
||||||
// Every swap command, SWEEP, WRAP_ETH, UNWRAP_WETH, TRANSFER, PAY_PORTION,
|
// Every swap command, SWEEP, WRAP_ETH, UNWRAP_WETH, TRANSFER, PAY_PORTION,
|
||||||
// PAY_PORTION_FULL_PRECISION, PERMIT2_TRANSFER_FROM, ACROSS_V4_DEPOSIT_V3 and
|
// PAY_PORTION_FULL_PRECISION, PERMIT2_TRANSFER_FROM, ACROSS_V4_DEPOSIT_V3 and
|
||||||
@@ -349,6 +351,13 @@ const PAID_ELSEWHERE = [
|
|||||||
"Steps",
|
"Steps",
|
||||||
"Deadline",
|
"Deadline",
|
||||||
];
|
];
|
||||||
|
// `lines` with the lines of one PERMIT2_TRANSFER_FROM after Amount.
|
||||||
|
const withMove = (lines) =>
|
||||||
|
lines.flatMap((l) =>
|
||||||
|
l === "Amount"
|
||||||
|
? [l, "Moved from the sender", "Token moved", "Moved to"]
|
||||||
|
: [l],
|
||||||
|
);
|
||||||
|
|
||||||
// A swap of 2,000 USDT for at least 1 WETH, paid to `recipient`.
|
// A swap of 2,000 USDT for at least 1 WETH, paid to `recipient`.
|
||||||
const swapToWeth = (recipient) =>
|
const swapToWeth = (recipient) =>
|
||||||
@@ -1154,12 +1163,20 @@ describe("each transfer command's recipient", () => {
|
|||||||
test("PERMIT2_TRANSFER_FROM: to the user it pays no one else, to another address that address is named", () => {
|
test("PERMIT2_TRANSFER_FROM: to the user it pays no one else, to another address that address is named", () => {
|
||||||
for (const r of [MSG_SENDER, USER]) {
|
for (const r of [MSG_SENDER, USER]) {
|
||||||
const result = decode(movedThenSwapped(r));
|
const result = decode(movedThenSwapped(r));
|
||||||
expect(labels(result)).toEqual(PAID_TO_USER);
|
expect(labels(result)).toEqual(withMove(PAID_TO_USER));
|
||||||
expect(detail(result, "Min. received").value).toBe("1.0000 WETH");
|
expect(detail(result, "Min. received").value).toBe("1.0000 WETH");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A move to the From address as written shows that address, not The
|
||||||
|
// sender.
|
||||||
|
expect(detail(decode(movedThenSwapped(USER)), "Moved to")).toEqual({
|
||||||
|
label: "Moved to",
|
||||||
|
value: USER,
|
||||||
|
address: USER,
|
||||||
|
});
|
||||||
|
|
||||||
const toOther = decode(movedThenSwapped(OTHER));
|
const toOther = decode(movedThenSwapped(OTHER));
|
||||||
expect(labels(toOther)).toEqual(PAID_ELSEWHERE);
|
expect(labels(toOther)).toEqual(withMove(PAID_ELSEWHERE));
|
||||||
expect(paidTo(toOther)).toEqual([OTHER]);
|
expect(paidTo(toOther)).toEqual([OTHER]);
|
||||||
expect(detail(toOther, "Min. paid out").value).toBe("1.0000 WETH");
|
expect(detail(toOther, "Min. paid out").value).toBe("1.0000 WETH");
|
||||||
|
|
||||||
@@ -1249,6 +1266,11 @@ describe("each transfer command's recipient", () => {
|
|||||||
for (const r of [MSG_SENDER, ADDRESS_THIS]) {
|
for (const r of [MSG_SENDER, ADDRESS_THIS]) {
|
||||||
const result = decode(movedThenSwapped(r), USER, ROUTER_V1);
|
const result = decode(movedThenSwapped(r), USER, ROUTER_V1);
|
||||||
expect(paidTo(result)).toEqual([r]);
|
expect(paidTo(result)).toEqual([r]);
|
||||||
|
expect(detail(result, "Moved to")).toEqual({
|
||||||
|
label: "Moved to",
|
||||||
|
value: r,
|
||||||
|
address: r,
|
||||||
|
});
|
||||||
expect(detail(result, "Min. received")).toBeUndefined();
|
expect(detail(result, "Min. received")).toBeUndefined();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1294,6 +1316,112 @@ describe("each transfer command's recipient", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// https://git.eeqj.de/sneak/AutistMask/issues/611: after the swap's own
|
||||||
|
// Amount, each PERMIT2_TRANSFER_FROM but one into the router gives the amount
|
||||||
|
// of the sender's tokens it moves, the token, and where it goes, whoever
|
||||||
|
// counts as paid.
|
||||||
|
describe("what each PERMIT2_TRANSFER_FROM moves", () => {
|
||||||
|
// The WETH/USDC pair on Ethereum Mainnet, as its V2 factory reports it.
|
||||||
|
const PAIR = "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc";
|
||||||
|
const USDT_1 = 1000000n;
|
||||||
|
const USDT_1000000 = 1000000000000n;
|
||||||
|
|
||||||
|
// The lines between Amount and Token Out.
|
||||||
|
function afterAmount(result) {
|
||||||
|
const all = labels(result);
|
||||||
|
return result.details.slice(
|
||||||
|
all.indexOf("Amount") + 1,
|
||||||
|
all.indexOf("Token Out"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// The lines of a move of `amount` of `token`, then `to`.
|
||||||
|
const moved = (amount, token, symbol, to) => [
|
||||||
|
{ label: "Moved from the sender", value: amount },
|
||||||
|
{
|
||||||
|
label: "Token moved",
|
||||||
|
value: symbol + " (" + token + ")",
|
||||||
|
address: token,
|
||||||
|
isToken: true,
|
||||||
|
},
|
||||||
|
to,
|
||||||
|
];
|
||||||
|
const movedTo = (value) => ({ label: "Moved to", value });
|
||||||
|
const movedToAddress = (address) => ({
|
||||||
|
label: "Moved to",
|
||||||
|
value: address,
|
||||||
|
address,
|
||||||
|
});
|
||||||
|
|
||||||
|
test("1,000,000 USDT moved to another address, then a swap of 1 USDT", () => {
|
||||||
|
const result = decode(
|
||||||
|
execute(
|
||||||
|
permit2TransferFrom(USDT, OTHER, USDT_1000000),
|
||||||
|
v3SwapExactIn(MSG_SENDER, USDT_1, ONE_WETH, USDT, WETH),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(detail(result, "Amount").value).toBe("1.0000 USDT");
|
||||||
|
expect(afterAmount(result)).toEqual(
|
||||||
|
moved("1000000.0000 USDT", USDT, "USDT", movedToAddress(OTHER)),
|
||||||
|
);
|
||||||
|
expect(paidTo(result)).toEqual([OTHER]);
|
||||||
|
expect(detail(result, "Min. paid out").value).toBe("1.0000 WETH");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("1 WETH moved to the pair the next V2 swap spends", () => {
|
||||||
|
const result = decode(
|
||||||
|
execute(
|
||||||
|
permit2TransferFrom(WETH, PAIR, ONE_WETH),
|
||||||
|
v2Swap(0x08, MSG_SENDER, 0n, USDC_1990, [WETH, USDC], false),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(detail(result, "Amount").value).toBe(
|
||||||
|
"Whatever an earlier step sent to the pair (V2 already paid)",
|
||||||
|
);
|
||||||
|
expect(afterAmount(result)).toEqual(
|
||||||
|
moved(
|
||||||
|
"1.0000 WETH",
|
||||||
|
WETH,
|
||||||
|
"WETH",
|
||||||
|
movedTo("The V2 pair the next swap spends"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(paidTo(result)).toEqual([]);
|
||||||
|
expect(detail(result, "Min. received").value).toBe("1990.0000 USDC");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Its Paid to and Token moved into the router lines already say what a
|
||||||
|
// move into the router moves, so it gets no lines after Amount.
|
||||||
|
test("2,000 USDT moved into the router, then a swap of 1 USDT from the router's balance, shows the 2,000 USDT once", () => {
|
||||||
|
const result = decode(
|
||||||
|
execute(
|
||||||
|
permit2TransferFrom(USDT, ADDRESS_THIS, USDT_2000),
|
||||||
|
v3SwapExactIn(MSG_SENDER, USDT_1, ONE_WETH, USDT, WETH, false),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(detail(result, "Amount").value).toBe("1.0000 USDT");
|
||||||
|
expect(afterAmount(result)).toEqual([]);
|
||||||
|
expect(paidTo(result)).toEqual([movedIntoRouter("2000.0000 USDT")]);
|
||||||
|
expect(
|
||||||
|
result.details.filter((d) => d.value.includes("2000.0000 USDT")),
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(detail(result, "Min. paid out").value).toBe("1.0000 WETH");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a move to address(1) goes to the sender, and each move is shown in step order", () => {
|
||||||
|
const result = decode(
|
||||||
|
execute(
|
||||||
|
permit2TransferFrom(USDT, MSG_SENDER, USDT_2000),
|
||||||
|
permit2TransferFrom(USDC, OTHER, USDC_1990),
|
||||||
|
swapToWeth(MSG_SENDER),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(afterAmount(result)).toEqual([
|
||||||
|
...moved("2000.0000 USDT", USDT, "USDT", movedTo("The sender")),
|
||||||
|
...moved("1990.0000 USDC", USDC, "USDC", movedToAddress(OTHER)),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// A payout of only part of what the router holds leaves the rest there.
|
// A payout of only part of what the router holds leaves the rest there.
|
||||||
describe("a payout of part of the router's output", () => {
|
describe("a payout of part of the router's output", () => {
|
||||||
test.each([
|
test.each([
|
||||||
@@ -2787,3 +2915,26 @@ describe("the screens show each token moved into the router as an address", () =
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The issue's example: 1,000,000 USDT moved to another address, then a swap
|
||||||
|
// of 1 USDT. Both screens show the amount moved and where it goes.
|
||||||
|
describe("the screens show what a PERMIT2_TRANSFER_FROM moves", () => {
|
||||||
|
const millionToOther = execute(
|
||||||
|
permit2TransferFrom(USDT, OTHER, 1000000000000n),
|
||||||
|
v3SwapExactIn(MSG_SENDER, 1000000n, ONE_WETH, USDT, WETH),
|
||||||
|
);
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{ screen: "approval", show: showApproval },
|
||||||
|
{ screen: "transaction detail", show: showDetail },
|
||||||
|
])("the $screen screen", async ({ show }) => {
|
||||||
|
const html = await show(millionToOther);
|
||||||
|
expect(html).toContain("1000000.0000 USDT");
|
||||||
|
// Each line is drawn in a div of its own; the Paid to line names
|
||||||
|
// OTHER too, so the Moved to line's own div is checked.
|
||||||
|
const movedToLine = html
|
||||||
|
.split('<div class="mb-2">')
|
||||||
|
.find((line) => line.includes(">Moved to<"));
|
||||||
|
expect(movedToLine).toContain('data-copy="' + OTHER + '"');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user