Compare commits
2
Commits
22170faae6
...
a96085a2ba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a96085a2ba | ||
|
|
f79390cad5 |
No files matched your search
@@ -786,6 +786,7 @@ Shown after a broadcast. The screen holds:
|
|||||||
|
|
||||||
- the heading "Transaction Broadcast", with no "Back" button;
|
- the heading "Transaction Broadcast", with no "Back" button;
|
||||||
- the amount and symbol;
|
- the amount and symbol;
|
||||||
|
- each `Moved from the sender` amount of a swap;
|
||||||
- the recipient;
|
- the recipient;
|
||||||
- the transaction hash, with an explorer link;
|
- the transaction hash, with an explorer link;
|
||||||
- the counter "Waiting for confirmation... Ns".
|
- the counter "Waiting for confirmation... Ns".
|
||||||
@@ -823,7 +824,9 @@ Shown when signing or a broadcast fails, or when a wait ends without a receipt.
|
|||||||
The screen holds:
|
The screen holds:
|
||||||
|
|
||||||
- the heading "Transaction Failed";
|
- the heading "Transaction Failed";
|
||||||
- the amount and the recipient;
|
- the amount;
|
||||||
|
- each `Moved from the sender` amount of a swap;
|
||||||
|
- the recipient;
|
||||||
- the error message, in a dashed box;
|
- the error message, in a dashed box;
|
||||||
- the transaction hash, when one exists;
|
- the transaction hash, when one exists;
|
||||||
- a "Done" button that behaves as on SuccessTx.
|
- a "Done" button that behaves as on SuccessTx.
|
||||||
@@ -1197,14 +1200,23 @@ The token's symbol comes from the same places. The bundled list answers for a
|
|||||||
token on Ethereum Mainnet only. When none of them answers, the amount reads
|
token on Ethereum Mainnet only. When none of them answers, the amount reads
|
||||||
`5000000000 base units (decimals unknown)`, and nothing is formatted with
|
`5000000000 base units (decimals unknown)`, and nothing is formatted with
|
||||||
guessed decimals. A holding whose decimals are unknown shows "quantity unknown"
|
guessed decimals. A holding whose decimals are unknown shows "quantity unknown"
|
||||||
in the balance lists. A token just added shows "quantity unknown" in place of a
|
in the balance lists.
|
||||||
zero, until a refresh has read its balance at every address. Only a whole number
|
|
||||||
from 0 to 80 counts as a token's decimals. A token's `decimals` is stored only
|
A tracked token with no stored balance at an address can show "quantity unknown"
|
||||||
as a source reported it, never guessed. A `null` balance means the decimals are
|
there in place of a zero. It does so after each of these:
|
||||||
unknown, never that the balance is zero. While "Use Blockscout" is off, the
|
|
||||||
bundled list and the tracked tokens may lack a token's symbol or decimals. The
|
- the token is added;
|
||||||
approval and status screens then add a line under that token saying that
|
- an address or a wallet is added;
|
||||||
Blockscout's report is not used.
|
- the network is switched.
|
||||||
|
|
||||||
|
This lasts until a refresh has read the token's balance at every address.
|
||||||
|
|
||||||
|
Only a whole number from 0 to 80 counts as a token's decimals. A token's
|
||||||
|
`decimals` is stored only as a source reported it, never guessed. A `null`
|
||||||
|
balance means the decimals are unknown, never that the balance is zero. While
|
||||||
|
"Use Blockscout" is off, the bundled list and the tracked tokens may lack a
|
||||||
|
token's symbol or decimals. The approval and status screens then add a line
|
||||||
|
under that token saying that Blockscout's report is not used.
|
||||||
|
|
||||||
The wallet's own ERC-20 send is encoded with the decimals the screen showed. At
|
The wallet's own ERC-20 send is encoded with the decimals the screen showed. At
|
||||||
signing time the contract's `decimals()` is read only to be compared with those.
|
signing time the contract's `decimals()` is read only to be compared with those.
|
||||||
|
|||||||
+8
-3
@@ -98,8 +98,10 @@ let refreshTimer = null;
|
|||||||
// not tracked if the stored setting is off by then.
|
// not tracked if the stored setting is off by then.
|
||||||
//
|
//
|
||||||
// The save also deletes `balanceNotFetched` from each tracked token whose
|
// The save also deletes `balanceNotFetched` from each tracked token whose
|
||||||
// balance this refresh read at every address. trackToken() in
|
// balance this refresh read at every address. A refresh that did not read every
|
||||||
// src/popup/views/helpers.js sets that field. The save takes the field off the
|
// address this page now holds deletes no `balanceNotFetched`. Adding a token
|
||||||
|
// sets that field (trackToken() in src/popup/views/helpers.js). So does adding
|
||||||
|
// an address, and so does a network switch. The save takes the field off the
|
||||||
// matching tokens of the stored record (clearBalanceNotFetched() in
|
// matching tokens of the stored record (clearBalanceNotFetched() in
|
||||||
// src/shared/state.js). The rest of the stored tracked token list stays as it
|
// src/shared/state.js). The rest of the stored tracked token list stays as it
|
||||||
// is.
|
// is.
|
||||||
@@ -149,7 +151,10 @@ async function doRefreshAndRender() {
|
|||||||
if (!stored.useBlockscout) {
|
if (!stored.useBlockscout) {
|
||||||
dropUntrackedTokens(state.wallets, stored.trackedTokens);
|
dropUntrackedTokens(state.wallets, stored.trackedTokens);
|
||||||
}
|
}
|
||||||
clearBalanceNotFetched(stored, readTokens);
|
const readEveryAddress = state.wallets.every((wallet) =>
|
||||||
|
wallet.addresses.every((addr) => refreshed.has(addr.address)),
|
||||||
|
);
|
||||||
|
if (readEveryAddress) clearBalanceNotFetched(stored, readTokens);
|
||||||
state.lastBalanceRefresh = Date.now();
|
state.lastBalanceRefresh = Date.now();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const {
|
|||||||
goBack,
|
goBack,
|
||||||
clearViewStack,
|
clearViewStack,
|
||||||
onViewLeave,
|
onViewLeave,
|
||||||
|
markTrackedTokensNotFetched,
|
||||||
} = require("./helpers");
|
} = require("./helpers");
|
||||||
const {
|
const {
|
||||||
generateMnemonic,
|
generateMnemonic,
|
||||||
@@ -195,6 +196,7 @@ async function importMnemonic(ctx) {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
state.wallets.push(wallet);
|
state.wallets.push(wallet);
|
||||||
|
markTrackedTokensNotFetched();
|
||||||
state.hasWallet = true;
|
state.hasWallet = true;
|
||||||
if (state.activeAddress === null) {
|
if (state.activeAddress === null) {
|
||||||
state.activeAddress = firstAddress;
|
state.activeAddress = firstAddress;
|
||||||
@@ -220,6 +222,7 @@ async function importMnemonic(ctx) {
|
|||||||
tokenBalances: [],
|
tokenBalances: [],
|
||||||
}));
|
}));
|
||||||
wallet.nextIndex = scan.nextIndex;
|
wallet.nextIndex = scan.nextIndex;
|
||||||
|
markTrackedTokensNotFetched();
|
||||||
// The new list leaves out the first address when the scan found it
|
// The new list leaves out the first address when the scan found it
|
||||||
// unused. If it was the active one, the new list's first takes over,
|
// unused. If it was the active one, the new list's first takes over,
|
||||||
// and the background is told as when the user picks one on Home.
|
// and the background is told as when the user picks one on Home.
|
||||||
@@ -266,6 +269,7 @@ async function importPrivateKey(ctx) {
|
|||||||
encryptedSecret: encrypted,
|
encryptedSecret: encrypted,
|
||||||
addresses: [{ address: addr, balance: "0.0000", tokenBalances: [] }],
|
addresses: [{ address: addr, balance: "0.0000", tokenBalances: [] }],
|
||||||
});
|
});
|
||||||
|
markTrackedTokensNotFetched();
|
||||||
state.hasWallet = true;
|
state.hasWallet = true;
|
||||||
if (state.activeAddress === null) {
|
if (state.activeAddress === null) {
|
||||||
state.activeAddress = addr;
|
state.activeAddress = addr;
|
||||||
@@ -330,6 +334,7 @@ async function importXprvKey(ctx) {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
state.wallets.push(wallet);
|
state.wallets.push(wallet);
|
||||||
|
markTrackedTokensNotFetched();
|
||||||
state.hasWallet = true;
|
state.hasWallet = true;
|
||||||
if (state.activeAddress === null) {
|
if (state.activeAddress === null) {
|
||||||
state.activeAddress = firstAddress;
|
state.activeAddress = firstAddress;
|
||||||
@@ -355,6 +360,7 @@ async function importXprvKey(ctx) {
|
|||||||
tokenBalances: [],
|
tokenBalances: [],
|
||||||
}));
|
}));
|
||||||
wallet.nextIndex = scan.nextIndex;
|
wallet.nextIndex = scan.nextIndex;
|
||||||
|
markTrackedTokensNotFetched();
|
||||||
// As for a recovery phrase: an active first address the scan left
|
// As for a recovery phrase: an active first address the scan left
|
||||||
// out gives way to the new list's first.
|
// out gives way to the new list's first.
|
||||||
const activeMoved =
|
const activeMoved =
|
||||||
|
|||||||
@@ -172,8 +172,8 @@ function renderBalance() {
|
|||||||
(t) => t.address.toLowerCase() === id,
|
(t) => t.address.toLowerCase() === id,
|
||||||
);
|
);
|
||||||
// null when the scale is unknown, or when no refresh has read the
|
// null when the scale is unknown, or when no refresh has read the
|
||||||
// balance of a token just added: no quantity to show, and none to
|
// token's balance: no quantity to show, and none to price.
|
||||||
// price. balanceLine() states that rather than printing 0.0000.
|
// balanceLine() states that rather than printing 0.0000.
|
||||||
if (tb) amount = unknownableAmount(tb.balance);
|
if (tb) amount = unknownableAmount(tb.balance);
|
||||||
else amount = tracked && tracked.balanceNotFetched ? null : 0;
|
else amount = tracked && tracked.balanceNotFetched ? null : 0;
|
||||||
price = getPrice(tokenId);
|
price = getPrice(tokenId);
|
||||||
|
|||||||
@@ -430,6 +430,13 @@ function trackToken(token) {
|
|||||||
state.trackedTokens.push({ ...token, balanceNotFetched: true });
|
state.trackedTokens.push({ ...token, balanceNotFetched: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gives every tracked token `balanceNotFetched: true`, as trackToken() gives a
|
||||||
|
// new one. Called when an address is added, before the save: no refresh has
|
||||||
|
// read any token's balance at that address.
|
||||||
|
function markTrackedTokensNotFetched() {
|
||||||
|
for (const token of state.trackedTokens) token.balanceNotFetched = true;
|
||||||
|
}
|
||||||
|
|
||||||
function balanceLinesForAddress(addr, trackedTokens, showZero) {
|
function balanceLinesForAddress(addr, trackedTokens, showZero) {
|
||||||
let html = balanceLine(
|
let html = balanceLine(
|
||||||
nativeCurrency(),
|
nativeCurrency(),
|
||||||
@@ -850,6 +857,7 @@ module.exports = {
|
|||||||
balanceLine,
|
balanceLine,
|
||||||
balanceLinesForAddress,
|
balanceLinesForAddress,
|
||||||
trackToken,
|
trackToken,
|
||||||
|
markTrackedTokensNotFetched,
|
||||||
addressHoldsFunds,
|
addressHoldsFunds,
|
||||||
keyMaterialName,
|
keyMaterialName,
|
||||||
unknownableAmount,
|
unknownableAmount,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const {
|
|||||||
showView,
|
showView,
|
||||||
showFlash,
|
showFlash,
|
||||||
balanceLinesForAddress,
|
balanceLinesForAddress,
|
||||||
|
markTrackedTokensNotFetched,
|
||||||
isoDate,
|
isoDate,
|
||||||
timeAgo,
|
timeAgo,
|
||||||
addressDotHtml,
|
addressDotHtml,
|
||||||
@@ -394,6 +395,7 @@ function render(ctx) {
|
|||||||
tokenBalances: [],
|
tokenBalances: [],
|
||||||
});
|
});
|
||||||
wallet.nextIndex++;
|
wallet.nextIndex++;
|
||||||
|
markTrackedTokensNotFetched();
|
||||||
await saveState();
|
await saveState();
|
||||||
render(ctx);
|
render(ctx);
|
||||||
ctx.doRefreshAndRender();
|
ctx.doRefreshAndRender();
|
||||||
|
|||||||
@@ -242,10 +242,11 @@ function updateSendBalance() {
|
|||||||
state.trackedTokens,
|
state.trackedTokens,
|
||||||
currentNetwork().chainId,
|
currentNetwork().chainId,
|
||||||
);
|
);
|
||||||
// A null balance is a holding whose scale is unknown. Saying a figure
|
// A null balance is a holding whose scale is unknown, or a tracked
|
||||||
// for it would be a claim about the amount, so it reads as the
|
// token whose balance no refresh has read. Saying a figure for it would
|
||||||
// confirmation screen's balance line reads it; the send itself is
|
// be a claim about the amount, so it reads as the confirmation screen's
|
||||||
// refused later by transferAmountUnits() for the same missing scale.
|
// balance line reads it. transferAmountUnits() later refuses a send at
|
||||||
|
// a missing scale.
|
||||||
const bal = tokenBalanceAndDecimals(addr, token).tokenBalance;
|
const bal = tokenBalanceAndDecimals(addr, token).tokenBalance;
|
||||||
$("send-balance").textContent =
|
$("send-balance").textContent =
|
||||||
bal == null
|
bal == null
|
||||||
|
|||||||
@@ -73,6 +73,29 @@ function toAddressHtml(address, chainId) {
|
|||||||
return renderAddressHtml(address, { title, chainId });
|
return renderAddressHtml(address, { title, chainId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The amount of each `Moved from the sender` line of a decoded swap, for the
|
||||||
|
// wait and error screens to show under the Amount figure. That figure can leave
|
||||||
|
// these amounts out, so alone it can read as less than can have left the
|
||||||
|
// wallet. `decoded` can come back from storage in any shape, so this shows only
|
||||||
|
// the lines whose value is text.
|
||||||
|
function movedFromSenderHtml(decoded) {
|
||||||
|
if (!decoded || !Array.isArray(decoded.details)) return "";
|
||||||
|
let html = "";
|
||||||
|
for (const d of decoded.details) {
|
||||||
|
if (
|
||||||
|
!d ||
|
||||||
|
d.label !== "Moved from the sender" ||
|
||||||
|
typeof d.value !== "string"
|
||||||
|
)
|
||||||
|
continue;
|
||||||
|
html += `<div class="mt-2">`;
|
||||||
|
html += `<div class="text-xs text-muted mb-1">${escapeHtml(d.label)}</div>`;
|
||||||
|
html += `<div class="font-bold">${escapeHtml(d.value)}</div>`;
|
||||||
|
html += `</div>`;
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
function txHashHtml(hash, chainId) {
|
function txHashHtml(hash, chainId) {
|
||||||
const link = explorerUrl("tx", hash, chainId);
|
const link = explorerUrl("tx", hash, chainId);
|
||||||
return copyableHtml(hash, "break-all") + etherscanLinkHtml(link);
|
return copyableHtml(hash, "break-all") + etherscanLinkHtml(link);
|
||||||
@@ -109,10 +132,9 @@ function startWait(txInfo, txHash, broadcastTime, pollNow) {
|
|||||||
}
|
}
|
||||||
$("wait-tx-summary").textContent =
|
$("wait-tx-summary").textContent =
|
||||||
txInfo.amount + (symbol ? " " + symbol : "");
|
txInfo.amount + (symbol ? " " + symbol : "");
|
||||||
$("wait-tx-token-note").innerHTML = blockscoutOffTokenNoteHtml(
|
$("wait-tx-token-note").innerHTML =
|
||||||
txInfo.token,
|
blockscoutOffTokenNoteHtml(txInfo.token, txInfo.chainId) +
|
||||||
txInfo.chainId,
|
movedFromSenderHtml(txInfo.decoded);
|
||||||
);
|
|
||||||
$("wait-tx-to").innerHTML = toAddressHtml(txInfo.to, txInfo.chainId);
|
$("wait-tx-to").innerHTML = toAddressHtml(txInfo.to, txInfo.chainId);
|
||||||
$("wait-tx-hash").innerHTML = txHashHtml(txHash, txInfo.chainId);
|
$("wait-tx-hash").innerHTML = txHashHtml(txHash, txInfo.chainId);
|
||||||
attachCopyHandlers("view-wait-tx");
|
attachCopyHandlers("view-wait-tx");
|
||||||
@@ -223,7 +245,7 @@ function showWait(txInfo, txHash) {
|
|||||||
// to name a network. txInfo.token and txInfo.tokenSymbol are not checked here.
|
// to name a network. txInfo.token and txInfo.tokenSymbol are not checked here.
|
||||||
// blockscoutOffTokenNoteHtml() shows no line for a token that is not text.
|
// blockscoutOffTokenNoteHtml() shows no line for a token that is not text.
|
||||||
// Everything else only compares and coalesces them, and tokenSymbol is null for
|
// Everything else only compares and coalesces them, and tokenSymbol is null for
|
||||||
// ETH.
|
// ETH. txInfo.decoded is not checked either: see movedFromSenderHtml().
|
||||||
function restoreWait() {
|
function restoreWait() {
|
||||||
const d = state.viewData;
|
const d = state.viewData;
|
||||||
if (!d || !d.pendingWait) return false;
|
if (!d || !d.pendingWait) return false;
|
||||||
@@ -356,6 +378,7 @@ function showError(txInfo, txHash, message) {
|
|||||||
to: txInfo.to,
|
to: txInfo.to,
|
||||||
hash: txHash || null,
|
hash: txHash || null,
|
||||||
message: message,
|
message: message,
|
||||||
|
decoded: txInfo.decoded || null,
|
||||||
chainId: txInfo.chainId,
|
chainId: txInfo.chainId,
|
||||||
};
|
};
|
||||||
renderError();
|
renderError();
|
||||||
@@ -366,10 +389,9 @@ function renderError() {
|
|||||||
if (!d || !d.message) return;
|
if (!d || !d.message) return;
|
||||||
$("error-tx-summary").textContent =
|
$("error-tx-summary").textContent =
|
||||||
d.amount + (d.symbol ? " " + d.symbol : "");
|
d.amount + (d.symbol ? " " + d.symbol : "");
|
||||||
$("error-tx-token-note").innerHTML = blockscoutOffTokenNoteHtml(
|
$("error-tx-token-note").innerHTML =
|
||||||
d.token,
|
blockscoutOffTokenNoteHtml(d.token, d.chainId) +
|
||||||
d.chainId,
|
movedFromSenderHtml(d.decoded);
|
||||||
);
|
|
||||||
$("error-tx-to").innerHTML = toAddressHtml(d.to, d.chainId);
|
$("error-tx-to").innerHTML = toAddressHtml(d.to, d.chainId);
|
||||||
$("error-tx-message").textContent = d.message;
|
$("error-tx-message").textContent = d.message;
|
||||||
|
|
||||||
|
|||||||
@@ -48,9 +48,14 @@ function applyChainSwitchFields(s, newNetworkId) {
|
|||||||
// It is a copy because a save writes a changed s.networkTrackedTokens whole,
|
// It is a copy because a save writes a changed s.networkTrackedTokens whole,
|
||||||
// over any list another page stored. An add or removal on the active network
|
// over any list another page stored. An add or removal on the active network
|
||||||
// must change only s.trackedTokens.
|
// must change only s.trackedTokens.
|
||||||
|
//
|
||||||
|
// Each token of the copy gets `balanceNotFetched`, because the balances are
|
||||||
|
// cleared below and no refresh has read this network's yet. Each reads
|
||||||
|
// `quantity unknown` until one has, not 0.0000.
|
||||||
if (!s.networkTrackedTokens) s.networkTrackedTokens = {};
|
if (!s.networkTrackedTokens) s.networkTrackedTokens = {};
|
||||||
s.networkTrackedTokens[s.networkId] = s.trackedTokens;
|
s.networkTrackedTokens[s.networkId] = s.trackedTokens;
|
||||||
s.trackedTokens = structuredClone(s.networkTrackedTokens[net.id] || []);
|
s.trackedTokens = structuredClone(s.networkTrackedTokens[net.id] || []);
|
||||||
|
for (const token of s.trackedTokens) token.balanceNotFetched = true;
|
||||||
|
|
||||||
s.networkId = net.id;
|
s.networkId = net.id;
|
||||||
s.rpcUrl = remembered.rpcUrl || net.defaultRpcUrl;
|
s.rpcUrl = remembered.rpcUrl || net.defaultRpcUrl;
|
||||||
|
|||||||
@@ -11,14 +11,44 @@
|
|||||||
// switch keeps each network's records apart. A refresh takes the field off the
|
// switch keeps each network's records apart. A refresh takes the field off the
|
||||||
// stored record's tokens and keeps another page's changes to the stored list.
|
// stored record's tokens and keeps another page's changes to the stored list.
|
||||||
//
|
//
|
||||||
|
// Every tracked token carries that field again once an address or a wallet is
|
||||||
|
// added, and after a network switch, so it reads the same until a refresh has
|
||||||
|
// read it (https://git.eeqj.de/sneak/AutistMask/issues/720).
|
||||||
|
//
|
||||||
// Driven through the real popup over a stored profile on Ethereum Mainnet,
|
// Driven through the real popup over a stored profile on Ethereum Mainnet,
|
||||||
// with "Show tracked tokens with zero balance" on, as it is by default.
|
// with "Show tracked tokens with zero balance" on, as it is by default.
|
||||||
|
|
||||||
|
const { HDNodeWallet, Mnemonic } = require("ethers");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
bootPopup,
|
bootPopup,
|
||||||
cleanupPopup,
|
cleanupPopup,
|
||||||
unversionedValidProfile,
|
unversionedValidProfile,
|
||||||
} = require("./support/popupBoot");
|
} = require("./support/popupBoot");
|
||||||
|
const { hdWalletFromMnemonic } = require("../src/shared/wallet");
|
||||||
|
|
||||||
|
const PHRASE =
|
||||||
|
"abandon abandon abandon abandon abandon abandon " +
|
||||||
|
"abandon abandon abandon abandon abandon about";
|
||||||
|
const PASSWORD = "correct horse battery staple";
|
||||||
|
|
||||||
|
// What AddWallet's tabs take. The first and last scan for more addresses.
|
||||||
|
const FROM_PHRASE = {
|
||||||
|
tab: "tab-mnemonic",
|
||||||
|
field: "wallet-mnemonic",
|
||||||
|
secret: PHRASE,
|
||||||
|
};
|
||||||
|
const FROM_PRIVATE_KEY = {
|
||||||
|
tab: "tab-privkey",
|
||||||
|
field: "import-private-key",
|
||||||
|
secret: "0x" + "11".repeat(32),
|
||||||
|
};
|
||||||
|
const FROM_XPRV = {
|
||||||
|
tab: "tab-xprv",
|
||||||
|
field: "import-xprv-key",
|
||||||
|
secret: HDNodeWallet.fromSeed(Mnemonic.fromPhrase(PHRASE).computeSeed())
|
||||||
|
.extendedKey,
|
||||||
|
};
|
||||||
|
|
||||||
// Tracked by nobody until a test adds it.
|
// Tracked by nobody until a test adds it.
|
||||||
const NEW_TOKEN = "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
const NEW_TOKEN = "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
||||||
@@ -66,6 +96,34 @@ function bootOnAddress(extra) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The popup over one wallet that "+" can derive an address from. Its one
|
||||||
|
// address holds a million NEW_TOKEN, and NEW_TOKEN is tracked.
|
||||||
|
function bootOnDerivingWallet() {
|
||||||
|
const { xpub, firstAddress } = hdWalletFromMnemonic(PHRASE);
|
||||||
|
return bootPopup(
|
||||||
|
unversionedValidProfile({
|
||||||
|
wallets: [
|
||||||
|
{
|
||||||
|
type: "hd",
|
||||||
|
name: "Wallet 1",
|
||||||
|
xpub,
|
||||||
|
encryptedSecret: "encrypted-secret-1",
|
||||||
|
nextIndex: 1,
|
||||||
|
addresses: [
|
||||||
|
{
|
||||||
|
address: firstAddress,
|
||||||
|
balance: "1.5",
|
||||||
|
tokenBalances: [MILLION],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
activeAddress: firstAddress,
|
||||||
|
trackedTokens: [NEW_TRACKED],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Holds the next balance refresh open. The returned function lets it finish
|
// Holds the next balance refresh open. The returned function lets it finish
|
||||||
// with the address's ETH balance at 2 and `tokenBalances` as its token
|
// with the address's ETH balance at 2 and `tokenBalances` as its token
|
||||||
// entries, having read the balance of every token tracked when it started.
|
// entries, having read the balance of every token tracked when it started.
|
||||||
@@ -175,6 +233,47 @@ function newTokenRow(page, id = "address-balances") {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every address's row for NEW_TOKEN on Home, as markup, in the order Home lists
|
||||||
|
// the addresses.
|
||||||
|
function newTokenRowsOnHome(page) {
|
||||||
|
return page
|
||||||
|
.node("wallet-list")
|
||||||
|
.innerHTML.split("<div ")
|
||||||
|
.filter((row) =>
|
||||||
|
row.includes('data-token="' + NEW_TOKEN.toLowerCase() + '"'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The "+" button Home draws for the first wallet. As in firstRemoveButton(),
|
||||||
|
// the list hands Home a stand-in for that button each time it is drawn, and
|
||||||
|
// `press()` presses the last one drawn. Call it before Home next draws its list.
|
||||||
|
function plusButton(page) {
|
||||||
|
const button = { press: null };
|
||||||
|
page.node("wallet-list").querySelectorAll = (selector) =>
|
||||||
|
selector !== ".btn-add-address"
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
dataset: { wallet: "0" },
|
||||||
|
addEventListener: (name, fn) => {
|
||||||
|
button.press = () =>
|
||||||
|
fn({ stopPropagation: () => {} });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Add wallet" on Home, filled in from `source`, and "Import".
|
||||||
|
async function addWallet(page, { tab, field, secret }) {
|
||||||
|
await page.click("btn-add-wallet-bottom");
|
||||||
|
await page.click(tab);
|
||||||
|
page.node(field).value = secret;
|
||||||
|
page.node("add-wallet-password").value = PASSWORD;
|
||||||
|
page.node("add-wallet-password-confirm").value = PASSWORD;
|
||||||
|
await page.click("btn-add-wallet-confirm");
|
||||||
|
}
|
||||||
|
|
||||||
// AddressDetail for the one address opened from Home, as its `[info]` button
|
// AddressDetail for the one address opened from Home, as its `[info]` button
|
||||||
// does.
|
// does.
|
||||||
async function openAddress(page) {
|
async function openAddress(page) {
|
||||||
@@ -456,3 +555,208 @@ test("a refresh does not bring back a token that another page removed after this
|
|||||||
NEW_TRACKED,
|
NEW_TRACKED,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("an address just added reads quantity unknown for a tracked token until a refresh has read it", async () => {
|
||||||
|
const page = await bootOnDerivingWallet();
|
||||||
|
const plus = plusButton(page);
|
||||||
|
await refreshOnce(page, [MILLION]);
|
||||||
|
const finish = holdNextRefresh(page);
|
||||||
|
|
||||||
|
await plus.press();
|
||||||
|
await page.settle();
|
||||||
|
|
||||||
|
const [held, added] = newTokenRowsOnHome(page);
|
||||||
|
expect(held).toContain("1000000.0000");
|
||||||
|
expect(added).toContain("quantity unknown");
|
||||||
|
expect(added).not.toContain("0.0000");
|
||||||
|
expect(
|
||||||
|
page.storage.read("autistmask").trackedTokens[0].balanceNotFetched,
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
// It reads the new address's balance, which is zero.
|
||||||
|
await finish([MILLION]);
|
||||||
|
|
||||||
|
expect(newTokenRowsOnHome(page)[1]).toContain("0.0000");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('an address "+" adds during a refresh reads quantity unknown when that refresh returns', async () => {
|
||||||
|
const page = await bootOnDerivingWallet();
|
||||||
|
const plus = plusButton(page);
|
||||||
|
await refreshOnce(page, [MILLION]);
|
||||||
|
const finishRunning = holdNextRefresh(page);
|
||||||
|
const ticking = page.tick();
|
||||||
|
await page.settle();
|
||||||
|
|
||||||
|
await plus.press();
|
||||||
|
await page.settle();
|
||||||
|
const finishNext = holdNextRefresh(page);
|
||||||
|
await finishRunning([MILLION]);
|
||||||
|
await ticking;
|
||||||
|
|
||||||
|
const added = newTokenRowsOnHome(page)[1];
|
||||||
|
expect(added).toContain("quantity unknown");
|
||||||
|
expect(added).not.toContain("0.0000");
|
||||||
|
|
||||||
|
// It reads the new address's balance, which is zero.
|
||||||
|
await finishNext([MILLION]);
|
||||||
|
|
||||||
|
expect(newTokenRowsOnHome(page)[1]).toContain("0.0000");
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([FROM_PHRASE, FROM_PRIVATE_KEY, FROM_XPRV])(
|
||||||
|
"a wallet just added from $tab reads quantity unknown for a tracked token until a refresh has read it",
|
||||||
|
async (source) => {
|
||||||
|
const page = await bootPopup(
|
||||||
|
unversionedValidProfile({ trackedTokens: [NEW_TRACKED] }),
|
||||||
|
);
|
||||||
|
const finish = holdNextRefresh(page);
|
||||||
|
|
||||||
|
await addWallet(page, source);
|
||||||
|
|
||||||
|
const added = newTokenRowsOnHome(page)[1];
|
||||||
|
expect(added).toContain("quantity unknown");
|
||||||
|
expect(added).not.toContain("0.0000");
|
||||||
|
|
||||||
|
// It reads the new address's balance, which is zero.
|
||||||
|
await finish([]);
|
||||||
|
|
||||||
|
expect(newTokenRowsOnHome(page)[1]).toContain("0.0000");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test("a wallet from a private key added during a refresh reads quantity unknown when that refresh returns", async () => {
|
||||||
|
const page = await bootPopup(
|
||||||
|
unversionedValidProfile({ trackedTokens: [NEW_TRACKED] }),
|
||||||
|
);
|
||||||
|
const finishRunning = holdNextRefresh(page);
|
||||||
|
const ticking = page.tick();
|
||||||
|
await page.settle();
|
||||||
|
|
||||||
|
await addWallet(page, FROM_PRIVATE_KEY);
|
||||||
|
const finishNext = holdNextRefresh(page);
|
||||||
|
await finishRunning([]);
|
||||||
|
await ticking;
|
||||||
|
|
||||||
|
const added = newTokenRowsOnHome(page)[1];
|
||||||
|
expect(added).toContain("quantity unknown");
|
||||||
|
expect(added).not.toContain("0.0000");
|
||||||
|
|
||||||
|
// It reads the new address's balance, which is zero.
|
||||||
|
await finishNext([]);
|
||||||
|
|
||||||
|
expect(newTokenRowsOnHome(page)[1]).toContain("0.0000");
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([FROM_PHRASE, FROM_XPRV])(
|
||||||
|
"addresses the scan finds for a wallet from $tab read quantity unknown, even after a refresh returned during the scan",
|
||||||
|
async (source) => {
|
||||||
|
const page = await bootPopup(
|
||||||
|
unversionedValidProfile({ trackedTokens: [NEW_TRACKED] }),
|
||||||
|
);
|
||||||
|
const { scanForAddresses } = require("../src/shared/balances");
|
||||||
|
let finish;
|
||||||
|
scanForAddresses.mockImplementationOnce(async () => {
|
||||||
|
await refreshOnce(page, []);
|
||||||
|
finish = holdNextRefresh(page);
|
||||||
|
return {
|
||||||
|
addresses: [
|
||||||
|
{ address: "0x" + "dd".repeat(20), index: 0 },
|
||||||
|
{ address: "0x" + "ee".repeat(20), index: 1 },
|
||||||
|
],
|
||||||
|
nextIndex: 2,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await addWallet(page, source);
|
||||||
|
|
||||||
|
const [, first, second] = newTokenRowsOnHome(page);
|
||||||
|
for (const found of [first, second]) {
|
||||||
|
expect(found).toContain("quantity unknown");
|
||||||
|
expect(found).not.toContain("0.0000");
|
||||||
|
}
|
||||||
|
|
||||||
|
await finish([]);
|
||||||
|
|
||||||
|
expect(newTokenRowsOnHome(page)[2]).toContain("0.0000");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test.each([FROM_PHRASE, FROM_XPRV])(
|
||||||
|
"addresses the scan finds for a wallet from $tab read quantity unknown when a refresh running as the scan ends returns",
|
||||||
|
async (source) => {
|
||||||
|
const page = await bootPopup(
|
||||||
|
unversionedValidProfile({ trackedTokens: [NEW_TRACKED] }),
|
||||||
|
);
|
||||||
|
const { scanForAddresses } = require("../src/shared/balances");
|
||||||
|
let finishRunning;
|
||||||
|
let ticking;
|
||||||
|
scanForAddresses.mockImplementationOnce(async () => {
|
||||||
|
finishRunning = holdNextRefresh(page);
|
||||||
|
ticking = page.tick();
|
||||||
|
await page.settle();
|
||||||
|
return {
|
||||||
|
addresses: [
|
||||||
|
{ address: "0x" + "dd".repeat(20), index: 0 },
|
||||||
|
{ address: "0x" + "ee".repeat(20), index: 1 },
|
||||||
|
],
|
||||||
|
nextIndex: 2,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await addWallet(page, source);
|
||||||
|
const finishNext = holdNextRefresh(page);
|
||||||
|
await finishRunning([]);
|
||||||
|
await ticking;
|
||||||
|
|
||||||
|
const [, first, second] = newTokenRowsOnHome(page);
|
||||||
|
for (const found of [first, second]) {
|
||||||
|
expect(found).toContain("quantity unknown");
|
||||||
|
expect(found).not.toContain("0.0000");
|
||||||
|
}
|
||||||
|
|
||||||
|
// It reads the new addresses' balances, which are zero.
|
||||||
|
await finishNext([]);
|
||||||
|
|
||||||
|
expect(newTokenRowsOnHome(page)[2]).toContain("0.0000");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test("after a network switch in Settings, a tracked token reads quantity unknown until a refresh has read it", async () => {
|
||||||
|
const page = await bootOnAddress({
|
||||||
|
trackedTokens: [OTHER_TOKEN],
|
||||||
|
networkTrackedTokens: { sepolia: [NEW_TRACKED] },
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.click("btn-settings");
|
||||||
|
await chooseNetwork(page, "sepolia");
|
||||||
|
await page.click("btn-settings-back");
|
||||||
|
|
||||||
|
expect(page.visibleViews()).toEqual(["address"]);
|
||||||
|
const row = newTokenRow(page);
|
||||||
|
expect(row).toContain("quantity unknown");
|
||||||
|
expect(row).not.toContain("0.0000");
|
||||||
|
|
||||||
|
// It reads the balance, which is zero.
|
||||||
|
await refreshOnce(page, []);
|
||||||
|
|
||||||
|
expect(newTokenRow(page)).toContain("0.0000");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("after a switch saved by the background, a tracked token reads quantity unknown until a refresh has read it", async () => {
|
||||||
|
const page = await bootOnAddress({
|
||||||
|
networkTrackedTokens: { sepolia: [NEW_TRACKED] },
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
applyChainSwitchFields,
|
||||||
|
} = require("../src/shared/chainSwitchFields");
|
||||||
|
const record = page.storage.read("autistmask");
|
||||||
|
applyChainSwitchFields(record, "sepolia");
|
||||||
|
page.storage.write("autistmask", record);
|
||||||
|
|
||||||
|
// The popup takes the switch, and this refresh reads no token's balance.
|
||||||
|
await page.tick();
|
||||||
|
|
||||||
|
const row = newTokenRow(page);
|
||||||
|
expect(row).toContain("quantity unknown");
|
||||||
|
expect(row).not.toContain("0.0000");
|
||||||
|
});
|
||||||
@@ -44,6 +44,7 @@ jest.doMock("../src/popup/views/helpers", () => ({
|
|||||||
goBack: () => {},
|
goBack: () => {},
|
||||||
clearViewStack: () => {},
|
clearViewStack: () => {},
|
||||||
onViewLeave: () => {},
|
onViewLeave: () => {},
|
||||||
|
markTrackedTokensNotFetched: () => {},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// state.js reads chrome.storage.local at load, and saveState() writes it.
|
// state.js reads chrome.storage.local at load, and saveState() writes it.
|
||||||
|
|||||||
@@ -242,7 +242,12 @@ describe("a chain switch on a worker that never loaded state", () => {
|
|||||||
expect(bg.persisted().blockscoutUrl).toBe(CUSTOM_BLOCKSCOUT);
|
expect(bg.persisted().blockscoutUrl).toBe(CUSTOM_BLOCKSCOUT);
|
||||||
expect(bg.persisted().wallets).toEqual(walletFixture());
|
expect(bg.persisted().wallets).toEqual(walletFixture());
|
||||||
expect(bg.persisted().trackedTokens).toEqual([
|
expect(bg.persisted().trackedTokens).toEqual([
|
||||||
{ address: TOKEN, symbol: "DAI", decimals: 18 },
|
{
|
||||||
|
address: TOKEN,
|
||||||
|
symbol: "DAI",
|
||||||
|
decimals: 18,
|
||||||
|
balanceNotFetched: true,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -245,7 +245,9 @@ test("a switch back in Settings after a follow whose write failed is stored", as
|
|||||||
const stored = page.storage.read("autistmask");
|
const stored = page.storage.read("autistmask");
|
||||||
expect(stored.networkId).toBe("mainnet");
|
expect(stored.networkId).toBe("mainnet");
|
||||||
expect(stored.rpcUrl).toBe(NETWORKS.mainnet.defaultRpcUrl);
|
expect(stored.rpcUrl).toBe(NETWORKS.mainnet.defaultRpcUrl);
|
||||||
expect(stored.trackedTokens).toEqual([MAINNET_TOKEN]);
|
expect(stored.trackedTokens).toEqual([
|
||||||
|
{ ...MAINNET_TOKEN, balanceNotFetched: true },
|
||||||
|
]);
|
||||||
expect(page.text("flash-msg")).toBe("Switched to Ethereum Mainnet.");
|
expect(page.text("flash-msg")).toBe("Switched to Ethereum Mainnet.");
|
||||||
const told = globalThis.chrome.runtime.sendMessage.mock.calls
|
const told = globalThis.chrome.runtime.sendMessage.mock.calls
|
||||||
.map(([message]) => message)
|
.map(([message]) => message)
|
||||||
|
|||||||
@@ -340,3 +340,75 @@ describe.each([
|
|||||||
expect(node("error-tx-summary").textContent).toBe("1.0000 ETH");
|
expect(node("error-tx-summary").textContent).toBe("1.0000 ETH");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// https://git.eeqj.de/sneak/AutistMask/issues/722: a WRAP_ETH of 1 ETH into the
|
||||||
|
// router, an UNWRAP_WETH into the router, then a V4 swap of 1 ETH the user
|
||||||
|
// settles. The Amount line reads 1.0000 ETH, though up to 2 ETH can leave. The
|
||||||
|
// rest is on the swap's own Moved from the sender line.
|
||||||
|
describe("a swap with a Moved from the sender line of its own", () => {
|
||||||
|
const ADDRESS_THIS = "0x0000000000000000000000000000000000000002";
|
||||||
|
const TWO_ETH = 2n * ONE_ETH;
|
||||||
|
const SEND_FAILS = {
|
||||||
|
error: "The transaction could not be sent.",
|
||||||
|
retryable: false,
|
||||||
|
};
|
||||||
|
const data = routerIface.encodeFunctionData("execute", [
|
||||||
|
"0x0b0c10",
|
||||||
|
[
|
||||||
|
// WRAP_ETH and UNWRAP_WETH have the same layout.
|
||||||
|
coder.encode(["address", "uint256"], [ADDRESS_THIS, ONE_ETH]),
|
||||||
|
coder.encode(["address", "uint256"], [ADDRESS_THIS, 0n]),
|
||||||
|
coder.encode(
|
||||||
|
["bytes", "bytes[]"],
|
||||||
|
[
|
||||||
|
new Uint8Array([SWAP_EXACT_IN_SINGLE, SETTLE, TAKE_ALL]),
|
||||||
|
[
|
||||||
|
coder.encode(
|
||||||
|
[`tuple(${POOL_KEY},bool,uint128,uint128,bytes)`],
|
||||||
|
[[ETH_USDC_POOL, true, ONE_ETH, 990000n, "0x"]],
|
||||||
|
),
|
||||||
|
// The user pays this one, not the router.
|
||||||
|
coder.encode(
|
||||||
|
["address", "uint256", "bool"],
|
||||||
|
[ZERO, 0n, true],
|
||||||
|
),
|
||||||
|
takeAllUsdc,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
9999999999n,
|
||||||
|
]);
|
||||||
|
|
||||||
|
function expectMovedLine(id) {
|
||||||
|
const html = node(id).innerHTML;
|
||||||
|
expect(html).toContain("Moved from the sender");
|
||||||
|
expect(html).toContain("Up to 1.0000 ETH");
|
||||||
|
}
|
||||||
|
|
||||||
|
test("the wait screen shows that line's amount under the Amount figure", async () => {
|
||||||
|
await approveSwap(data, { txHash: TX_HASH }, TWO_ETH);
|
||||||
|
expect(node("wait-tx-summary").textContent).toBe("1.0000 ETH");
|
||||||
|
expectMovedLine("wait-tx-token-note");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the error screen shows that line's amount under the Amount figure", async () => {
|
||||||
|
await approveSwap(data, SEND_FAILS, TWO_ETH);
|
||||||
|
expect(node("error-tx-summary").textContent).toBe("1.0000 ETH");
|
||||||
|
expectMovedLine("error-tx-token-note");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a reopened popup's wait screen still shows it", async () => {
|
||||||
|
await approveSwap(data, { txHash: TX_HASH }, TWO_ETH);
|
||||||
|
node("wait-tx-token-note").innerHTML = "";
|
||||||
|
txStatus.restoreWait();
|
||||||
|
expectMovedLine("wait-tx-token-note");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a reopened popup's error screen still shows it", async () => {
|
||||||
|
await approveSwap(data, SEND_FAILS, TWO_ETH);
|
||||||
|
node("error-tx-token-note").innerHTML = "";
|
||||||
|
txStatus.renderError();
|
||||||
|
expectMovedLine("error-tx-token-note");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -180,7 +180,10 @@ test("a token added by contract address on Sepolia is tracked on Sepolia only",
|
|||||||
await boot.click("btn-settings");
|
await boot.click("btn-settings");
|
||||||
await chooseNetwork(boot, "mainnet");
|
await chooseNetwork(boot, "mainnet");
|
||||||
expect(stored().networkId).toBe("mainnet");
|
expect(stored().networkId).toBe("mainnet");
|
||||||
expect(stored().trackedTokens).toEqual([MAINNET_TOKEN]);
|
// No refresh has read a balance on this network since the switch.
|
||||||
|
expect(stored().trackedTokens).toEqual([
|
||||||
|
{ ...MAINNET_TOKEN, balanceNotFetched: true },
|
||||||
|
]);
|
||||||
const onMainnet = boot.node("settings-tracked-tokens").innerHTML;
|
const onMainnet = boot.node("settings-tracked-tokens").innerHTML;
|
||||||
expect(onMainnet).toContain("DAI");
|
expect(onMainnet).toContain("DAI");
|
||||||
expect(onMainnet).not.toContain("TST");
|
expect(onMainnet).not.toContain("TST");
|
||||||
@@ -276,7 +279,9 @@ test("a token tracked on Sepolia when another page switched there is not tracked
|
|||||||
|
|
||||||
const stored = boot.storage.read("autistmask");
|
const stored = boot.storage.read("autistmask");
|
||||||
expect(stored.networkId).toBe("sepolia");
|
expect(stored.networkId).toBe("sepolia");
|
||||||
expect(stored.trackedTokens).toEqual([SEPOLIA_TOKEN]);
|
expect(stored.trackedTokens).toEqual([
|
||||||
|
{ ...SEPOLIA_TOKEN, balanceNotFetched: true },
|
||||||
|
]);
|
||||||
expect(stored.networkTrackedTokens.mainnet).toEqual([]);
|
expect(stored.networkTrackedTokens.mainnet).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -441,7 +446,10 @@ test("a token another page added on Ethereum Mainnet is tracked when this page s
|
|||||||
|
|
||||||
const stored = boot.storage.read("autistmask");
|
const stored = boot.storage.read("autistmask");
|
||||||
expect(stored.networkId).toBe("mainnet");
|
expect(stored.networkId).toBe("mainnet");
|
||||||
expect(stored.trackedTokens).toEqual([MAINNET_TOKEN, newToken]);
|
expect(stored.trackedTokens).toEqual([
|
||||||
|
{ ...MAINNET_TOKEN, balanceNotFetched: true },
|
||||||
|
{ ...newToken, balanceNotFetched: true },
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// This page tracks DAI on Ethereum Mainnet and switches to Sepolia. Another
|
// This page tracks DAI on Ethereum Mainnet and switches to Sepolia. Another
|
||||||
@@ -489,7 +497,10 @@ test("a token another page added on Ethereum Mainnet stays when this page adds a
|
|||||||
const stored = boot.storage.read("autistmask");
|
const stored = boot.storage.read("autistmask");
|
||||||
expect(stored.networkTrackedTokens.mainnet).toEqual([MAINNET_TOKEN]);
|
expect(stored.networkTrackedTokens.mainnet).toEqual([MAINNET_TOKEN]);
|
||||||
expect(stored.networkTrackedTokens.sepolia).toEqual([SEPOLIA_TOKEN]);
|
expect(stored.networkTrackedTokens.sepolia).toEqual([SEPOLIA_TOKEN]);
|
||||||
expect(stored.trackedTokens).toEqual([SEPOLIA_TOKEN, added]);
|
expect(stored.trackedTokens).toEqual([
|
||||||
|
{ ...SEPOLIA_TOKEN, balanceNotFetched: true },
|
||||||
|
added,
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// This page tracks DAI on Ethereum Mainnet and switches to Sepolia, where TST is
|
// This page tracks DAI on Ethereum Mainnet and switches to Sepolia, where TST is
|
||||||
@@ -513,7 +524,10 @@ test("a token another page removed on Ethereum Mainnet stays removed when this p
|
|||||||
const stored = boot.storage.read("autistmask");
|
const stored = boot.storage.read("autistmask");
|
||||||
expect(stored.networkTrackedTokens.mainnet).toEqual([]);
|
expect(stored.networkTrackedTokens.mainnet).toEqual([]);
|
||||||
expect(stored.networkTrackedTokens.sepolia).toEqual([SEPOLIA_TOKEN]);
|
expect(stored.networkTrackedTokens.sepolia).toEqual([SEPOLIA_TOKEN]);
|
||||||
expect(stored.trackedTokens).toEqual([SEPOLIA_TOKEN, added]);
|
expect(stored.trackedTokens).toEqual([
|
||||||
|
{ ...SEPOLIA_TOKEN, balanceNotFetched: true },
|
||||||
|
added,
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Another popup page records a fraud contract on Ethereum Mainnet after this
|
// Another popup page records a fraud contract on Ethereum Mainnet after this
|
||||||
@@ -1031,7 +1045,9 @@ describe("a contract lookup that answers after a switch adds nothing", () => {
|
|||||||
await addAcrossSwitch(boot, "btn-settings-addtoken-manual", "mainnet");
|
await addAcrossSwitch(boot, "btn-settings-addtoken-manual", "mainnet");
|
||||||
expect(boot.visibleViews()).toEqual(["settings"]);
|
expect(boot.visibleViews()).toEqual(["settings"]);
|
||||||
expect(boot.text("flash-msg")).toBe(NETWORK_CHANGED_FLASH);
|
expect(boot.text("flash-msg")).toBe(NETWORK_CHANGED_FLASH);
|
||||||
expect(stored().trackedTokens).toEqual([MAINNET_TOKEN]);
|
expect(stored().trackedTokens).toEqual([
|
||||||
|
{ ...MAINNET_TOKEN, balanceNotFetched: true },
|
||||||
|
]);
|
||||||
expect(stored().networkTrackedTokens.sepolia).toEqual([]);
|
expect(stored().networkTrackedTokens.sepolia).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1044,7 +1060,9 @@ describe("a contract lookup that answers after a switch adds nothing", () => {
|
|||||||
await addAcrossSwitch(boot, "btn-add-token-confirm", "sepolia");
|
await addAcrossSwitch(boot, "btn-add-token-confirm", "sepolia");
|
||||||
expect(boot.visibleViews()).toEqual(["settings"]);
|
expect(boot.visibleViews()).toEqual(["settings"]);
|
||||||
expect(boot.text("flash-msg")).toBe(NETWORK_CHANGED_FLASH);
|
expect(boot.text("flash-msg")).toBe(NETWORK_CHANGED_FLASH);
|
||||||
expect(stored().trackedTokens).toEqual([SEPOLIA_TOKEN]);
|
expect(stored().trackedTokens).toEqual([
|
||||||
|
{ ...SEPOLIA_TOKEN, balanceNotFetched: true },
|
||||||
|
]);
|
||||||
expect(stored().networkTrackedTokens.mainnet).toEqual([MAINNET_TOKEN]);
|
expect(stored().networkTrackedTokens.mainnet).toEqual([MAINNET_TOKEN]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -672,6 +672,86 @@ describe("SuccessTx and ErrorTx for a Sepolia transaction with mainnet active",
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("the Moved from the sender lines of a swap on WaitTx and ErrorTx", () => {
|
||||||
|
const moved = (value) => ({ label: "Moved from the sender", value });
|
||||||
|
const decoded = {
|
||||||
|
details: [
|
||||||
|
moved("Up to 1.0000 ETH"),
|
||||||
|
{ label: "Token moved", value: "ETH" },
|
||||||
|
moved("<b>400.0000</b> DAI"),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
function expectEveryLine(id) {
|
||||||
|
const html = getElement(id).innerHTML;
|
||||||
|
expect(html).toContain("Up to 1.0000 ETH");
|
||||||
|
expect(html).toContain("<b>400.0000</b> DAI");
|
||||||
|
expect(html).not.toContain("Token moved");
|
||||||
|
}
|
||||||
|
|
||||||
|
test("WaitTx shows each line's amount, escaped", () => {
|
||||||
|
mockReceiptLookup.mockResolvedValue(null);
|
||||||
|
txStatus.showWait({ ...TX_INFO, decoded }, TX_HASH);
|
||||||
|
expectEveryLine("wait-tx-token-note");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ErrorTx shows each line's amount, escaped", () => {
|
||||||
|
txStatus.showError({ ...TX_INFO, decoded }, TX_HASH, "It failed.");
|
||||||
|
expectEveryLine("error-tx-token-note");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a wait that ends in ErrorTx keeps them", async () => {
|
||||||
|
mockReceiptLookup.mockResolvedValue(null);
|
||||||
|
txStatus.showWait({ ...TX_INFO, decoded }, TX_HASH);
|
||||||
|
await jest.advanceTimersByTimeAsync(60000);
|
||||||
|
expect(state.currentView).toBe("error-tx");
|
||||||
|
expectEveryLine("error-tx-token-note");
|
||||||
|
});
|
||||||
|
|
||||||
|
// A saved record comes back from storage, in whatever shape it is in. A
|
||||||
|
// throw here would leave the popup's restore unfinished.
|
||||||
|
test("a saved record in another shape shows only the lines it can read", () => {
|
||||||
|
mockReceiptLookup.mockResolvedValue(null);
|
||||||
|
function noteOnReopen(saved) {
|
||||||
|
state.viewData = {
|
||||||
|
pendingWait: {
|
||||||
|
hash: TX_HASH,
|
||||||
|
txInfo: { ...TX_INFO, decoded: saved },
|
||||||
|
broadcastTime: Date.now(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(txStatus.restoreWait()).toBe(true);
|
||||||
|
const waitNote = getElement("wait-tx-token-note").innerHTML;
|
||||||
|
txStatus.endWait();
|
||||||
|
|
||||||
|
state.viewData = {
|
||||||
|
message: "It failed.",
|
||||||
|
to: RECIPIENT,
|
||||||
|
chainId: TX_INFO.chainId,
|
||||||
|
decoded: saved,
|
||||||
|
};
|
||||||
|
txStatus.renderError();
|
||||||
|
expect(getElement("error-tx-token-note").innerHTML).toBe(waitNote);
|
||||||
|
return waitNote;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const saved of [
|
||||||
|
7,
|
||||||
|
"text",
|
||||||
|
{ details: 7 },
|
||||||
|
{ details: [null] },
|
||||||
|
{ details: [{ label: "Moved from the sender" }] },
|
||||||
|
{ details: [moved(7)] },
|
||||||
|
{ details: [moved({ toString: 1 })] },
|
||||||
|
]) {
|
||||||
|
expect(noteOnReopen(saved)).toBe("");
|
||||||
|
}
|
||||||
|
expect(
|
||||||
|
noteOnReopen({ details: [null, moved("Up to 1.0000 ETH")] }),
|
||||||
|
).toContain("Up to 1.0000 ETH");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
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