fix: SuccessTx and ErrorTx link to the explorer of the network the transaction was sent on (closes #622) #624
@@ -1922,9 +1922,12 @@ view would leave a wallet one click from deletion.
|
||||
- Amount + symbol
|
||||
- To: color dot + full address + etherscan link, or for a contract creation
|
||||
the same sentence as on WaitTx
|
||||
- Block number
|
||||
- Block number + etherscan link
|
||||
- Transaction hash: full hash (tap to copy) + etherscan link
|
||||
- "Done" button
|
||||
- **Behavior**: As on WaitTx, every etherscan link on the screen opens the
|
||||
explorer of the network the transaction was sent on, whichever network is
|
||||
active.
|
||||
- **Transitions**:
|
||||
- "Done" in the approval popup → closes the popup window
|
||||
- "Done" otherwise → resets the navigation stack, then → **AddressToken**
|
||||
@@ -1944,6 +1947,9 @@ view would leave a wallet one click from deletion.
|
||||
- Transaction hash section (hidden if broadcast failed before getting hash):
|
||||
full hash (tap to copy) + etherscan link
|
||||
- "Done" button
|
||||
- **Behavior**: As on WaitTx, every etherscan link on the screen opens the
|
||||
explorer of the network the transaction was sent on, whichever network is
|
||||
active.
|
||||
- **Transitions**:
|
||||
- "Done" in the approval popup → closes the popup window
|
||||
- "Done" otherwise → resets the navigation stack, then → **AddressToken**
|
||||
|
||||
@@ -205,11 +205,19 @@ function renderView(view, state, views) {
|
||||
if (!data.hash) return false;
|
||||
if (!isAddressText(data.to)) return false;
|
||||
if (!isRenderableDecoded(data.decoded)) return false;
|
||||
// `chainId` picks the network whose explorer the links open, so
|
||||
// when it is there it has to name one. Likewise on error-tx.
|
||||
if (data.chainId && !SUPPORTED_CHAIN_IDS.has(data.chainId)) {
|
||||
return false;
|
||||
}
|
||||
views.txStatus.renderSuccess();
|
||||
return true;
|
||||
case "error-tx":
|
||||
if (!data.message) return false;
|
||||
if (!isAddressText(data.to)) return false;
|
||||
if (data.chainId && !SUPPORTED_CHAIN_IDS.has(data.chainId)) {
|
||||
return false;
|
||||
}
|
||||
views.txStatus.renderError();
|
||||
return true;
|
||||
default:
|
||||
|
||||
+20
-18
@@ -64,8 +64,8 @@ function endWait() {
|
||||
}
|
||||
|
||||
// A contract creation reaches these screens with `to` as "" (approval.js
|
||||
// writes `to: toAddr || ""`). The link opens the explorer of the network with
|
||||
// `chainId`, or of the active network when none is given.
|
||||
// writes `to: toAddr || ""`). The links below open the explorer of the network
|
||||
// with `chainId`, or of the active network when none is given.
|
||||
function toAddressHtml(address, chainId) {
|
||||
if (!address) return escapeHtml(CONTRACT_CREATION_TEXT);
|
||||
const title = addressTitle(address, state.wallets);
|
||||
@@ -77,9 +77,9 @@ function txHashHtml(hash, chainId) {
|
||||
return copyableHtml(hash, "break-all") + etherscanLinkHtml(link);
|
||||
}
|
||||
|
||||
function blockNumberHtml(blockNumber) {
|
||||
function blockNumberHtml(blockNumber, chainId) {
|
||||
const num = String(blockNumber);
|
||||
const link = explorerUrl("block", num);
|
||||
const link = explorerUrl("block", num, chainId);
|
||||
return copyableHtml(num) + etherscanLinkHtml(link);
|
||||
}
|
||||
|
||||
@@ -95,11 +95,11 @@ function startWait(txInfo, txHash, broadcastTime, pollNow) {
|
||||
// native currency of txInfo.chainId, the network the transaction was sent
|
||||
// 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
|
||||
// is open or before a later popup resumes it. This screen's links open
|
||||
// that network's explorer, and its receipt is asked of that network's
|
||||
// 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.
|
||||
// is open or before a later popup resumes it. The links on all three
|
||||
// screens open that network's explorer, and this screen's receipt is
|
||||
// asked of that network's 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 = "";
|
||||
if (txInfo.token === "ETH") {
|
||||
symbol = nativeCurrencyByChainId(txInfo.chainId);
|
||||
@@ -258,12 +258,13 @@ function showSuccess(txInfo, txHash, blockNumber) {
|
||||
hash: txHash,
|
||||
blockNumber: blockNumber,
|
||||
decoded: txInfo.decoded || null,
|
||||
chainId: txInfo.chainId,
|
||||
};
|
||||
renderSuccess();
|
||||
ctx.doRefreshAndRender();
|
||||
}
|
||||
|
||||
function decodedDetailsHtml(decoded) {
|
||||
function decodedDetailsHtml(decoded, chainId) {
|
||||
if (!decoded || !decoded.details) return "";
|
||||
let html = `<div class="border border-border border-dashed p-2 mb-3">`;
|
||||
if (decoded.name) {
|
||||
@@ -281,9 +282,9 @@ function decodedDetailsHtml(decoded) {
|
||||
if (d.isToken) {
|
||||
const sym = tokenLabel(d.address) || "Unknown token";
|
||||
html += `<div class="font-bold">${escapeHtml(sym)}</div>`;
|
||||
html += toAddressHtml(d.address);
|
||||
html += toAddressHtml(d.address, chainId);
|
||||
} else {
|
||||
html += toAddressHtml(d.address);
|
||||
html += toAddressHtml(d.address, chainId);
|
||||
}
|
||||
} else {
|
||||
html += `<div class="font-bold">${escapeHtml(d.value)}</div>`;
|
||||
@@ -312,16 +313,16 @@ function renderSuccess() {
|
||||
toSection.classList.remove("hidden");
|
||||
$("success-tx-summary").textContent =
|
||||
d.amount + (d.symbol ? " " + d.symbol : "");
|
||||
$("success-tx-to").innerHTML = toAddressHtml(d.to);
|
||||
$("success-tx-to").innerHTML = toAddressHtml(d.to, d.chainId);
|
||||
}
|
||||
|
||||
$("success-tx-block").innerHTML = blockNumberHtml(d.blockNumber);
|
||||
$("success-tx-hash").innerHTML = txHashHtml(d.hash);
|
||||
$("success-tx-block").innerHTML = blockNumberHtml(d.blockNumber, d.chainId);
|
||||
$("success-tx-hash").innerHTML = txHashHtml(d.hash, d.chainId);
|
||||
|
||||
// Show decoded calldata details if present
|
||||
const decodedEl = $("success-tx-decoded");
|
||||
if (decodedEl && hasDecoded) {
|
||||
decodedEl.innerHTML = decodedDetailsHtml(d.decoded);
|
||||
decodedEl.innerHTML = decodedDetailsHtml(d.decoded, d.chainId);
|
||||
decodedEl.classList.remove("hidden");
|
||||
} else if (decodedEl) {
|
||||
decodedEl.classList.add("hidden");
|
||||
@@ -347,6 +348,7 @@ function showError(txInfo, txHash, message) {
|
||||
to: txInfo.to,
|
||||
hash: txHash || null,
|
||||
message: message,
|
||||
chainId: txInfo.chainId,
|
||||
};
|
||||
renderError();
|
||||
}
|
||||
@@ -356,11 +358,11 @@ function renderError() {
|
||||
if (!d || !d.message) return;
|
||||
$("error-tx-summary").textContent =
|
||||
d.amount + (d.symbol ? " " + d.symbol : "");
|
||||
$("error-tx-to").innerHTML = toAddressHtml(d.to);
|
||||
$("error-tx-to").innerHTML = toAddressHtml(d.to, d.chainId);
|
||||
$("error-tx-message").textContent = d.message;
|
||||
|
||||
if (d.hash) {
|
||||
$("error-tx-hash").innerHTML = txHashHtml(d.hash);
|
||||
$("error-tx-hash").innerHTML = txHashHtml(d.hash, d.chainId);
|
||||
$("error-tx-hash-section").classList.remove("hidden");
|
||||
attachCopyHandlers("view-error-tx");
|
||||
} else {
|
||||
|
||||
@@ -441,6 +441,17 @@ const CONTRACT = [
|
||||
views: ["error-tx"],
|
||||
restored: false,
|
||||
},
|
||||
// A chain id naming no network has no explorer to link to.
|
||||
{
|
||||
value: { hash: "0x1", to: ADDRESS, chainId: "0x5" },
|
||||
views: ["success-tx"],
|
||||
restored: false,
|
||||
},
|
||||
{
|
||||
value: { message: "boom", to: ADDRESS, chainId: "0x5" },
|
||||
views: ["error-tx"],
|
||||
restored: false,
|
||||
},
|
||||
// transaction passes on `data.tx`.
|
||||
{
|
||||
value: { tx: { hash: "0x1" } },
|
||||
|
||||
@@ -566,6 +566,85 @@ describe("WaitTx for a Sepolia transaction with mainnet active", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// SuccessTx and ErrorTx follow WaitTx and describe the same transaction, but
|
||||
// their links went to the active network's explorer
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/622). Each test checks the
|
||||
// screen again after rendering it from its stored view data alone, as a
|
||||
// reopened popup does.
|
||||
describe("SuccessTx and ErrorTx for a Sepolia transaction with mainnet active", () => {
|
||||
const SEPOLIA_EXPLORER = "https://sepolia.etherscan.io";
|
||||
const TOKEN = "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238";
|
||||
const sepoliaTx = { ...TX_INFO, chainId: NETWORKS.sepolia.chainId };
|
||||
|
||||
test("the recipient, block and hash on SuccessTx link to the Sepolia explorer", async () => {
|
||||
function expectSepoliaLinks() {
|
||||
expect(getElement("success-tx-to").innerHTML).toContain(
|
||||
`href="${SEPOLIA_EXPLORER}/address/${RECIPIENT}"`,
|
||||
);
|
||||
expect(getElement("success-tx-block").innerHTML).toContain(
|
||||
`href="${SEPOLIA_EXPLORER}/block/21000000"`,
|
||||
);
|
||||
expect(getElement("success-tx-hash").innerHTML).toContain(
|
||||
`href="${SEPOLIA_EXPLORER}/tx/${TX_HASH}"`,
|
||||
);
|
||||
}
|
||||
mockReceiptLookup.mockResolvedValue({ blockNumber: 21000000 });
|
||||
txStatus.showWait(sepoliaTx, TX_HASH);
|
||||
await jest.advanceTimersByTimeAsync(10000);
|
||||
expect(state.currentView).toBe("success-tx");
|
||||
expectSepoliaLinks();
|
||||
|
||||
elements.clear();
|
||||
txStatus.renderSuccess();
|
||||
expectSepoliaLinks();
|
||||
});
|
||||
|
||||
test("the addresses in SuccessTx's decoded action link to the Sepolia explorer", async () => {
|
||||
function expectSepoliaLinks() {
|
||||
const html = getElement("success-tx-decoded").innerHTML;
|
||||
expect(html).toContain(
|
||||
`href="${SEPOLIA_EXPLORER}/address/${TOKEN}"`,
|
||||
);
|
||||
expect(html).toContain(
|
||||
`href="${SEPOLIA_EXPLORER}/address/${RECIPIENT}"`,
|
||||
);
|
||||
}
|
||||
const decoded = {
|
||||
name: "Transfer",
|
||||
details: [
|
||||
{ label: "Token", address: TOKEN, isToken: true },
|
||||
{ label: "Recipient", address: RECIPIENT },
|
||||
],
|
||||
};
|
||||
mockReceiptLookup.mockResolvedValue({ blockNumber: 21000000 });
|
||||
txStatus.showWait({ ...sepoliaTx, decoded }, TX_HASH);
|
||||
await jest.advanceTimersByTimeAsync(10000);
|
||||
expect(state.currentView).toBe("success-tx");
|
||||
expectSepoliaLinks();
|
||||
|
||||
elements.clear();
|
||||
txStatus.renderSuccess();
|
||||
expectSepoliaLinks();
|
||||
});
|
||||
|
||||
test("the recipient and hash on ErrorTx link to the Sepolia explorer", () => {
|
||||
function expectSepoliaLinks() {
|
||||
expect(getElement("error-tx-to").innerHTML).toContain(
|
||||
`href="${SEPOLIA_EXPLORER}/address/${RECIPIENT}"`,
|
||||
);
|
||||
expect(getElement("error-tx-hash").innerHTML).toContain(
|
||||
`href="${SEPOLIA_EXPLORER}/tx/${TX_HASH}"`,
|
||||
);
|
||||
}
|
||||
txStatus.showError(sepoliaTx, TX_HASH, "The transaction failed.");
|
||||
expectSepoliaLinks();
|
||||
|
||||
elements.clear();
|
||||
txStatus.renderError();
|
||||
expectSepoliaLinks();
|
||||
});
|
||||
});
|
||||
|
||||
describe("wait-tx is a view the popup may reopen onto", () => {
|
||||
// The resume feature is wired through RESTORABLE_VIEWS: restoreView()
|
||||
// refuses any view not in the set, so dropping "wait-tx" from it kills
|
||||
|
||||
Reference in New Issue
Block a user