Compare commits
2
Commits
| 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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user