Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72f1eb7217 |
@@ -899,15 +899,13 @@ on the confirmation screen for the wallet's own send
|
|||||||
(`src/popup/views/confirmTx.js`). Both screens render a network fee through
|
(`src/popup/views/confirmTx.js`). Both screens render a network fee through
|
||||||
`formatFee()` in `src/popup/views/helpers.js`, which prices the exact fee in USD
|
`formatFee()` in `src/popup/views/helpers.js`, which prices the exact fee in USD
|
||||||
rather than its truncated figure, so the same fee reads the same on both, USD
|
rather than its truncated figure, so the same fee reads the same on both, USD
|
||||||
value included. Balances are stored exactly (`src/shared/balances.js`), so a
|
value included. The ETH balance is stored exactly, so a balance below the floor
|
||||||
balance below the floor reaches these screens as it is; the one cut is that a
|
reaches these screens as it is. A token balance is stored to six decimal places,
|
||||||
token declaring more than 18 decimals is stored to 18, the most the confirmation
|
and a holding below 0.000001 is not listed at all. The history and balance lists
|
||||||
screen's balance check reads. The history list (`src/shared/transactions.js`)
|
(`src/shared/transactions.js`) use the unfloored one: the transaction detail
|
||||||
uses the unfloored one: the transaction detail view is the authoritative record
|
view is the authoritative record and already shows exact precision. The
|
||||||
and already shows exact precision. The balance lists use neither: they round to
|
4-decimal rule is unchanged everywhere else, including for amounts at or above
|
||||||
four places with `toFixed(4)` (`balanceLine()` in `src/popup/views/helpers.js`).
|
the floor on the approval screens.
|
||||||
The 4-decimal rule is unchanged everywhere else, including for amounts at or
|
|
||||||
above the floor on the approval screens.
|
|
||||||
|
|
||||||
The floor applies only where the token's scale is known. Where it is not, the
|
The floor applies only where the token's scale is known. Where it is not, the
|
||||||
approval screen states base units instead of a quantity — see Unknown token
|
approval screen states base units instead of a quantity — see Unknown token
|
||||||
@@ -1060,15 +1058,13 @@ Which tokens an address shows is decided by `fetchTokenBalances()` in
|
|||||||
`src/shared/balances.js`, from the Blockscout `token-balances` response, so
|
`src/shared/balances.js`, from the Blockscout `token-balances` response, so
|
||||||
tokens do appear without the user adding them. An ERC-20 is shown when its
|
tokens do appear without the user adding them. An ERC-20 is shown when its
|
||||||
balance is nonzero and it is in the bundled known-token list, is tracked by the
|
balance is nonzero and it is in the bundled known-token list, is tracked by the
|
||||||
user, or has 1,000 or more holders. A holding below 0.000001 of a token the user
|
user, or has 1,000 or more holders; a token claiming a symbol from the bundled
|
||||||
does not track is dust and is not shown; a tracked token's holding is shown
|
list from any other contract address is always dropped, and so is any token
|
||||||
whatever its size. A token claiming a symbol from the bundled list from any
|
claiming a symbol that belongs to the native asset and therefore has no
|
||||||
other contract address is always dropped, and so is any token claiming a symbol
|
legitimate contract at all (`"ETH"`). That filter is unconditional — the "Hide
|
||||||
that belongs to the native asset and therefore has no legitimate contract at all
|
tokens with fewer than 1,000 holders" setting governs the transaction history
|
||||||
(`"ETH"`). That filter is unconditional — the "Hide tokens with fewer than 1,000
|
and the send-screen token selector, not this list. Tracked tokens with a zero
|
||||||
holders" setting governs the transaction history and the send-screen token
|
balance are listed as well while "Show tracked tokens with zero balance" is on.
|
||||||
selector, not this list. Tracked tokens with a zero balance are listed as well
|
|
||||||
while "Show tracked tokens with zero balance" is on.
|
|
||||||
|
|
||||||
#### Stored state and its version
|
#### Stored state and its version
|
||||||
|
|
||||||
@@ -1594,14 +1590,8 @@ view would leave a wallet one click from deletion.
|
|||||||
a value carrying its unit, hex (`0x10`) or exponent (`1e3`) notation —
|
a value carrying its unit, hex (`0x10`) or exponent (`1e3`) notation —
|
||||||
is refused with a flash message and the field snaps back to the stored
|
is refused with a flash message and the field snaps back to the stored
|
||||||
threshold, so a number the user did not type is never stored.
|
threshold, so a number the user did not type is never stored.
|
||||||
- Allowed Sites: the hostnames remembered as allowed, under any address,
|
- Allowed Sites: list with remove buttons
|
||||||
with remove buttons
|
- Denied Sites: list with remove buttons
|
||||||
- Connected Sites: the hostnames of the sites allowed without "Remember my
|
|
||||||
choice" that are still connected, with remove buttons. Only the background
|
|
||||||
holds these, in memory, and Settings asks it for them with
|
|
||||||
`AUTISTMASK_GET_CONNECTED_SITES`
|
|
||||||
- Denied Sites: the hostnames remembered as denied, under any address, with
|
|
||||||
remove buttons
|
|
||||||
- About: project link, license, author, version, release date, and the
|
- About: project link, license, author, version, release date, and the
|
||||||
commit, which links to the commit in the repository
|
commit, which links to the commit in the repository
|
||||||
- Debug: hidden until revealed, then an "Enable debug mode" checkbox that
|
- Debug: hidden until revealed, then an "Enable debug mode" checkbox that
|
||||||
@@ -1612,15 +1602,8 @@ view would leave a wallet one click from deletion.
|
|||||||
- `[recovery phrase]` on an HD wallet → **ShowRecoveryPhrase**
|
- `[recovery phrase]` on an HD wallet → **ShowRecoveryPhrase**
|
||||||
- `[x]` on a wallet → **DeleteWallet**
|
- `[x]` on a wallet → **DeleteWallet**
|
||||||
- Tap wallet name → inline rename field (no screen change)
|
- Tap wallet name → inline rename field (no screen change)
|
||||||
- `[x]` on a tracked token → removes it in place (no screen change)
|
- `[x]` on a tracked token or a site → removes it in place (no screen
|
||||||
- `[x]` on an allowed or connected site → disconnects that site, in place:
|
change)
|
||||||
its hostname is dropped from Allowed Sites under every address, and
|
|
||||||
`AUTISTMASK_REMOVE_SITE` has the background end every connection approved
|
|
||||||
without "Remember" from an origin with that hostname, under any address,
|
|
||||||
and send `accountsChanged` with an empty list to the site's open tabs.
|
|
||||||
Only the extension's own pages may send either message
|
|
||||||
- `[x]` on a denied site → forgets the refusal, in place; it connects
|
|
||||||
nothing and tells the background nothing
|
|
||||||
- Ten clicks on the version → reveals the Debug well (no screen change)
|
- Ten clicks on the version → reveals the Debug well (no screen change)
|
||||||
- "Back" (or Settings gear again) → previous screen (Home)
|
- "Back" (or Settings gear again) → previous screen (Home)
|
||||||
|
|
||||||
@@ -1815,8 +1798,7 @@ view would leave a wallet one click from deletion.
|
|||||||
- **Transitions**:
|
- **Transitions**:
|
||||||
- "Allow" / "Deny" → closes popup (returns result to background script; the
|
- "Allow" / "Deny" → closes popup (returns result to background script; the
|
||||||
choice is persisted to the allowed or denied list when "Remember" is
|
choice is persisted to the allowed or denied list when "Remember" is
|
||||||
checked, and an "Allow" without it is listed under Connected Sites in
|
checked)
|
||||||
**Settings**)
|
|
||||||
- Popup closed without answering → treated as a denial
|
- Popup closed without answering → treated as a denial
|
||||||
|
|
||||||
#### TxApproval (`approve-tx`)
|
#### TxApproval (`approve-tx`)
|
||||||
|
|||||||
@@ -45,34 +45,20 @@ but the review is broader than any of them.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
- 2026-10-04: The Send and confirmation screens no longer show an ETH balance, a
|
- 2026-10-04: The Send and confirmation screens no longer show an ETH balance or
|
||||||
token balance or a network fee below 0.000001 as zero
|
a network fee below 0.000001 as `0.0`
|
||||||
([#343](https://git.eeqj.de/sneak/AutistMask/issues/343)). The stored balances
|
([#343](https://git.eeqj.de/sneak/AutistMask/issues/343)). The stored ETH
|
||||||
(`src/shared/balances.js`) and the confirmation screen's fee were each cut to
|
balance (`src/shared/balances.js`) and the confirmation screen's fee were each
|
||||||
six decimal places by a rule of their own. Balances are now stored exactly,
|
cut to six decimal places by a rule of their own. The ETH balance is now
|
||||||
except that a token declaring more than 18 decimals is stored to 18, the most
|
stored exactly, and the Send screen's `Current balance`, and the confirmation
|
||||||
the balance check reads. A token holding below 0.000001 is still not listed,
|
screen's balance, fee, reserve and insufficient-balance messages, go through
|
||||||
but only for a token the user does not track, so a tracked token's holding
|
`truncateAmountNeverZero()` in `src/shared/amountDisplay.js`, the helper the
|
||||||
reaches the Send screen and the send is checked against it. The Send screen's
|
approval screen already used. The confirmation and approval screens both
|
||||||
`Current balance`, and the confirmation screen's balance, fee, reserve and
|
render the fee through `formatFee()` in `src/popup/views/helpers.js`, which
|
||||||
insufficient-balance messages, go through `truncateAmountNeverZero()` in
|
prices the exact fee in USD, so the same fee reads the same on both, USD value
|
||||||
`src/shared/amountDisplay.js`, the helper the approval screen already used.
|
included. Token balances are still stored to six decimal places: that cut is
|
||||||
The confirmation and approval screens both render the fee through
|
also what leaves a holding below 0.000001 off the balance list, and keeps the
|
||||||
`formatFee()` in `src/popup/views/helpers.js`, which prices the exact fee in
|
stored string within the 18 decimals the balance check reads.
|
||||||
USD, so the same fee reads the same on both, USD value included.
|
|
||||||
- 2026-10-04: Settings lists the sites connected without "Remember", and
|
|
||||||
removing a site there disconnects it
|
|
||||||
([#406](https://git.eeqj.de/sneak/AutistMask/issues/406)). Such a connection
|
|
||||||
lives only in the background's in-memory `connectedSites` map, so Settings
|
|
||||||
never showed it and the user could not end it; `AUTISTMASK_REMOVE_SITE`, sent
|
|
||||||
on every remove, did nothing. Settings now asks the background for those sites
|
|
||||||
(`AUTISTMASK_GET_CONNECTED_SITES`) and lists them under Connected Sites.
|
|
||||||
Removing a site from Allowed Sites or Connected Sites drops its remembered
|
|
||||||
entry under every address and sends `AUTISTMASK_REMOVE_SITE` with the
|
|
||||||
hostname; the background deletes every `connectedSites` entry for that
|
|
||||||
hostname and sends `accountsChanged` with an empty list to its open tabs. Only
|
|
||||||
the extension's own pages may send either message. Removing a denied site no
|
|
||||||
longer sends it, since forgetting a refusal ends no connection.
|
|
||||||
- 2026-10-04: Removing an address or deleting a wallet ends every site
|
- 2026-10-04: Removing an address or deleting a wallet ends every site
|
||||||
connection approved without "Remember" for the addresses removed
|
connection approved without "Remember" for the addresses removed
|
||||||
([#245](https://git.eeqj.de/sneak/AutistMask/issues/245)). Such a connection
|
([#245](https://git.eeqj.de/sneak/AutistMask/issues/245)). Such a connection
|
||||||
|
|||||||
+1
-41
@@ -1110,24 +1110,6 @@ async function broadcastAccountsChanged() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tell every open tab of a site Settings removed that it has no account.
|
|
||||||
async function broadcastSiteRemoved(hostname) {
|
|
||||||
let tabs;
|
|
||||||
try {
|
|
||||||
tabs = await tabsQuery({});
|
|
||||||
} catch {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const tab of tabs) {
|
|
||||||
if (!tab.url || extractHostname(tab.url) !== hostname) continue;
|
|
||||||
tabsSendMessage(tab.id, {
|
|
||||||
type: "AUTISTMASK_EVENT",
|
|
||||||
eventName: "accountsChanged",
|
|
||||||
data: [],
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Background balance refresh: every 60 seconds when the popup isn't open.
|
// Background balance refresh: every 60 seconds when the popup isn't open.
|
||||||
// When the popup IS open, its 10-second interval keeps lastBalanceRefresh
|
// When the popup IS open, its 10-second interval keeps lastBalanceRefresh
|
||||||
// fresh, so this naturally skips.
|
// fresh, so this naturally skips.
|
||||||
@@ -1324,8 +1306,6 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
"AUTISTMASK_TX_RESPONSE",
|
"AUTISTMASK_TX_RESPONSE",
|
||||||
"AUTISTMASK_SIGN_RESPONSE",
|
"AUTISTMASK_SIGN_RESPONSE",
|
||||||
"AUTISTMASK_ADDRESSES_REMOVED",
|
"AUTISTMASK_ADDRESSES_REMOVED",
|
||||||
"AUTISTMASK_GET_CONNECTED_SITES",
|
|
||||||
"AUTISTMASK_REMOVE_SITE",
|
|
||||||
];
|
];
|
||||||
if (POPUP_ONLY_TYPES.includes(msg.type) && !isExtensionSender(sender)) {
|
if (POPUP_ONLY_TYPES.includes(msg.type) && !isExtensionSender(sender)) {
|
||||||
sendResponse({ error: "Unauthorized sender" });
|
sendResponse({ error: "Unauthorized sender" });
|
||||||
@@ -1682,28 +1662,8 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Settings lists the sites connected without "Remember", which only this
|
|
||||||
// worker knows. The origin is what precedes the key's last colon.
|
|
||||||
if (msg.type === "AUTISTMASK_GET_CONNECTED_SITES") {
|
|
||||||
sendResponse(
|
|
||||||
Object.keys(connectedSites).map((key) =>
|
|
||||||
extractHostname(key.slice(0, key.lastIndexOf(":"))),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Settings removed this site and has already dropped its remembered
|
|
||||||
// entries. Its connections approved without "Remember" end here, under
|
|
||||||
// every address, and its open tabs are told it has no account.
|
|
||||||
if (msg.type === "AUTISTMASK_REMOVE_SITE") {
|
if (msg.type === "AUTISTMASK_REMOVE_SITE") {
|
||||||
for (const key of Object.keys(connectedSites)) {
|
// Popup already saved state; nothing else needed
|
||||||
const origin = key.slice(0, key.lastIndexOf(":"));
|
|
||||||
if (extractHostname(origin) === msg.hostname) {
|
|
||||||
delete connectedSites[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
broadcastSiteRemoved(msg.hostname);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1064,15 +1064,6 @@
|
|||||||
<div id="settings-allowed-sites"></div>
|
<div id="settings-allowed-sites"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-well p-3 mx-1 mb-3">
|
|
||||||
<h3 class="font-bold mb-1">Connected Sites</h3>
|
|
||||||
<p class="text-xs text-muted mb-2">
|
|
||||||
Sites you allowed without "Remember my choice".
|
|
||||||
Switching address disconnects them.
|
|
||||||
</p>
|
|
||||||
<div id="settings-connected-sites"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-well p-3 mx-1 mb-3">
|
<div class="bg-well p-3 mx-1 mb-3">
|
||||||
<h3 class="font-bold mb-1">Denied Sites</h3>
|
<h3 class="font-bold mb-1">Denied Sites</h3>
|
||||||
<p class="text-xs text-muted mb-2">
|
<p class="text-xs text-muted mb-2">
|
||||||
|
|||||||
+23
-51
@@ -29,61 +29,44 @@ const {
|
|||||||
GITEA_COMMIT_URL,
|
GITEA_COMMIT_URL,
|
||||||
} = require("../../shared/buildInfo");
|
} = require("../../shared/buildInfo");
|
||||||
|
|
||||||
const { notify, sendMessage } = require("../../shared/browserApi");
|
const { notify } = require("../../shared/browserApi");
|
||||||
|
|
||||||
let versionClickCount = 0;
|
let versionClickCount = 0;
|
||||||
let versionClickTimer = null;
|
let versionClickTimer = null;
|
||||||
|
|
||||||
// One row per hostname, however many addresses or origins it appears under,
|
function renderSiteList(containerId, siteMap, stateKey) {
|
||||||
// each with an [x] that hands it to onRemove.
|
|
||||||
function renderSiteList(containerId, hostnames, onRemove) {
|
|
||||||
const container = $(containerId);
|
const container = $(containerId);
|
||||||
const unique = [...new Set(hostnames)];
|
const hostnames = [...new Set(Object.values(siteMap).flat())];
|
||||||
if (unique.length === 0) {
|
if (hostnames.length === 0) {
|
||||||
container.innerHTML = '<p class="text-xs text-muted">None</p>';
|
container.innerHTML = '<p class="text-xs text-muted">None</p>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let html = "";
|
let html = "";
|
||||||
unique.forEach((hostname) => {
|
hostnames.forEach((hostname) => {
|
||||||
html += `<div class="flex justify-between items-center text-xs py-1 border-b border-border-light">`;
|
html += `<div class="flex justify-between items-center text-xs py-1 border-b border-border-light">`;
|
||||||
// A hostname the URL parser produced cannot carry a delimiter, so
|
// A hostname the URL parser produced cannot carry a delimiter, so
|
||||||
// this is escaped for the rule rather than for a known hole — the
|
// this is escaped for the rule rather than for a known hole — the
|
||||||
// rule being that nothing reaches innerHTML unescaped.
|
// rule being that nothing reaches innerHTML unescaped.
|
||||||
html += `<span>${escapeHtml(hostname)}</span>`;
|
html += `<span>${escapeHtml(hostname)}</span>`;
|
||||||
html += `<button class="btn-remove-site border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer" data-hostname="${escapeHtml(hostname)}">[x]</button>`;
|
html += `<button class="btn-remove-site border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer" data-key="${escapeHtml(stateKey)}" data-hostname="${escapeHtml(hostname)}">[x]</button>`;
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
});
|
});
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
container.querySelectorAll(".btn-remove-site").forEach((btn) => {
|
container.querySelectorAll(".btn-remove-site").forEach((btn) => {
|
||||||
btn.addEventListener("click", () => onRemove(btn.dataset.hostname));
|
btn.addEventListener("click", async () => {
|
||||||
|
const key = btn.dataset.key;
|
||||||
|
const host = btn.dataset.hostname;
|
||||||
|
for (const addr of Object.keys(state[key])) {
|
||||||
|
state[key][addr] = state[key][addr].filter((h) => h !== host);
|
||||||
|
if (state[key][addr].length === 0) {
|
||||||
|
delete state[key][addr];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await saveState();
|
||||||
|
notify({ type: "AUTISTMASK_REMOVE_SITE" });
|
||||||
|
renderSiteList(containerId, state[key], key);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Drop a hostname from a remembered site list under every address.
|
|
||||||
function forgetHostname(siteMap, hostname) {
|
|
||||||
for (const addr of Object.keys(siteMap)) {
|
|
||||||
siteMap[addr] = siteMap[addr].filter((h) => h !== hostname);
|
|
||||||
if (siteMap[addr].length === 0) {
|
|
||||||
delete siteMap[addr];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removing a site from Allowed Sites or Connected Sites disconnects it: it is
|
|
||||||
// no longer allowed under any address, and the background ends its
|
|
||||||
// connections approved without "Remember" and tells its open tabs.
|
|
||||||
async function removeAllowedSite(hostname) {
|
|
||||||
forgetHostname(state.allowedSites, hostname);
|
|
||||||
await saveState();
|
|
||||||
notify({ type: "AUTISTMASK_REMOVE_SITE", hostname });
|
|
||||||
await renderSiteLists();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removing a denied site only forgets the refusal; it connects nothing.
|
|
||||||
async function removeDeniedSite(hostname) {
|
|
||||||
forgetHostname(state.deniedSites, hostname);
|
|
||||||
await saveState();
|
|
||||||
await renderSiteLists();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTrackedTokens() {
|
function renderTrackedTokens() {
|
||||||
@@ -219,24 +202,13 @@ function show() {
|
|||||||
showView("settings");
|
showView("settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderSiteLists() {
|
function renderSiteLists() {
|
||||||
renderSiteList(
|
renderSiteList(
|
||||||
"settings-allowed-sites",
|
"settings-allowed-sites",
|
||||||
Object.values(state.allowedSites).flat(),
|
state.allowedSites,
|
||||||
removeAllowedSite,
|
"allowedSites",
|
||||||
);
|
|
||||||
renderSiteList(
|
|
||||||
"settings-denied-sites",
|
|
||||||
Object.values(state.deniedSites).flat(),
|
|
||||||
removeDeniedSite,
|
|
||||||
);
|
|
||||||
// Sites allowed without "Remember" are held only by the background, in
|
|
||||||
// memory, so it is asked for them.
|
|
||||||
renderSiteList(
|
|
||||||
"settings-connected-sites",
|
|
||||||
await sendMessage({ type: "AUTISTMASK_GET_CONNECTED_SITES" }),
|
|
||||||
removeAllowedSite,
|
|
||||||
);
|
);
|
||||||
|
renderSiteList("settings-denied-sites", state.deniedSites, "deniedSites");
|
||||||
}
|
}
|
||||||
|
|
||||||
function init(ctx) {
|
function init(ctx) {
|
||||||
|
|||||||
+13
-19
@@ -17,7 +17,6 @@ const { LOW_HOLDER_THRESHOLD, parseHoldersCount } = require("./holders");
|
|||||||
const { isSpoofedSymbol } = require("./symbolSpoof");
|
const { isSpoofedSymbol } = require("./symbolSpoof");
|
||||||
const { toDecimals } = require("./transferAmount");
|
const { toDecimals } = require("./transferAmount");
|
||||||
const { resolveTokenDecimals } = require("./approvalAmount");
|
const { resolveTokenDecimals } = require("./approvalAmount");
|
||||||
const { SCALE_DECIMALS } = require("./txValidation");
|
|
||||||
|
|
||||||
// Use a static network to skip auto-detection (which can fail and cause
|
// Use a static network to skip auto-detection (which can fail and cause
|
||||||
// "could not coalesce error" on some RPC endpoints like Cloudflare).
|
// "could not coalesce error" on some RPC endpoints like Cloudflare).
|
||||||
@@ -53,14 +52,17 @@ function requireNetworkId(networkId) {
|
|||||||
return net;
|
return net;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A token balance as a decimal string, exact except for a token that declares
|
// A token balance cut to six decimal places. Two things rely on the cut: a
|
||||||
// more than 18 decimals: that one is cut to 18 places, the most the balance
|
// holding below 0.000001 comes out as "0.0" and is left off the balance list as
|
||||||
// check on the confirmation screen reads (SCALE_DECIMALS in txValidation.js).
|
// dust, and the stored string never carries more decimals than the balance
|
||||||
|
// check on the confirmation screen can read (18, in txValidation.js), whatever
|
||||||
|
// scale the token declares. Screens truncate it again for display, through
|
||||||
|
// src/shared/amountDisplay.js.
|
||||||
function formatTokenBalance(raw, decimals) {
|
function formatTokenBalance(raw, decimals) {
|
||||||
const val = formatUnits(raw, decimals);
|
const val = formatUnits(raw, decimals);
|
||||||
const parts = val.split(".");
|
const parts = val.split(".");
|
||||||
if (parts.length === 1) return val + ".0";
|
if (parts.length === 1) return val + ".0";
|
||||||
const dec = parts[1].slice(0, SCALE_DECIMALS).replace(/0+$/, "") || "0";
|
const dec = parts[1].slice(0, 6).replace(/0+$/, "") || "0";
|
||||||
return parts[0] + "." + dec;
|
return parts[0] + "." + dec;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +147,11 @@ async function fetchTokenBalances(address, blockscoutUrl, trackedTokens) {
|
|||||||
const scale = known !== null ? known : decimals;
|
const scale = known !== null ? known : decimals;
|
||||||
// null is a holding of an amount that cannot be stated, which is
|
// null is a holding of an amount that cannot be stated, which is
|
||||||
// not the same as a holding of zero, and must never render as one.
|
// not the same as a holding of zero, and must never render as one.
|
||||||
|
// With a scale, the display filter proper applies: a balance that
|
||||||
|
// rounds to zero at six places is dust and is not listed. Without
|
||||||
|
// one there is no such judgement to make, and the row is kept.
|
||||||
const bal = scale === null ? null : formatTokenBalance(raw, scale);
|
const bal = scale === null ? null : formatTokenBalance(raw, scale);
|
||||||
|
if (bal === "0.0") continue;
|
||||||
// null means the explorer reported no count, which is not the
|
// null means the explorer reported no count, which is not the
|
||||||
// same as a count of zero. This gate is not the low-holder
|
// same as a count of zero. This gate is not the low-holder
|
||||||
// display filter: it has no user-facing off switch and governs
|
// display filter: it has no user-facing off switch and governs
|
||||||
@@ -164,19 +170,6 @@ async function fetchTokenBalances(address, blockscoutUrl, trackedTokens) {
|
|||||||
// Skip spam tokens the user never asked to see
|
// Skip spam tokens the user never asked to see
|
||||||
if (!isKnown && !isTracked && !hasEnoughHolders) continue;
|
if (!isKnown && !isTracked && !hasEnoughHolders) continue;
|
||||||
|
|
||||||
// Skip dust: a holding below 0.000001 of a token the user does not
|
|
||||||
// track. A tracked token keeps it, so the Send and confirmation
|
|
||||||
// screens show that holding and check the send against it rather
|
|
||||||
// than against zero. Without a scale there is no such judgement to
|
|
||||||
// make, and the row is kept.
|
|
||||||
if (
|
|
||||||
!isTracked &&
|
|
||||||
scale !== null &&
|
|
||||||
raw * 1000000n < 10n ** BigInt(scale)
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip tokens spoofing a known symbol from a different address.
|
// Skip tokens spoofing a known symbol from a different address.
|
||||||
// Every row here is an ERC-20 the explorer reported, so it has a
|
// Every row here is an ERC-20 the explorer reported, so it has a
|
||||||
// contract address; the native ETH balance is fetched over RPC in
|
// contract address; the native ETH balance is fetched over RPC in
|
||||||
@@ -226,7 +219,8 @@ async function refreshBalances(
|
|||||||
provider
|
provider
|
||||||
.getBalance(addr.address)
|
.getBalance(addr.address)
|
||||||
.then((bal) => {
|
.then((bal) => {
|
||||||
// Exact, never cut: a cut here stores a small nonzero
|
// Exact, never cut: the screens truncate for display
|
||||||
|
// themselves, and a cut here stores a small nonzero
|
||||||
// balance as zero.
|
// balance as zero.
|
||||||
addr.balance = formatEther(bal);
|
addr.balance = formatEther(bal);
|
||||||
log.debugf("ETH balance", addr.address, addr.balance);
|
log.debugf("ETH balance", addr.address, addr.balance);
|
||||||
|
|||||||
@@ -2101,16 +2101,6 @@ describe("a site connection decided as the popup closes", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// What a site is told when it asks which account it may use.
|
|
||||||
async function siteAccounts(bg, origin) {
|
|
||||||
const { sendResponse } = bg.send(
|
|
||||||
{ type: "AUTISTMASK_RPC", method: "eth_accounts", params: [] },
|
|
||||||
{ origin: origin || FRESH_ORIGIN },
|
|
||||||
);
|
|
||||||
await settle();
|
|
||||||
return sendResponse.mock.calls[0][0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// A site connected without "Remember" is held only in the background's memory,
|
// A site connected without "Remember" is held only in the background's memory,
|
||||||
// keyed to the address it was connected to. Removing that address, or the
|
// keyed to the address it was connected to. Removing that address, or the
|
||||||
// wallet holding it, must end the connection as part of the removal itself.
|
// wallet holding it, must end the connection as part of the removal itself.
|
||||||
@@ -2146,6 +2136,16 @@ describe("removing an address ends a site's connection to it", () => {
|
|||||||
return bg;
|
return bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// What FRESH_ORIGIN is told when it asks which account it may use.
|
||||||
|
async function siteAccounts(bg) {
|
||||||
|
const { sendResponse } = bg.send(
|
||||||
|
{ type: "AUTISTMASK_RPC", method: "eth_accounts", params: [] },
|
||||||
|
{ origin: FRESH_ORIGIN },
|
||||||
|
);
|
||||||
|
await settle();
|
||||||
|
return sendResponse.mock.calls[0][0];
|
||||||
|
}
|
||||||
|
|
||||||
test("removing the connected address ends the connection", async () => {
|
test("removing the connected address ends the connection", async () => {
|
||||||
const bg = await connectedBackground();
|
const bg = await connectedBackground();
|
||||||
expect(await siteAccounts(bg)).toEqual({ result: [signer.address] });
|
expect(await siteAccounts(bg)).toEqual({ result: [signer.address] });
|
||||||
@@ -2192,168 +2192,3 @@ describe("removing an address ends a site's connection to it", () => {
|
|||||||
expect(await siteAccounts(bg)).toEqual({ result: [signer.address] });
|
expect(await siteAccounts(bg)).toEqual({ result: [signer.address] });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Settings lists the sites allowed without "Remember", which only the
|
|
||||||
// background holds, and removing a site there, from either list, disconnects
|
|
||||||
// it. These drive the real Settings view against the real background and
|
|
||||||
// click the [x] the user clicks.
|
|
||||||
describe("removing a site in Settings disconnects it", () => {
|
|
||||||
// FRESH_ORIGIN on another port, so its hostname is FRESH_ORIGIN's.
|
|
||||||
const FRESH_OTHER_PORT = "https://fresh.example:8443";
|
|
||||||
|
|
||||||
// A site list's container. Its [x] buttons, data attributes and all, are
|
|
||||||
// read back out of the rows the view wrote into it, so clicking one runs
|
|
||||||
// the handler the view attached to it.
|
|
||||||
function fakeSiteList() {
|
|
||||||
const list = {
|
|
||||||
innerHTML: "",
|
|
||||||
buttons: [],
|
|
||||||
querySelectorAll() {
|
|
||||||
const tags = list.innerHTML.match(/<button[^>]*>/g) || [];
|
|
||||||
list.buttons = tags.map((tag) => ({
|
|
||||||
dataset: Object.fromEntries(
|
|
||||||
[...tag.matchAll(/data-(\w+)="([^"]*)"/g)].map(
|
|
||||||
(match) => [match[1], match[2]],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
addEventListener(event, handler) {
|
|
||||||
this[event] = handler;
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
return list.buttons;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The hostnames a site list shows.
|
|
||||||
function listed(list) {
|
|
||||||
return [...list.innerHTML.matchAll(/data-hostname="([^"]*)"/g)].map(
|
|
||||||
(match) => match[1],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A site connected the way the user does it, in the approval popup.
|
|
||||||
async function connect(bg, origin, remember) {
|
|
||||||
const pending = bg.requestSite(origin);
|
|
||||||
await settle();
|
|
||||||
bg.connectApproval(pending.id()).decide(true, remember);
|
|
||||||
await settle();
|
|
||||||
expect(pending.result()).toEqual({ result: [signer.address] });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Settings, opened over the background's storage and wired to it the way
|
|
||||||
// the popup is: what Settings sends reaches the background from the
|
|
||||||
// extension's own page, and the answer comes back.
|
|
||||||
async function openSettings(bg) {
|
|
||||||
const lists = {};
|
|
||||||
const element = (id) => (lists[id] ||= fakeSiteList());
|
|
||||||
global.document = { getElementById: element };
|
|
||||||
global.chrome.runtime.sendMessage = (msg, callback) => {
|
|
||||||
const { sendResponse } = bg.send(msg, bg.fromPopup);
|
|
||||||
if (callback) callback(sendResponse.mock.calls[0]?.[0]);
|
|
||||||
};
|
|
||||||
await require("../src/shared/state").loadState();
|
|
||||||
await require("../src/popup/views/settings").renderSiteLists();
|
|
||||||
return {
|
|
||||||
allowed: element("settings-allowed-sites"),
|
|
||||||
connected: element("settings-connected-sites"),
|
|
||||||
// Click the [x] beside a site, and let what it sends run.
|
|
||||||
remove: async (list, hostname) => {
|
|
||||||
expect(listed(list)).toContain(hostname);
|
|
||||||
const button = list.buttons.find(
|
|
||||||
(b) => b.dataset.hostname === hostname,
|
|
||||||
);
|
|
||||||
await button.click();
|
|
||||||
await settle();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
delete global.document;
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Settings lists a site connected without Remember", async () => {
|
|
||||||
const bg = loadBackground({ actionPopup: true });
|
|
||||||
await connect(bg, FRESH_ORIGIN, false);
|
|
||||||
|
|
||||||
const settings = await openSettings(bg);
|
|
||||||
|
|
||||||
expect(listed(settings.connected)).toEqual(["fresh.example"]);
|
|
||||||
expect(listed(settings.allowed)).toEqual([HOSTNAME]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("removing a site connected without Remember disconnects it and tells its tabs", async () => {
|
|
||||||
const bg = loadBackground({ actionPopup: true });
|
|
||||||
await connect(bg, FRESH_ORIGIN, false);
|
|
||||||
const sentToTabs = [];
|
|
||||||
global.chrome.tabs = {
|
|
||||||
query: (q, cb) =>
|
|
||||||
cb([
|
|
||||||
{ id: 1, url: FRESH_ORIGIN + "/app" },
|
|
||||||
{ id: 2, url: ORIGIN + "/app" },
|
|
||||||
]),
|
|
||||||
sendMessage: (tabId, msg, cb) => {
|
|
||||||
sentToTabs.push({ tabId, msg });
|
|
||||||
cb();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const settings = await openSettings(bg);
|
|
||||||
|
|
||||||
await settings.remove(settings.connected, "fresh.example");
|
|
||||||
|
|
||||||
expect(await siteAccounts(bg)).toEqual({ result: [] });
|
|
||||||
expect(sentToTabs).toEqual([
|
|
||||||
{
|
|
||||||
tabId: 1,
|
|
||||||
msg: {
|
|
||||||
type: "AUTISTMASK_EVENT",
|
|
||||||
eventName: "accountsChanged",
|
|
||||||
data: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
expect(listed(settings.connected)).toEqual([]);
|
|
||||||
// The other site is untouched.
|
|
||||||
expect(await siteAccounts(bg, ORIGIN)).toEqual({
|
|
||||||
result: [signer.address],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// The same hostname can hold both kinds of connection: one origin allowed
|
|
||||||
// without Remember, then another, on a different port, allowed with it.
|
|
||||||
test("removing a remembered site also ends its connection made without Remember", async () => {
|
|
||||||
const bg = loadBackground({ actionPopup: true });
|
|
||||||
await connect(bg, FRESH_ORIGIN, false);
|
|
||||||
await connect(bg, FRESH_OTHER_PORT, true);
|
|
||||||
const settings = await openSettings(bg);
|
|
||||||
|
|
||||||
await settings.remove(settings.allowed, "fresh.example");
|
|
||||||
|
|
||||||
expect(await siteAccounts(bg, FRESH_OTHER_PORT)).toEqual({
|
|
||||||
result: [],
|
|
||||||
});
|
|
||||||
expect(await siteAccounts(bg, FRESH_ORIGIN)).toEqual({ result: [] });
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a page can neither remove a site nor list the connected ones", async () => {
|
|
||||||
const bg = loadBackground({ actionPopup: true });
|
|
||||||
await connect(bg, FRESH_ORIGIN, false);
|
|
||||||
const page = { url: FRESH_ORIGIN + "/index.html" };
|
|
||||||
|
|
||||||
const remove = bg.send(
|
|
||||||
{ type: "AUTISTMASK_REMOVE_SITE", hostname: "fresh.example" },
|
|
||||||
page,
|
|
||||||
);
|
|
||||||
const list = bg.send({ type: "AUTISTMASK_GET_CONNECTED_SITES" }, page);
|
|
||||||
|
|
||||||
expect(remove.sendResponse).toHaveBeenCalledWith({
|
|
||||||
error: "Unauthorized sender",
|
|
||||||
});
|
|
||||||
expect(list.sendResponse).toHaveBeenCalledWith({
|
|
||||||
error: "Unauthorized sender",
|
|
||||||
});
|
|
||||||
expect(await siteAccounts(bg)).toEqual({ result: [signer.address] });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
+23
-102
@@ -1,16 +1,15 @@
|
|||||||
// The balance and fee lines of the Send and confirmation screens, and the fee
|
// The balance and fee lines of the Send and confirmation screens, and the fee
|
||||||
// line they must share with the approval screen.
|
// line they must share with the approval screen.
|
||||||
//
|
//
|
||||||
// An ETH balance, a token balance or a fee below 0.000001 rendered as zero on
|
// An ETH balance or a fee below 0.000001 rendered as `0.0` on these screens
|
||||||
// these screens (https://git.eeqj.de/sneak/AutistMask/issues/343): the stored
|
// (https://git.eeqj.de/sneak/AutistMask/issues/343): the stored balance and the
|
||||||
// balances and the fee were each cut to six decimal places, a rule of their
|
// fee were each cut to six decimal places, a rule of their own, while the
|
||||||
// own, while the approval screen showed the same fee through
|
// approval screen showed the same fee through src/shared/amountDisplay.js with
|
||||||
// src/shared/amountDisplay.js with the nonzero floor. The balances are now
|
// the nonzero floor. Both now go through that one helper.
|
||||||
// stored exactly, and the screens show them and the fee through that helper.
|
|
||||||
//
|
//
|
||||||
// Driven through the real refreshBalances(), Send screen, confirmation screen
|
// Driven through the real refreshBalances(), Send screen, confirmation screen
|
||||||
// and approval screen, with only the node, the explorer and the DOM stubbed: a
|
// and approval screen, with only the node and the DOM stubbed: a balance
|
||||||
// balance written onto state by hand would skip the place the cut happened.
|
// written onto state by hand would skip the place the cut happened.
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
@@ -20,9 +19,6 @@ const mockNode = {
|
|||||||
feeData: { maxFeePerGas: 1n, gasPrice: 1n },
|
feeData: { maxFeePerGas: 1n, gasPrice: 1n },
|
||||||
};
|
};
|
||||||
|
|
||||||
// The token rows the stub explorer reports for the address.
|
|
||||||
const mockExplorer = { items: [] };
|
|
||||||
|
|
||||||
jest.mock("ethers", () => {
|
jest.mock("ethers", () => {
|
||||||
const actual = jest.requireActual("ethers");
|
const actual = jest.requireActual("ethers");
|
||||||
class StubProvider {
|
class StubProvider {
|
||||||
@@ -59,11 +55,11 @@ jest.mock("../src/shared/log", () => ({
|
|||||||
warnf: () => {},
|
warnf: () => {},
|
||||||
errorf: () => {},
|
errorf: () => {},
|
||||||
},
|
},
|
||||||
// The explorer's token list, which refreshBalances() also fetches.
|
// The explorer's token list, which refreshBalances() also fetches: empty.
|
||||||
debugFetch: jest.fn(async () => ({
|
debugFetch: jest.fn(async () => ({
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
json: async () => mockExplorer.items,
|
json: async () => [],
|
||||||
})),
|
})),
|
||||||
setRuntimeDebug: () => {},
|
setRuntimeDebug: () => {},
|
||||||
isDebug: () => false,
|
isDebug: () => false,
|
||||||
@@ -151,42 +147,17 @@ const approval = require("../src/popup/views/approval");
|
|||||||
const HOLDER = "0x" + "a".repeat(40);
|
const HOLDER = "0x" + "a".repeat(40);
|
||||||
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
|
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
|
||||||
|
|
||||||
// 0.0000005 ETH, or 0.0000005 of an 18-decimal token.
|
// 0.0000005 ETH.
|
||||||
const HALF_MICRO_ETH = 500000000000n;
|
const HALF_MICRO_ETH = 500000000000n;
|
||||||
|
|
||||||
// A token the bundled list does not know.
|
|
||||||
const TOKEN = "0x" + "d".repeat(40);
|
|
||||||
|
|
||||||
// The explorer's row for TOKEN, holding `value` base units. With only five
|
|
||||||
// holders, it is listed only when the user tracks the token.
|
|
||||||
function tokenRow(value, token = {}) {
|
|
||||||
return {
|
|
||||||
value: String(value),
|
|
||||||
token: {
|
|
||||||
type: "ERC-20",
|
|
||||||
address_hash: TOKEN,
|
|
||||||
symbol: "TOK",
|
|
||||||
name: "Token",
|
|
||||||
decimals: "18",
|
|
||||||
holders_count: "5",
|
|
||||||
...token,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function text(id) {
|
function text(id) {
|
||||||
return global.document.getElementById(id).textContent;
|
return global.document.getElementById(id).textContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
function errors() {
|
// The ETH balance the node reports, fetched and stored exactly where the popup
|
||||||
return global.document.getElementById("confirm-errors").innerHTML;
|
// stores it.
|
||||||
}
|
async function refreshWith(balanceWei) {
|
||||||
|
|
||||||
// The ETH balance the node reports and the token rows the explorer reports,
|
|
||||||
// fetched and stored exactly where the popup stores them.
|
|
||||||
async function refreshWith(balanceWei, tokenItems = []) {
|
|
||||||
mockNode.balanceWei = balanceWei;
|
mockNode.balanceWei = balanceWei;
|
||||||
mockExplorer.items = tokenItems;
|
|
||||||
state.wallets = [{ name: "Wallet 1", addresses: [{ address: HOLDER }] }];
|
state.wallets = [{ name: "Wallet 1", addresses: [{ address: HOLDER }] }];
|
||||||
state.selectedWallet = 0;
|
state.selectedWallet = 0;
|
||||||
state.selectedAddress = 0;
|
state.selectedAddress = 0;
|
||||||
@@ -194,18 +165,17 @@ async function refreshWith(balanceWei, tokenItems = []) {
|
|||||||
state.wallets,
|
state.wallets,
|
||||||
"https://rpc.example.invalid",
|
"https://rpc.example.invalid",
|
||||||
"https://blockscout.example/api/v2",
|
"https://blockscout.example/api/v2",
|
||||||
state.trackedTokens,
|
[],
|
||||||
"mainnet",
|
"mainnet",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Press Review on the Send screen for a send of `token` ("ETH" or a token
|
// Press Review on the Send screen for an ETH send, and show the confirmation
|
||||||
// address), and show the confirmation screen it leads to with its fee estimate
|
// screen it leads to with its fee estimate settled.
|
||||||
// settled.
|
async function confirmEthSend(amount) {
|
||||||
async function confirmSend(amount, token = "ETH") {
|
|
||||||
let txInfo = null;
|
let txInfo = null;
|
||||||
send.init({ showConfirmTx: (info) => (txInfo = info) });
|
send.init({ showConfirmTx: (info) => (txInfo = info) });
|
||||||
state.selectedToken = token;
|
state.selectedToken = "ETH";
|
||||||
global.document.getElementById("send-to").value = RECIPIENT;
|
global.document.getElementById("send-to").value = RECIPIENT;
|
||||||
global.document.getElementById("send-amount").value = amount;
|
global.document.getElementById("send-amount").value = amount;
|
||||||
await global.document
|
await global.document
|
||||||
@@ -254,70 +224,21 @@ describe("an ETH balance below 0.000001 never renders as zero", () => {
|
|||||||
|
|
||||||
test("on the confirmation screen", async () => {
|
test("on the confirmation screen", async () => {
|
||||||
await refreshWith(HALF_MICRO_ETH);
|
await refreshWith(HALF_MICRO_ETH);
|
||||||
await confirmSend("0.0000001");
|
await confirmEthSend("0.0000001");
|
||||||
expect(text("confirm-balance")).toBe("0.0000005 ETH");
|
expect(text("confirm-balance")).toBe("0.0000005 ETH");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("while a balance above the floor keeps four decimals", async () => {
|
test("while a balance above the floor keeps four decimals", async () => {
|
||||||
await refreshWith(1234567890000000000n);
|
await refreshWith(1234567890000000000n);
|
||||||
await confirmSend("0.1");
|
await confirmEthSend("0.1");
|
||||||
expect(text("confirm-balance")).toBe("1.2345 ETH");
|
expect(text("confirm-balance")).toBe("1.2345 ETH");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// A token the user tracks stays on the balance list when the explorer's row is
|
|
||||||
// dropped, so a holding of it below 0.000001 reached these screens as zero, and
|
|
||||||
// the send was checked against zero.
|
|
||||||
describe("a tracked token holding below 0.000001 never renders as zero", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
state.trackedTokens = [
|
|
||||||
{ address: TOKEN, symbol: "TOK", name: "Token", decimals: 18 },
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
test("on the Send screen", async () => {
|
|
||||||
await refreshWith(10n ** 18n, [tokenRow(HALF_MICRO_ETH)]);
|
|
||||||
state.selectedToken = TOKEN;
|
|
||||||
send.updateSendBalance();
|
|
||||||
expect(text("send-balance")).toBe("Current balance: 0.0000005 TOK");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("on the confirmation screen, which checks the send against it", async () => {
|
|
||||||
await refreshWith(10n ** 18n, [tokenRow(HALF_MICRO_ETH)]);
|
|
||||||
await confirmSend("0.0000005", TOKEN);
|
|
||||||
expect(text("confirm-balance")).toBe("0.0000005 TOK");
|
|
||||||
expect(errors()).toBe("");
|
|
||||||
await confirmSend("0.0000006", TOKEN);
|
|
||||||
expect(errors()).toContain(
|
|
||||||
"You have 0.0000005 TOK but are trying to send 0.0000006 TOK.",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Past 18 places the stored balance is cut: the balance check reads 18,
|
|
||||||
// and refuses a balance string longer than that as no balance at all.
|
|
||||||
test("with more than 18 decimals, the send is checked against 18 of them", async () => {
|
|
||||||
state.trackedTokens[0].decimals = 24;
|
|
||||||
// 1.5 plus one base unit.
|
|
||||||
const value = 15n * 10n ** 23n + 1n;
|
|
||||||
await refreshWith(10n ** 18n, [tokenRow(value, { decimals: "24" })]);
|
|
||||||
await confirmSend("1.5", TOKEN);
|
|
||||||
expect(text("confirm-balance")).toBe("1.5000 TOK");
|
|
||||||
expect(errors()).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("while an untracked holding below 0.000001 is still not listed", async () => {
|
|
||||||
state.trackedTokens = [];
|
|
||||||
await refreshWith(10n ** 18n, [
|
|
||||||
tokenRow(HALF_MICRO_ETH, { holders_count: "50000" }),
|
|
||||||
]);
|
|
||||||
expect(state.wallets[0].addresses[0].tokenBalances).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("a fee below 0.000001 ETH never renders as zero", () => {
|
describe("a fee below 0.000001 ETH never renders as zero", () => {
|
||||||
test("when the estimate and the reserve are the same", async () => {
|
test("when the estimate and the reserve are the same", async () => {
|
||||||
await refreshWith(10n ** 18n);
|
await refreshWith(10n ** 18n);
|
||||||
await confirmSend("0.1");
|
await confirmEthSend("0.1");
|
||||||
// 21000 gas at 1 wei is 0.000000000000021 ETH, shown to its first
|
// 21000 gas at 1 wei is 0.000000000000021 ETH, shown to its first
|
||||||
// significant digit.
|
// significant digit.
|
||||||
expect(text("confirm-fee-amount")).toBe("0.00000000000002 ETH");
|
expect(text("confirm-fee-amount")).toBe("0.00000000000002 ETH");
|
||||||
@@ -326,7 +247,7 @@ describe("a fee below 0.000001 ETH never renders as zero", () => {
|
|||||||
test("when they differ, on both lines", async () => {
|
test("when they differ, on both lines", async () => {
|
||||||
mockNode.feeData = { maxFeePerGas: 2n, gasPrice: 1n };
|
mockNode.feeData = { maxFeePerGas: 2n, gasPrice: 1n };
|
||||||
await refreshWith(10n ** 18n);
|
await refreshWith(10n ** 18n);
|
||||||
await confirmSend("0.1");
|
await confirmEthSend("0.1");
|
||||||
expect(text("confirm-fee-amount")).toBe("~0.00000000000002 ETH");
|
expect(text("confirm-fee-amount")).toBe("~0.00000000000002 ETH");
|
||||||
expect(text("confirm-fee-reserve")).toBe(
|
expect(text("confirm-fee-reserve")).toBe(
|
||||||
"up to 0.00000000000004 ETH reserved",
|
"up to 0.00000000000004 ETH reserved",
|
||||||
@@ -355,7 +276,7 @@ describe("the same fee reads the same on the confirmation and approval screens",
|
|||||||
])("%s", async (_label, feePerGas, expected) => {
|
])("%s", async (_label, feePerGas, expected) => {
|
||||||
mockNode.feeData = { maxFeePerGas: feePerGas, gasPrice: feePerGas };
|
mockNode.feeData = { maxFeePerGas: feePerGas, gasPrice: feePerGas };
|
||||||
await refreshWith(10n ** 18n);
|
await refreshWith(10n ** 18n);
|
||||||
await confirmSend("0.1");
|
await confirmEthSend("0.1");
|
||||||
expect(text("confirm-fee-amount")).toBe(expected);
|
expect(text("confirm-fee-amount")).toBe(expected);
|
||||||
await approveTxWithFeePerGas(feePerGas);
|
await approveTxWithFeePerGas(feePerGas);
|
||||||
expect(text("approve-tx-fee")).toBe(expected);
|
expect(text("approve-tx-fee")).toBe(expected);
|
||||||
|
|||||||
Reference in New Issue
Block a user