fix: Blockscout and Etherscan lookups are off until the user turns them on (closes #668)
Out of the box the wallet sent the user's addresses to Blockscout for history and token lookups. It sent each recipient to Etherscan for its label. Two settings, "Use Blockscout" and "Check recipients on Etherscan", now gate those requests. Both are off in a new install and offered unticked on Welcome and in Settings. While one is off, the screens that relied on it say so in a plain line. The balance refresh and the Etherscan check read the stored setting. An open popup takes a change another page saved and draws its screens again. While "Use Blockscout" is off, no address keeps an untracked token. ENS lookups ask only the RPC endpoint. Model: opus-5-5
This commit is contained in:
@@ -12,16 +12,27 @@ intercepts tx status links to their own site instead of going to Etherscan, etc.
|
|||||||
None of the common alternatives work on Firefox.
|
None of the common alternatives work on Firefox.
|
||||||
|
|
||||||
Hence, a minimally viable ERC20 browser wallet/signer that works cross-platform.
|
Hence, a minimally viable ERC20 browser wallet/signer that works cross-platform.
|
||||||
Everything you need, nothing you don't. We import as few libraries as possible,
|
Everything you need, nothing you don't. We import as few libraries as possible
|
||||||
don't implement any crypto, and don't send user-specific data anywhere but a
|
and don't implement any crypto. By default the wallet sends user-specific data
|
||||||
(user-configurable) Ethereum RPC endpoint (which defaults to a public node). The
|
only to the Ethereum RPC endpoint. The user can change that endpoint in
|
||||||
extension contacts three user-configurable services: the configured RPC node for
|
Settings. It defaults to a public node.
|
||||||
blockchain interactions, a public CoinDesk API (no API key) for realtime price
|
|
||||||
information, and a Blockscout block-explorer API for transaction history and
|
Out of the box the extension contacts two services:
|
||||||
token balances. It also performs best-effort Etherscan address label lookups
|
|
||||||
during transaction confirmation. A community-maintained phishing domain
|
- the configured RPC node, for blockchain interactions
|
||||||
blocklist is built into the extension at build time and checked locally; nothing
|
- a public CoinDesk API (no API key), for realtime prices. It is sent token
|
||||||
is fetched for it at runtime.
|
symbols and nothing else.
|
||||||
|
|
||||||
|
Two more services are sent none of the user's data until the user turns them on.
|
||||||
|
Each has a checkbox on the Welcome screen and in Settings:
|
||||||
|
|
||||||
|
- "Use Blockscout": a Blockscout block-explorer API, for transaction history,
|
||||||
|
the tokens the user has not tracked, and transaction details
|
||||||
|
- "Check recipients on Etherscan": a best-effort Etherscan label lookup of the
|
||||||
|
recipient during transaction confirmation
|
||||||
|
|
||||||
|
A community-maintained phishing domain blocklist is built into the extension at
|
||||||
|
build time and checked locally. Nothing is fetched for it at runtime.
|
||||||
|
|
||||||
In the extension is a hardcoded list of the top ERC20 contract addresses. You
|
In the extension is a hardcoded list of the top ERC20 contract addresses. You
|
||||||
can add any ERC20 contract by contract address if you wish, but the hardcoded
|
can add any ERC20 contract by contract address if you wish, but the hardcoded
|
||||||
@@ -422,6 +433,35 @@ It also covers the red banner on the Settings screen: switching the network
|
|||||||
there to Sepolia must put up the `[TESTNET]` banner, and switching back to
|
there to Sepolia must put up the `[TESTNET]` banner, and switching back to
|
||||||
mainnet must take it down, both while Settings is still on screen.
|
mainnet must take it down, both while Settings is still on screen.
|
||||||
|
|
||||||
|
It also covers what a new install sends to Blockscout and Etherscan
|
||||||
|
([#668](https://git.eeqj.de/sneak/AutistMask/issues/668)). The harness installs
|
||||||
|
a route handler before the first popup page opens. From then on, the handler
|
||||||
|
records every request the extension makes, from its pages and from its
|
||||||
|
background worker. A request the background worker makes while it starts up
|
||||||
|
comes earlier and is not recorded. The tests take these steps in order:
|
||||||
|
|
||||||
|
- Welcome must offer "Use Blockscout" and "Check recipients on Etherscan"
|
||||||
|
unticked.
|
||||||
|
- With neither ticked, Home, AddressDetail, AddressToken and ConfirmTx must each
|
||||||
|
show the line saying what is off.
|
||||||
|
- The popup is then closed until the background has run a balance refresh of its
|
||||||
|
own. No recorded request may have gone to Blockscout or Etherscan.
|
||||||
|
- "Use Blockscout" is ticked from AddressDetail's gear. On "Back", AddressDetail
|
||||||
|
must ask Blockscout for its history and no longer say the setting is off.
|
||||||
|
Blockscout must then be asked for the token balances.
|
||||||
|
- The stub Blockscout then reports a token the user has not tracked. Home lists
|
||||||
|
it. "Use Blockscout" is unticked from AddressDetail's gear. On "Back", the
|
||||||
|
token must be gone from AddressDetail, from Send's token dropdown and from
|
||||||
|
Home. Those three screens must each show the line saying the setting is off.
|
||||||
|
The setting is then ticked again.
|
||||||
|
- "Check recipients on Etherscan" is ticked from ConfirmTx's gear. On "Back",
|
||||||
|
ConfirmTx must ask Etherscan about the recipient and show the label the stub
|
||||||
|
page gives it.
|
||||||
|
|
||||||
|
Both settings stay on for the rest of the run. TransactionDetail with "Use
|
||||||
|
Blockscout" off is not driven here, because no history row leads to it then. The
|
||||||
|
unit tests cover it.
|
||||||
|
|
||||||
It also covers the confirmation screen, for both a native ETH send and an ERC-20
|
It also covers the confirmation screen, for both a native ETH send and an ERC-20
|
||||||
send: Send disabled while the fee estimate is in flight, enabled once it lands,
|
send: Send disabled while the fee estimate is in flight, enabled once it lands,
|
||||||
the fee block quoting the expected cost and the reserve separately, the distinct
|
the fee block quoting the expected cost and the reserve separately, the distinct
|
||||||
@@ -845,7 +885,8 @@ src/
|
|||||||
views/ — one JS module per screen (home, send, approval, etc.)
|
views/ — one JS module per screen (home, send, approval, etc.)
|
||||||
shared/ — modules used by both popup and background
|
shared/ — modules used by both popup and background
|
||||||
alarms.js — recurring background jobs (extension alarms API)
|
alarms.js — recurring background jobs (extension alarms API)
|
||||||
balances.js — ETH and tracked token balances via RPC, others via Blockscout
|
balances.js — ETH and tracked token balances via RPC, others via
|
||||||
|
Blockscout while "Use Blockscout" is on
|
||||||
constants.js — chain IDs, default RPC endpoint, ERC-20 ABI
|
constants.js — chain IDs, default RPC endpoint, ERC-20 ABI
|
||||||
ens.js — ENS forward/reverse resolution (popup only)
|
ens.js — ENS forward/reverse resolution (popup only)
|
||||||
holders.js — holder-count parsing and the low-holder rule
|
holders.js — holder-count parsing and the low-holder rule
|
||||||
@@ -1388,23 +1429,38 @@ that token's previous entry, as a failed ETH balance keeps its figure, or takes
|
|||||||
Blockscout's entry from the same refresh when there is none. Every other token
|
Blockscout's entry from the same refresh when there is none. Every other token
|
||||||
comes from the Blockscout `token-balances` response (`fetchTokenBalances()`), so
|
comes from the Blockscout `token-balances` response (`fetchTokenBalances()`), so
|
||||||
tokens do appear without the user adding them, and they keep their previous
|
tokens do appear without the user adding them, and they keep their previous
|
||||||
figures while Blockscout fails. A tracked token is shown when its balance is
|
figures while Blockscout fails.
|
||||||
nonzero, and any other ERC-20 when its balance is nonzero and it is in the
|
|
||||||
bundled known-token list or has 1,000 or more holders. A token claiming a symbol
|
The refresh asks Blockscout only while the user has "Use Blockscout" on. The
|
||||||
from the bundled list from any other contract address is always dropped, tracked
|
setting is off until the user turns it on. While it is off:
|
||||||
or not, and so is any token claiming a symbol that belongs to the native asset
|
|
||||||
and therefore has no legitimate contract at all (`"ETH"`, and every network's
|
- Only tracked tokens are listed. Turning the setting off drops every other
|
||||||
`nativeCurrency`, such as `"SepoliaETH"`, on every network). That filter is
|
token from every address at once.
|
||||||
unconditional — the "Hide tokens with fewer than 1,000 holders" setting governs
|
- A tracked token's entry has no holder count and no `decimals` from Blockscout.
|
||||||
the transaction history and the send-screen token selector, not this list. A
|
- A tracked token's balance uses the decimals from the bundled list, or else the
|
||||||
token's holder count is unknown when the explorer reports none, or reports
|
decimals saved when the token was tracked.
|
||||||
anything other than a whole number written in digits alone, such as `1,000` or
|
|
||||||
`1e3` (`parseHoldersCount()` in `src/shared/holders.js`). This list does not
|
Each refresh, in the popup or the background, reads the stored setting when it
|
||||||
take an unknown count as 1,000 or more, so such a token is shown only when it is
|
starts. If the setting is off when the refresh saves, the save drops every token
|
||||||
on the bundled list or tracked. Every nonzero holding of a token shown is stored
|
the user has not tracked.
|
||||||
exactly, however small, but a holding below 0.000001 is left out of the balance
|
|
||||||
lists, the send-screen token selector, the address total and the remove-address
|
A tracked token is shown when its balance is nonzero, and any other ERC-20 when
|
||||||
warning (`isBelowOneMillionth()` in `src/shared/amountDisplay.js`). The Send and
|
its balance is nonzero and it is in the bundled known-token list or has 1,000 or
|
||||||
|
more holders. A token claiming a symbol from the bundled list from any other
|
||||||
|
contract address is always dropped, tracked or not, and so is any token claiming
|
||||||
|
a symbol that belongs to the native asset and therefore has no legitimate
|
||||||
|
contract at all (`"ETH"`, and every network's `nativeCurrency`, such as
|
||||||
|
`"SepoliaETH"`, on every network). That filter is unconditional — the "Hide
|
||||||
|
tokens with fewer than 1,000 holders" setting governs the transaction history
|
||||||
|
and the send-screen token selector, not this list. A token's holder count is
|
||||||
|
unknown when the explorer reports none, or reports anything other than a whole
|
||||||
|
number written in digits alone, such as `1,000` or `1e3` (`parseHoldersCount()`
|
||||||
|
in `src/shared/holders.js`). This list does not take an unknown count as 1,000
|
||||||
|
or more, so such a token is shown only when it is on the bundled list or
|
||||||
|
tracked. Every nonzero holding of a token shown is stored exactly, however
|
||||||
|
small, but a holding below 0.000001 is left out of the balance lists, the
|
||||||
|
send-screen token selector, the address total and the remove-address warning
|
||||||
|
(`isBelowOneMillionth()` in `src/shared/amountDisplay.js`). The Send and
|
||||||
confirmation screens show it when its token is the one being sent. Tracked
|
confirmation screens show it when its token is the one being sent. Tracked
|
||||||
tokens with a zero balance are listed as well while "Show tracked tokens with
|
tokens with a zero balance are listed as well while "Show tracked tokens with
|
||||||
zero balance" is on.
|
zero balance" is on.
|
||||||
@@ -1578,6 +1634,17 @@ such as an edit typed into Settings and not yet saved. Home is the one screen
|
|||||||
"Back" always re-renders, so the wallet list reflects anything that changed
|
"Back" always re-renders, so the wallet list reflects anything that changed
|
||||||
while the user was away from it.
|
while the user was away from it.
|
||||||
|
|
||||||
|
Changing "Use Blockscout" or "Check recipients on Etherscan" in Settings makes
|
||||||
|
every screen but Settings count as not yet rendered. "Back" then renders again
|
||||||
|
any screen a reopened popup can render. A change to "Use Blockscout" also cuts
|
||||||
|
the stack, as a network switch does (below). The cut drops Send, whose token
|
||||||
|
dropdown was filled under the old setting. "Back" cannot render Send again. A
|
||||||
|
change to "Check recipients on Etherscan" leaves the stack as it is.
|
||||||
|
|
||||||
|
Any other popup open meanwhile takes either change from the stored record, as it
|
||||||
|
takes a network switch (below). It then draws its screen on top again as a
|
||||||
|
reopened popup draws it, so Home takes the place of a screen such as Send.
|
||||||
|
|
||||||
A network switch leaves every screen on the page showing the previous network.
|
A network switch leaves every screen on the page showing the previous network.
|
||||||
The user makes it in Settings, or approves a site's request and the background
|
The user makes it in Settings, or approves a site's request and the background
|
||||||
saves it. Any other popup open meanwhile takes the switch from the stored
|
saves it. Any other popup open meanwhile takes the switch from the stored
|
||||||
@@ -1619,8 +1686,15 @@ view would leave a wallet one click from deletion.
|
|||||||
screen in that case.
|
screen in that case.
|
||||||
- **Elements**:
|
- **Elements**:
|
||||||
- "Welcome! To get started, add a wallet." text
|
- "Welcome! To get started, add a wallet." text
|
||||||
|
- A well with a line saying that the wallet sends the user's data only to
|
||||||
|
the Ethereum RPC endpoint unless one of the two checkboxes below is ticked
|
||||||
|
- In the well, the "Use Blockscout" and "Check recipients on Etherscan"
|
||||||
|
checkboxes, the same two as in Settings. Each has a sentence saying what
|
||||||
|
it sends and where. Both are unticked until the user ticks them
|
||||||
- "Add wallet" button
|
- "Add wallet" button
|
||||||
- **Transitions**:
|
- **Transitions**:
|
||||||
|
- Ticking or unticking either checkbox → stores the setting, as Settings
|
||||||
|
does (no screen change)
|
||||||
- "Add wallet" → **AddWallet**
|
- "Add wallet" → **AddWallet**
|
||||||
|
|
||||||
#### Home (`main`)
|
#### Home (`main`)
|
||||||
@@ -1641,6 +1715,8 @@ view would leave a wallet one click from deletion.
|
|||||||
holding more than one address); the ENS name, if resolved, is below it;
|
holding more than one address); the ENS name, if resolved, is below it;
|
||||||
then the full address on a row of its own, followed by the address USD
|
then the full address on a row of its own, followed by the address USD
|
||||||
total and a balance line for ETH and for each token shown for that address
|
total and a balance line for ETH and for each token shown for that address
|
||||||
|
- While "Use Blockscout" is off, a line under the wallet list: "Only tracked
|
||||||
|
tokens are listed, because "Use Blockscout" is off in Settings."
|
||||||
- "Recent Transactions": up to 25 transactions merged across every address
|
- "Recent Transactions": up to 25 transactions merged across every address
|
||||||
of every wallet, deduplicated by hash and filtered. Each row is three
|
of every wallet, deduplicated by hash and filtered. Each row is three
|
||||||
lines: age and direction, then the counterparty's colour dot (with our own
|
lines: age and direction, then the counterparty's colour dot (with our own
|
||||||
@@ -1648,7 +1724,11 @@ view would leave a wallet one click from deletion.
|
|||||||
counterparty's full address on a row of its own. A contract creation has
|
counterparty's full address on a row of its own. A contract creation has
|
||||||
no counterparty: its second line is the amount alone and its third line
|
no counterparty: its second line is the amount alone and its third line
|
||||||
says "This transaction creates a new contract. It has no recipient." The
|
says "This transaction creates a new contract. It has no recipient." The
|
||||||
transaction lists on AddressDetail and AddressToken draw the same rows
|
transaction lists on AddressDetail and AddressToken draw the same rows.
|
||||||
|
The history comes from Blockscout. While "Use Blockscout" is off,
|
||||||
|
Blockscout is not asked. This list then shows only the line "No
|
||||||
|
transaction history is shown, because "Use Blockscout" is off in
|
||||||
|
Settings."
|
||||||
- "Add additional wallet..." link at bottom
|
- "Add additional wallet..." link at bottom
|
||||||
- **Transitions**:
|
- **Transitions**:
|
||||||
- Tap address row → sets the active address and broadcasts
|
- Tap address row → sets the active address and broadcasts
|
||||||
@@ -1717,9 +1797,13 @@ view would leave a wallet one click from deletion.
|
|||||||
- USD total for address (see [Partial USD totals](#partial-usd-totals))
|
- USD total for address (see [Partial USD totals](#partial-usd-totals))
|
||||||
- Balance list: ETH + the ERC-20 tokens shown for this address (4 decimal
|
- Balance list: ETH + the ERC-20 tokens shown for this address (4 decimal
|
||||||
places, USD inline). Each balance row is clickable → **AddressToken**
|
places, USD inline). Each balance row is clickable → **AddressToken**
|
||||||
|
- While "Use Blockscout" is off, a line under the balance list: "Only
|
||||||
|
tracked tokens are listed, because "Use Blockscout" is off in Settings."
|
||||||
- Send / Receive / + Token buttons and a "···" menu button
|
- Send / Receive / + Token buttons and a "···" menu button
|
||||||
- "···" dropdown containing a single "Export Private Key" entry
|
- "···" dropdown containing a single "Export Private Key" entry
|
||||||
- Transaction list (with ENS resolution for counterparties)
|
- Transaction list (with ENS resolution for counterparties). While "Use
|
||||||
|
Blockscout" is off, it shows only the line "No transaction history is
|
||||||
|
shown, because "Use Blockscout" is off in Settings."
|
||||||
- **Transitions**:
|
- **Transitions**:
|
||||||
- Tap balance row → **AddressToken** (for that token)
|
- Tap balance row → **AddressToken** (for that token)
|
||||||
- "Send" → **Send** (refuses with a flash message on a zero balance)
|
- "Send" → **Send** (refuses with a flash message on a zero balance)
|
||||||
@@ -1787,9 +1871,13 @@ view would leave a wallet one click from deletion.
|
|||||||
- Token contract well (ERC-20 only): full contract address (tap to copy,
|
- Token contract well (ERC-20 only): full contract address (tap to copy,
|
||||||
etherscan link) plus name, symbol, decimals, holder count and project
|
etherscan link) plus name, symbol, decimals, holder count and project
|
||||||
website where known. The "Holders:" row is left out, not shown as 0, when
|
website where known. The "Holders:" row is left out, not shown as 0, when
|
||||||
the token's balance-list entry has no holder count: the explorer did not
|
the token's balance-list entry has no holder count. That happens when:
|
||||||
report a readable one, or the token is not in the balance list
|
- the explorer did not report a readable one
|
||||||
- Token-filtered transaction list (only this token's transfers)
|
- "Use Blockscout" is off
|
||||||
|
- the token is not in the balance list
|
||||||
|
- Token-filtered transaction list (only this token's transfers). While "Use
|
||||||
|
Blockscout" is off, it shows only the line "No transaction history is
|
||||||
|
shown, because "Use Blockscout" is off in Settings."
|
||||||
- **Transitions**:
|
- **Transitions**:
|
||||||
- "Send" → **Send** (token locked: the dropdown is replaced by a static
|
- "Send" → **Send** (token locked: the dropdown is replaced by a static
|
||||||
symbol and contract address)
|
symbol and contract address)
|
||||||
@@ -1806,6 +1894,9 @@ view would leave a wallet one click from deletion.
|
|||||||
- From: address with color dot + etherscan link
|
- From: address with color dot + etherscan link
|
||||||
- What to send: token dropdown (or static display with contract address when
|
- What to send: token dropdown (or static display with contract address when
|
||||||
locked from AddressToken)
|
locked from AddressToken)
|
||||||
|
- While "Use Blockscout" is off, a line under the token dropdown: "Only
|
||||||
|
tracked tokens are listed, because "Use Blockscout" is off in Settings."
|
||||||
|
The line is hidden with the dropdown when Send is opened from AddressToken
|
||||||
- To: address or ENS name input, with an inline validation message
|
- To: address or ENS name input, with an inline validation message
|
||||||
- Amount input with current balance display, which reads
|
- Amount input with current balance display, which reads
|
||||||
`Current balance: unknown (SYMBOL)` for a token whose scale is unknown, as
|
`Current balance: unknown (SYMBOL)` for a token whose scale is unknown, as
|
||||||
@@ -1875,7 +1966,12 @@ view would leave a wallet one click from deletion.
|
|||||||
recipient that has never sent a transaction and holds no ETH, recipient is
|
recipient that has never sent a transaction and holds no ETH, recipient is
|
||||||
a contract, burn address, and an Etherscan phishing/scam label. An address
|
a contract, burn address, and an Etherscan phishing/scam label. An address
|
||||||
that has received ETH but never sent a transaction still holds that ETH,
|
that has received ETH but never sent a transaction still holds that ETH,
|
||||||
so it gets no warning
|
so it gets no warning. The Etherscan label check is made only while "Check
|
||||||
|
recipients on Etherscan" is on. While it is off, the Etherscan box is left
|
||||||
|
out. A line in its place says "This address was not checked on Etherscan,
|
||||||
|
because "Check recipients on Etherscan" is off in Settings." The box or
|
||||||
|
the line is chosen when the screen is drawn, so neither moves anything
|
||||||
|
later
|
||||||
- Errors (an invalid amount), plus five reserved error boxes, so the balance
|
- Errors (an invalid amount), plus five reserved error boxes, so the balance
|
||||||
and the fee arriving later move nothing — the amount exceeds the balance
|
and the fee arriving later move nothing — the amount exceeds the balance
|
||||||
(for a token whose scale is unknown it says the token's balance is
|
(for a token whose scale is unknown it says the token's balance is
|
||||||
@@ -2041,7 +2137,11 @@ view would leave a wallet one click from deletion.
|
|||||||
AddressToken. The screen describes the transaction on the network it was sent
|
AddressToken. The screen describes the transaction on the network it was sent
|
||||||
on, whichever network is active: its block, network details, decoded details
|
on, whichever network is active: its block, network details, decoded details
|
||||||
and raw data are read from that network's Blockscout, the one Settings has for
|
and raw data are read from that network's Blockscout, the one Settings has for
|
||||||
it, and its etherscan links open that network's explorer.
|
it, and its etherscan links open that network's explorer. The block, network
|
||||||
|
details, decoded details and raw data are read only while "Use Blockscout" is
|
||||||
|
on. While it is off, a line in their place says "No block, decoded details,
|
||||||
|
network details or raw data are shown, because "Use Blockscout" is off in
|
||||||
|
Settings."
|
||||||
- **Elements** (grouped into logical blocks using light well containers; field
|
- **Elements** (grouped into logical blocks using light well containers; field
|
||||||
labels are self-explanatory so groups have no headings):
|
labels are self-explanatory so groups have no headings):
|
||||||
- "Transaction" heading, "Back" button
|
- "Transaction" heading, "Back" button
|
||||||
@@ -2124,8 +2224,21 @@ view would leave a wallet one click from deletion.
|
|||||||
already active changes nothing and sends nothing
|
already active changes nothing and sends nothing
|
||||||
- Ethereum RPC: endpoint URL input + "Save" button (validated against
|
- Ethereum RPC: endpoint URL input + "Save" button (validated against
|
||||||
`eth_chainId` before being saved)
|
`eth_chainId` before being saved)
|
||||||
- Blockscout API: endpoint URL input + "Save" button (validated against
|
- Blockscout API:
|
||||||
`/stats` before being saved)
|
- "Use Blockscout" checkbox, unticked until the user ticks it
|
||||||
|
- A sentence saying what it shows and what it sends. It sends the
|
||||||
|
Blockscout API the user's addresses and the hash of each transaction
|
||||||
|
opened
|
||||||
|
- Endpoint URL input + "Save" button. "Save" checks the URL by asking it
|
||||||
|
for `/stats` before saving it. That request carries no user data. It
|
||||||
|
is made whether or not "Use Blockscout" is ticked.
|
||||||
|
- Etherscan:
|
||||||
|
- "Check recipients on Etherscan" checkbox, unticked until the user
|
||||||
|
ticks it
|
||||||
|
- A sentence saying it sends each recipient's address to Etherscan to
|
||||||
|
check it for a phishing or scam label
|
||||||
|
- Each of the two settings applies to every network. Welcome offers the same
|
||||||
|
two checkboxes
|
||||||
- Token Spam Protection:
|
- Token Spam Protection:
|
||||||
- "Hide fake tokens impersonating a known symbol" checkbox
|
- "Hide fake tokens impersonating a known symbol" checkbox
|
||||||
- "Hide tokens with fewer than 1,000 holders" checkbox
|
- "Hide tokens with fewer than 1,000 holders" checkbox
|
||||||
@@ -2149,6 +2262,10 @@ view would leave a wallet one click from deletion.
|
|||||||
- Debug: hidden until revealed, then an "Enable debug mode" checkbox that
|
- Debug: hidden until revealed, then an "Enable debug mode" checkbox that
|
||||||
turns on the red banner and verbose logging
|
turns on the red banner and verbose logging
|
||||||
- **Transitions**:
|
- **Transitions**:
|
||||||
|
- Ticking or unticking "Use Blockscout" or "Check recipients on Etherscan" →
|
||||||
|
stores the setting (no screen change). The next balance refresh and every
|
||||||
|
screen drawn after that follow it. Unticking "Use Blockscout" also drops
|
||||||
|
every token the user has not tracked from every address
|
||||||
- "+ Add wallet" → **AddWallet**
|
- "+ Add wallet" → **AddWallet**
|
||||||
- "+ Add token" → **SettingsAddToken**
|
- "+ Add token" → **SettingsAddToken**
|
||||||
- `[recovery phrase]` on an HD wallet → **ShowRecoveryPhrase**
|
- `[recovery phrase]` on an HD wallet → **ShowRecoveryPhrase**
|
||||||
@@ -2167,7 +2284,8 @@ view would leave a wallet one click from deletion.
|
|||||||
nothing and tells the background nothing
|
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); after a network
|
- "Back" (or Settings gear again) → previous screen (Home); after a network
|
||||||
switch, the stack is cut first (see the top of Screen Map)
|
switch or a change to "Use Blockscout", the stack is cut first (see the
|
||||||
|
top of Screen Map)
|
||||||
|
|
||||||
#### ShowRecoveryPhrase (`show-phrase`)
|
#### ShowRecoveryPhrase (`show-phrase`)
|
||||||
|
|
||||||
@@ -2324,6 +2442,8 @@ view would leave a wallet one click from deletion.
|
|||||||
money. The predicate is `addressHoldsFunds()` in
|
money. The predicate is `addressHoldsFunds()` in
|
||||||
`src/popup/views/helpers.js`, unrounded and token-aware. A balance is a
|
`src/popup/views/helpers.js`, unrounded and token-aware. A balance is a
|
||||||
warning, never a refusal.
|
warning, never a refusal.
|
||||||
|
- While "Use Blockscout" is off, a line under the warning: "Only tracked
|
||||||
|
tokens are listed, because "Use Blockscout" is off in Settings."
|
||||||
- The rule that a wallet always keeps at least one address, and that
|
- The rule that a wallet always keeps at least one address, and that
|
||||||
removing the last one means deleting the wallet from Settings
|
removing the last one means deleting the wallet from Settings
|
||||||
- Error line
|
- Error line
|
||||||
@@ -2665,17 +2785,31 @@ view would leave a wallet one click from deletion.
|
|||||||
|
|
||||||
### External Services
|
### External Services
|
||||||
|
|
||||||
AutistMask is not a fully self-contained offline tool. It necessarily
|
AutistMask is not a fully self-contained offline tool. By default it sends the
|
||||||
communicates with three external services to function as a wallet:
|
user's data only to the Ethereum RPC endpoint. It also asks the CoinDesk price
|
||||||
|
API for prices, sending token symbols and nothing else.
|
||||||
|
|
||||||
|
Two more services are sent none of the user's data until the user turns on their
|
||||||
|
settings:
|
||||||
|
|
||||||
|
- Blockscout, with "Use Blockscout"
|
||||||
|
- Etherscan, with "Check recipients on Etherscan"
|
||||||
|
|
||||||
|
Both settings are on the Welcome screen and in Settings. Both are off in a new
|
||||||
|
install. Each setting applies to every network.
|
||||||
|
|
||||||
|
Contacted by default:
|
||||||
|
|
||||||
- **Ethereum JSON-RPC endpoint**: The extension needs an Ethereum node to query
|
- **Ethereum JSON-RPC endpoint**: The extension needs an Ethereum node to query
|
||||||
balances (`eth_getBalance`), read ERC-20 token contracts (`eth_call`), among
|
balances (`eth_getBalance`), read ERC-20 token contracts (`eth_call`), among
|
||||||
them the balance of every tracked token at every address (`balanceOf`),
|
them the balance of every tracked token at every address (`balanceOf`),
|
||||||
estimate gas (`eth_estimateGas`), fetch nonces (`eth_getTransactionCount`),
|
estimate gas (`eth_estimateGas`), fetch nonces (`eth_getTransactionCount`),
|
||||||
broadcast transactions (`eth_sendRawTransaction`), and check transaction
|
broadcast transactions (`eth_sendRawTransaction`), and check transaction
|
||||||
receipts. The default endpoint is a public RPC (configurable by the user to
|
receipts. Every ENS lookup is made through it alone. A name whose resolver
|
||||||
any endpoint they prefer, including a local node). By default the extension
|
says it is resolved off-chain therefore does not resolve. The default endpoint
|
||||||
talks to `https://ethereum-rpc.publicnode.com`.
|
is a public RPC (configurable by the user to any endpoint they prefer,
|
||||||
|
including a local node). By default the extension talks to
|
||||||
|
`https://ethereum-rpc.publicnode.com`.
|
||||||
- **Data sent**: Ethereum addresses, transaction data, contract call
|
- **Data sent**: Ethereum addresses, transaction data, contract call
|
||||||
parameters. The RPC endpoint can see all on-chain queries and submitted
|
parameters. The RPC endpoint can see all on-chain queries and submitted
|
||||||
transactions.
|
transactions.
|
||||||
@@ -2687,13 +2821,38 @@ communicates with three external services to function as a wallet:
|
|||||||
- **Data sent**: Token symbol strings only (e.g. "ETH", "USDC"). No
|
- **Data sent**: Token symbol strings only (e.g. "ETH", "USDC"). No
|
||||||
addresses or user-specific data.
|
addresses or user-specific data.
|
||||||
|
|
||||||
- **Blockscout block-explorer API**: Used to fetch transaction history (normal
|
Sent the user's data only while the user has turned the setting on:
|
||||||
transactions and ERC-20 token transfers), ERC-20 token balances, and token
|
|
||||||
holder counts (for spam filtering). The default endpoint is
|
- **Blockscout block-explorer API** ("Use Blockscout"): The default endpoint is
|
||||||
`https://eth.blockscout.com/api/v2` (configurable by the user in Settings).
|
`https://eth.blockscout.com/api/v2` (configurable by the user in Settings).
|
||||||
- **Data sent**: Ethereum addresses. Blockscout receives the user's
|
- **Used for**:
|
||||||
addresses to query their transaction history and token balances. No
|
- transaction history (normal transactions and ERC-20 token transfers),
|
||||||
private keys, passwords, or signing operations are sent.
|
for Home, AddressDetail and AddressToken
|
||||||
|
- the balances of the ERC-20 tokens the user has not tracked, for Home,
|
||||||
|
AddressDetail, Send and DeleteAddress
|
||||||
|
- token holder counts, for spam filtering
|
||||||
|
- a transaction's block, network details and calldata, for
|
||||||
|
TransactionDetail
|
||||||
|
- **While the setting is off**: each of those screens shows a line saying
|
||||||
|
what it leaves out and that "Use Blockscout" is off. Only tracked tokens
|
||||||
|
are listed.
|
||||||
|
- **Saving the URL**: pressing "Save" beside the URL in Settings asks that
|
||||||
|
URL for `/stats`. This request carries no user data. It is made whether or
|
||||||
|
not the setting is on.
|
||||||
|
- **Data sent**: Ethereum addresses, and the hash of each transaction the
|
||||||
|
user opens. Blockscout receives the user's addresses to query their
|
||||||
|
transaction history and token balances. No private keys, passwords, or
|
||||||
|
signing operations are sent.
|
||||||
|
|
||||||
|
- **Etherscan address labels** ("Check recipients on Etherscan"): When
|
||||||
|
confirming a transaction, the extension performs a best-effort lookup of the
|
||||||
|
recipient address on Etherscan to check for phishing/scam labels. This is a
|
||||||
|
direct page fetch with no API key; the user's browser makes the request. While
|
||||||
|
the setting is off, ConfirmTx says that no Etherscan check was made.
|
||||||
|
- **Data sent**: the recipient's address and nothing else.
|
||||||
|
|
||||||
|
The etherscan links on the screens open a page in the browser when the user
|
||||||
|
clicks one. They are not requests the wallet makes.
|
||||||
|
|
||||||
What the extension does NOT do:
|
What the extension does NOT do:
|
||||||
|
|
||||||
@@ -2702,16 +2861,9 @@ What the extension does NOT do:
|
|||||||
- No Infura/Alchemy dependency (any JSON-RPC endpoint works)
|
- No Infura/Alchemy dependency (any JSON-RPC endpoint works)
|
||||||
- No backend servers operated by the developer
|
- No backend servers operated by the developer
|
||||||
|
|
||||||
In addition to the three user-configurable services above (RPC endpoint,
|
Users who want maximum privacy can point the RPC URL at their own node. With
|
||||||
CoinDesk price API, and Blockscout API), AutistMask also contacts:
|
"Use Blockscout" on, they can point the Blockscout URL at their own instance
|
||||||
|
too.
|
||||||
- **Etherscan address labels**: When confirming a transaction, the extension
|
|
||||||
performs a best-effort lookup of the recipient address on Etherscan to check
|
|
||||||
for phishing/scam labels. This is a direct page fetch with no API key; the
|
|
||||||
user's browser makes the request.
|
|
||||||
|
|
||||||
Users who want maximum privacy can point the RPC and Blockscout URLs at their
|
|
||||||
own self-hosted instances (price fetching can be disabled in a future version).
|
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
@@ -2893,8 +3045,11 @@ logged.
|
|||||||
- Add multiple addresses within an HD wallet
|
- Add multiple addresses within an HD wallet
|
||||||
- Manage multiple wallets simultaneously
|
- Manage multiple wallets simultaneously
|
||||||
- View ETH balance per address
|
- View ETH balance per address
|
||||||
- View ERC-20 token balances (tokens on the bundled known-token list, tokens
|
- View ERC-20 token balances:
|
||||||
with 1,000 or more holders, and tokens the user adds by contract address)
|
- tokens the user adds by contract address
|
||||||
|
- while "Use Blockscout" is on, tokens on the bundled known-token list and
|
||||||
|
tokens with 1,000 or more holders
|
||||||
|
- View transaction history and transaction details, while "Use Blockscout" is on
|
||||||
- Send ETH to an address
|
- Send ETH to an address
|
||||||
- Send ERC-20 tokens to an address
|
- Send ERC-20 tokens to an address
|
||||||
- Receive ETH/tokens (display address, copy to clipboard, QR code)
|
- Receive ETH/tokens (display address, copy to clipboard, QR code)
|
||||||
|
|||||||
+58
-25
@@ -57,11 +57,12 @@ analytics, use a portfolio tracker. The wallet is not the place for any of that.
|
|||||||
|
|
||||||
- **No NFT galleries or portfolio views.** This is a wallet, not a dashboard.
|
- **No NFT galleries or portfolio views.** This is a wallet, not a dashboard.
|
||||||
|
|
||||||
- **No third-party token list APIs.** Token balances come from the same block
|
- **No third-party token list APIs.** The balances of the tokens you track are
|
||||||
explorer you configure for transaction history, and the extension ships its
|
read from your RPC endpoint. While "Use Blockscout" is on, the other tokens
|
||||||
own hardcoded list of top ERC-20 contract addresses for symbol-spoofing
|
come from the same block explorer you configure for transaction history. The
|
||||||
detection. Any token you want tracked across all your addresses, you add
|
extension ships its own hardcoded list of top ERC-20 contract addresses for
|
||||||
yourself by contract address.
|
symbol-spoofing detection. Any token you want tracked across all your
|
||||||
|
addresses, you add yourself by contract address.
|
||||||
|
|
||||||
- **No backend servers operated by the developer.** Nothing is sent to any
|
- **No backend servers operated by the developer.** Nothing is sent to any
|
||||||
server run by AutistMask. Every network destination is listed below.
|
server run by AutistMask. Every network destination is listed below.
|
||||||
@@ -69,8 +70,13 @@ analytics, use a portfolio tracker. The wallet is not the place for any of that.
|
|||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
AutistMask is a browser extension that runs entirely in your browser. It does
|
AutistMask is a browser extension that runs entirely in your browser. It does
|
||||||
not have a backend server. It communicates with five external destinations:
|
not have a backend server. Out of the box it sends your data only to the
|
||||||
three you configure yourself, and two fixed ones used for scam detection.
|
Ethereum RPC endpoint. It also asks the CoinDesk price API for prices, sending
|
||||||
|
token symbols and nothing else.
|
||||||
|
|
||||||
|
Blockscout and Etherscan are sent none of your data until you turn them on.
|
||||||
|
Their settings are "Use Blockscout" and "Check recipients on Etherscan". Both
|
||||||
|
are on the Welcome screen and in Settings. Both are off in a new install.
|
||||||
|
|
||||||
### External Services
|
### External Services
|
||||||
|
|
||||||
@@ -96,16 +102,31 @@ addresses are necessarily visible to the RPC provider when querying balances.
|
|||||||
**Blockscout API** (default: `eth.blockscout.com/api/v2`;
|
**Blockscout API** (default: `eth.blockscout.com/api/v2`;
|
||||||
`eth-sepolia.blockscout.com/api/v2` on Sepolia)
|
`eth-sepolia.blockscout.com/api/v2` on Sepolia)
|
||||||
|
|
||||||
Used to fetch token balances and transaction history. Blockscout is an
|
Blockscout is an open-source blockchain explorer. AutistMask uses it only while
|
||||||
open-source blockchain explorer. AutistMask queries it for your ERC-20 token
|
"Use Blockscout" is on. It then asks Blockscout for:
|
||||||
balances (including the holder counts used for spam filtering) and your recent
|
|
||||||
transactions and token transfers. You can change this in Settings to a
|
|
||||||
self-hosted Blockscout instance.
|
|
||||||
|
|
||||||
When it is contacted: on every balance refresh, and whenever a screen showing
|
- your recent transactions and token transfers
|
||||||
transaction history is opened.
|
- the balances of the tokens you have not tracked, with the holder counts used
|
||||||
|
for spam filtering
|
||||||
|
- the block, fees and calldata of a transaction you open
|
||||||
|
|
||||||
What gets sent: your Ethereum addresses (to look up balances and transactions).
|
You can change this in Settings to a self-hosted Blockscout instance. While the
|
||||||
|
setting is off, each screen that would show Blockscout's data says so instead.
|
||||||
|
Only the tokens you track are listed. When you turn the setting off, every other
|
||||||
|
token leaves the lists at once.
|
||||||
|
|
||||||
|
When it is contacted, while "Use Blockscout" is on:
|
||||||
|
|
||||||
|
- on every balance refresh
|
||||||
|
- whenever a screen showing transaction history is opened
|
||||||
|
- when you open a transaction
|
||||||
|
|
||||||
|
Pressing "Save" beside the Blockscout URL in Settings also contacts it, even
|
||||||
|
with the setting off. That request checks the URL by asking it for `/stats`. It
|
||||||
|
sends none of your data.
|
||||||
|
|
||||||
|
What gets sent: your Ethereum addresses (to look up balances and transactions)
|
||||||
|
and the hash of each transaction you open.
|
||||||
|
|
||||||
**CoinDesk CADLI price API** (`data-api.coindesk.com`)
|
**CoinDesk CADLI price API** (`data-api.coindesk.com`)
|
||||||
|
|
||||||
@@ -123,15 +144,19 @@ address.
|
|||||||
**Etherscan address labels** (`etherscan.io`; `sepolia.etherscan.io` on Sepolia)
|
**Etherscan address labels** (`etherscan.io`; `sepolia.etherscan.io` on Sepolia)
|
||||||
|
|
||||||
When you review a send, AutistMask fetches the recipient's public Etherscan
|
When you review a send, AutistMask fetches the recipient's public Etherscan
|
||||||
address page and looks for a "Fake_Phishing"/"Phish/Hack" label or a scam
|
address page. It looks for a "Fake_Phishing"/"Phish/Hack" label or a scam
|
||||||
warning, and shows a red warning if it finds one. This is a plain page fetch
|
warning, and shows a red warning if it finds one. This is a plain page fetch
|
||||||
with no API key, made by your browser. It is best-effort: if it fails, it is
|
with no API key, made by your browser. It is best-effort: if it fails, it is
|
||||||
silently ignored. This endpoint is not user-configurable.
|
silently ignored. This endpoint is not user-configurable.
|
||||||
|
|
||||||
When it is contacted: each time you reach the send confirmation screen.
|
The check is off until you turn on "Check recipients on Etherscan". While the
|
||||||
|
setting is off, the confirmation screen says that no Etherscan check was made.
|
||||||
|
|
||||||
What gets sent: the recipient address you are about to send to, and your IP
|
When it is contacted: only while "Check recipients on Etherscan" is on, each
|
||||||
address. Your own addresses are not sent.
|
time you reach the send confirmation screen.
|
||||||
|
|
||||||
|
What gets sent: the recipient's address and nothing else. As with any request,
|
||||||
|
Etherscan sees your IP address.
|
||||||
|
|
||||||
Etherscan links shown elsewhere in the UI (on addresses, transactions, and token
|
Etherscan links shown elsewhere in the UI (on addresses, transactions, and token
|
||||||
contracts) are ordinary links. They contact nothing until you click them.
|
contracts) are ordinary links. They contact nothing until you click them.
|
||||||
@@ -185,6 +210,9 @@ password.
|
|||||||
|
|
||||||
1. Click the AutistMask icon in your browser toolbar.
|
1. Click the AutistMask icon in your browser toolbar.
|
||||||
2. Click "Add wallet" (on first use), or open Settings and click "+ Add wallet".
|
2. Click "Add wallet" (on first use), or open Settings and click "+ Add wallet".
|
||||||
|
On first use, the screen with "Add wallet" also offers "Use Blockscout" and
|
||||||
|
"Check recipients on Etherscan", both off. Tick either if you want what it
|
||||||
|
does (see External Services). You can change both later in Settings.
|
||||||
3. On the "From Phrase" tab, click the die button to generate a random 12-word
|
3. On the "From Phrase" tab, click the die button to generate a random 12-word
|
||||||
recovery phrase.
|
recovery phrase.
|
||||||
4. **Write down the recovery phrase and store it safely.** Anyone with these
|
4. **Write down the recovery phrase and store it safely.** Anyone with these
|
||||||
@@ -220,10 +248,14 @@ always produce the same sequence of addresses.
|
|||||||
|
|
||||||
### Adding ERC-20 Tokens
|
### Adding ERC-20 Tokens
|
||||||
|
|
||||||
Tokens you hold show up automatically only if they are in the extension's
|
Tokens you hold show up automatically only while "Use Blockscout" is on. Even
|
||||||
bundled list of well-known tokens or have at least 1,000 holders; everything
|
then, a token shows up only if it is in the extension's bundled list of
|
||||||
else is treated as spam and hidden. To track a token explicitly (which also
|
well-known tokens or has at least 1,000 holders. Everything else is treated as
|
||||||
shows it at zero balance), add it by contract address:
|
spam and hidden.
|
||||||
|
|
||||||
|
A token you track is shown whether or not "Use Blockscout" is on. By default it
|
||||||
|
is shown at zero balance too (see Display in Settings). To track a token, add it
|
||||||
|
by contract address:
|
||||||
|
|
||||||
1. Go to an address detail view (click `[info]` on any address).
|
1. Go to an address detail view (click `[info]` on any address).
|
||||||
2. Click "+ Token".
|
2. Click "+ Token".
|
||||||
@@ -389,8 +421,9 @@ Click the gear icon on the home screen to access settings:
|
|||||||
that network's defaults if you have not used it before.
|
that network's defaults if you have not used it before.
|
||||||
- **Ethereum RPC**: Change the Ethereum node endpoint. Default is a public RPC.
|
- **Ethereum RPC**: Change the Ethereum node endpoint. Default is a public RPC.
|
||||||
You can use your own node for maximum privacy.
|
You can use your own node for maximum privacy.
|
||||||
- **Blockscout API**: Change the Blockscout instance used for token balances and
|
- **Blockscout API**: the "Use Blockscout" checkbox, off until you tick it, and
|
||||||
transaction history. You can use a self-hosted instance.
|
the URL of the Blockscout instance. You can use a self-hosted instance.
|
||||||
|
- **Etherscan**: "Check recipients on Etherscan", off until you tick it.
|
||||||
- **Token Spam Protection**: Toggle individual scam filters and set the dust
|
- **Token Spam Protection**: Toggle individual scam filters and set the dust
|
||||||
transaction threshold.
|
transaction threshold.
|
||||||
- **Allowed Sites / Denied Sites**: View and manage web3 site permissions.
|
- **Allowed Sites / Denied Sites**: View and manage web3 site permissions.
|
||||||
|
|||||||
+13
-1
@@ -17,7 +17,11 @@ const { applyChainSwitchFields } = require("../shared/chainSwitchFields");
|
|||||||
// the same prohibition reported early, not the guarantee.
|
// the same prohibition reported early, not the guarantee.
|
||||||
const { getState, updateState } = require("./state");
|
const { getState, updateState } = require("./state");
|
||||||
const { StateUnusableError } = require("../shared/stateSchema");
|
const { StateUnusableError } = require("../shared/stateSchema");
|
||||||
const { refreshBalances, getProvider } = require("../shared/balances");
|
const {
|
||||||
|
refreshBalances,
|
||||||
|
dropUntrackedTokens,
|
||||||
|
getProvider,
|
||||||
|
} = require("../shared/balances");
|
||||||
const { debugFetch, log } = require("../shared/log");
|
const { debugFetch, log } = require("../shared/log");
|
||||||
const {
|
const {
|
||||||
verifySignedTx,
|
verifySignedTx,
|
||||||
@@ -1386,6 +1390,10 @@ const RECENT_BALANCE_REFRESH_MS = Math.floor(BALANCE_REFRESH_PERIOD_MS / 2);
|
|||||||
// back over them (https://git.eeqj.de/sneak/AutistMask/issues/577).
|
// back over them (https://git.eeqj.de/sneak/AutistMask/issues/577).
|
||||||
// lastBalanceRefresh is left as the switch set it, so the next tick refreshes
|
// lastBalanceRefresh is left as the switch set it, so the next tick refreshes
|
||||||
// the new network.
|
// the new network.
|
||||||
|
//
|
||||||
|
// The refresh asks Blockscout only if "Use Blockscout" is on when it starts.
|
||||||
|
// The user may turn it off while the refresh runs. So when "Use Blockscout"
|
||||||
|
// stored NOW is off, the save drops every token the user has not tracked.
|
||||||
async function backgroundRefresh() {
|
async function backgroundRefresh() {
|
||||||
const s = await getState();
|
const s = await getState();
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -1397,6 +1405,7 @@ async function backgroundRefresh() {
|
|||||||
wallets,
|
wallets,
|
||||||
s.rpcUrl,
|
s.rpcUrl,
|
||||||
s.blockscoutUrl,
|
s.blockscoutUrl,
|
||||||
|
s.useBlockscout,
|
||||||
s.trackedTokens,
|
s.trackedTokens,
|
||||||
s.networkId,
|
s.networkId,
|
||||||
);
|
);
|
||||||
@@ -1423,6 +1432,9 @@ async function backgroundRefresh() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!fresh.useBlockscout) {
|
||||||
|
dropUntrackedTokens(fresh.wallets, fresh.trackedTokens);
|
||||||
|
}
|
||||||
fresh.lastBalanceRefresh = now;
|
fresh.lastBalanceRefresh = now;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+113
-7
@@ -43,6 +43,38 @@
|
|||||||
<!-- ============ WELCOME / FIRST USE ============ -->
|
<!-- ============ WELCOME / FIRST USE ============ -->
|
||||||
<div id="view-welcome" class="view hidden">
|
<div id="view-welcome" class="view hidden">
|
||||||
<p class="mb-3">Welcome! To get started, add a wallet.</p>
|
<p class="mb-3">Welcome! To get started, add a wallet.</p>
|
||||||
|
<div class="bg-well p-3 mx-1 mb-3">
|
||||||
|
<p class="text-xs mb-2">
|
||||||
|
This wallet sends your data only to the Ethereum RPC
|
||||||
|
endpoint, unless you turn on one of these. You can
|
||||||
|
change them later in Settings.
|
||||||
|
</p>
|
||||||
|
<label
|
||||||
|
class="text-xs flex items-center gap-1 cursor-pointer mb-1"
|
||||||
|
>
|
||||||
|
<input type="checkbox" id="welcome-use-blockscout" />
|
||||||
|
Use Blockscout
|
||||||
|
</label>
|
||||||
|
<p class="text-xs text-muted mb-2">
|
||||||
|
Shows transaction history, tokens you have not tracked
|
||||||
|
and transaction details. Sends your addresses, and the
|
||||||
|
hash of each transaction you open, to the Blockscout
|
||||||
|
API.
|
||||||
|
</p>
|
||||||
|
<label
|
||||||
|
class="text-xs flex items-center gap-1 cursor-pointer mb-1"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="welcome-check-recipients-on-etherscan"
|
||||||
|
/>
|
||||||
|
Check recipients on Etherscan
|
||||||
|
</label>
|
||||||
|
<p class="text-xs text-muted">
|
||||||
|
Sends each recipient's address to Etherscan to check it
|
||||||
|
for a phishing or scam label.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
id="btn-welcome-add"
|
id="btn-welcome-add"
|
||||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||||
@@ -272,6 +304,10 @@
|
|||||||
|
|
||||||
<!-- wallet list -->
|
<!-- wallet list -->
|
||||||
<div id="wallet-list"></div>
|
<div id="wallet-list"></div>
|
||||||
|
<p id="home-tokens-note" class="text-xs text-muted py-1 hidden">
|
||||||
|
Only tracked tokens are listed, because "Use Blockscout" is
|
||||||
|
off in Settings.
|
||||||
|
</p>
|
||||||
|
|
||||||
<!-- recent transactions across all addresses -->
|
<!-- recent transactions across all addresses -->
|
||||||
<div>
|
<div>
|
||||||
@@ -336,6 +372,13 @@
|
|||||||
<div id="address-balances" class="min-h-[1.25rem]">
|
<div id="address-balances" class="min-h-[1.25rem]">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<p
|
||||||
|
id="address-tokens-note"
|
||||||
|
class="text-xs text-muted hidden"
|
||||||
|
>
|
||||||
|
Only tracked tokens are listed, because "Use Blockscout"
|
||||||
|
is off in Settings.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- actions -->
|
<!-- actions -->
|
||||||
@@ -526,12 +569,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="block mb-1">What to send</label>
|
<label class="block mb-1">What to send</label>
|
||||||
<select
|
<!-- The token dropdown and the line under it. Both are
|
||||||
id="send-token"
|
hidden when AddressToken opens Send for its own token. -->
|
||||||
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
|
<div id="send-token-dropdown">
|
||||||
>
|
<select
|
||||||
<option value="ETH">ETH</option>
|
id="send-token"
|
||||||
</select>
|
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
|
||||||
|
>
|
||||||
|
<option value="ETH">ETH</option>
|
||||||
|
</select>
|
||||||
|
<p
|
||||||
|
id="send-tokens-note"
|
||||||
|
class="text-xs text-muted hidden"
|
||||||
|
>
|
||||||
|
Only tracked tokens are listed, because "Use
|
||||||
|
Blockscout" is off in Settings.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div id="send-token-static" class="text-sm hidden"></div>
|
<div id="send-token-static" class="text-sm hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
@@ -660,6 +714,13 @@
|
|||||||
phishing/scam. Do not send funds to this address.
|
phishing/scam. Do not send funds to this address.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p
|
||||||
|
id="confirm-etherscan-note"
|
||||||
|
class="mb-2 text-xs text-muted hidden"
|
||||||
|
>
|
||||||
|
This address was not checked on Etherscan, because "Check
|
||||||
|
recipients on Etherscan" is off in Settings.
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
id="confirm-errors"
|
id="confirm-errors"
|
||||||
class="mb-2 border border-border border-dashed p-2 invisible min-h-[1.25rem]"
|
class="mb-2 border border-border border-dashed p-2 invisible min-h-[1.25rem]"
|
||||||
@@ -1002,8 +1063,19 @@
|
|||||||
|
|
||||||
<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">Blockscout API</h3>
|
<h3 class="font-bold mb-1">Blockscout API</h3>
|
||||||
|
<label
|
||||||
|
class="text-xs flex items-center gap-1 cursor-pointer mb-1"
|
||||||
|
>
|
||||||
|
<input type="checkbox" id="settings-use-blockscout" />
|
||||||
|
Use Blockscout
|
||||||
|
</label>
|
||||||
|
<p class="text-xs text-muted mb-2">
|
||||||
|
Shows transaction history, tokens you have not tracked
|
||||||
|
and transaction details. Sends your addresses, and the
|
||||||
|
hash of each transaction you open, to the Blockscout API
|
||||||
|
below.
|
||||||
|
</p>
|
||||||
<p class="text-xs text-muted mb-1">
|
<p class="text-xs text-muted mb-1">
|
||||||
Used to fetch token balances and transaction history.
|
|
||||||
Change this if you run your own Blockscout instance.
|
Change this if you run your own Blockscout instance.
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
@@ -1019,6 +1091,23 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-well p-3 mx-1 mb-3">
|
||||||
|
<h3 class="font-bold mb-1">Etherscan</h3>
|
||||||
|
<label
|
||||||
|
class="text-xs flex items-center gap-1 cursor-pointer mb-1"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="settings-check-recipients-on-etherscan"
|
||||||
|
/>
|
||||||
|
Check recipients on Etherscan
|
||||||
|
</label>
|
||||||
|
<p class="text-xs text-muted">
|
||||||
|
Sends each recipient's address to Etherscan to check it
|
||||||
|
for a phishing or scam label.
|
||||||
|
</p>
|
||||||
|
</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">Token Spam Protection</h3>
|
<h3 class="font-bold mb-1">Token Spam Protection</h3>
|
||||||
<p class="text-xs text-muted mb-2">
|
<p class="text-xs text-muted mb-2">
|
||||||
@@ -1291,6 +1380,13 @@
|
|||||||
>
|
>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<p
|
||||||
|
id="delete-address-tokens-note"
|
||||||
|
class="text-xs text-muted mb-2 hidden"
|
||||||
|
>
|
||||||
|
Only tracked tokens are listed, because "Use Blockscout" is
|
||||||
|
off in Settings.
|
||||||
|
</p>
|
||||||
<p class="text-xs text-muted mb-3">
|
<p class="text-xs text-muted mb-3">
|
||||||
A wallet always keeps at least one address. To remove the
|
A wallet always keeps at least one address. To remove the
|
||||||
last one, delete the whole wallet from Settings instead.
|
last one, delete the whole wallet from Settings instead.
|
||||||
@@ -1504,6 +1600,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Shown in place of the block, decoded details, network
|
||||||
|
details and raw data, which only Blockscout gives. -->
|
||||||
|
<p
|
||||||
|
id="tx-detail-blockscout-note"
|
||||||
|
class="text-xs text-muted mx-1 mb-3 hidden"
|
||||||
|
>
|
||||||
|
No block, decoded details, network details or raw data are
|
||||||
|
shown, because "Use Blockscout" is off in Settings.
|
||||||
|
</p>
|
||||||
|
|
||||||
<!-- ── Decoded details ── -->
|
<!-- ── Decoded details ── -->
|
||||||
<div id="tx-detail-calldata-section" class="hidden">
|
<div id="tx-detail-calldata-section" class="hidden">
|
||||||
<div class="bg-well p-3 mx-1 mb-3">
|
<div class="bg-well p-3 mx-1 mb-3">
|
||||||
|
|||||||
+55
-18
@@ -11,7 +11,7 @@ const {
|
|||||||
const { StateUnusableError } = require("../shared/stateSchema");
|
const { StateUnusableError } = require("../shared/stateSchema");
|
||||||
const { log, setRuntimeDebug } = require("../shared/log");
|
const { log, setRuntimeDebug } = require("../shared/log");
|
||||||
const { refreshPrices } = require("../shared/prices");
|
const { refreshPrices } = require("../shared/prices");
|
||||||
const { refreshBalances } = require("../shared/balances");
|
const { refreshBalances, dropUntrackedTokens } = require("../shared/balances");
|
||||||
const { clearPopupCaches } = require("../shared/chainSwitch");
|
const { clearPopupCaches } = require("../shared/chainSwitch");
|
||||||
const {
|
const {
|
||||||
$,
|
$,
|
||||||
@@ -71,21 +71,27 @@ let refreshInFlight = false;
|
|||||||
// the recovery screen: there is no profile left to refresh.
|
// the recovery screen: there is no profile left to refresh.
|
||||||
let refreshTimer = null;
|
let refreshTimer = null;
|
||||||
|
|
||||||
|
// The refresh first takes any change to the network or to "Use Blockscout"
|
||||||
|
// saved elsewhere (followStoredSettings() below). So it asks Blockscout only if
|
||||||
|
// the stored setting is on when it starts.
|
||||||
|
//
|
||||||
// The refresh writes into a copy of the wallets. Inside the save, under the
|
// The refresh writes into a copy of the wallets. Inside the save, under the
|
||||||
// lock, its results are copied onto the address records in `state` only if the
|
// lock, its results are copied onto the address records in `state` only if the
|
||||||
// network it ran on is still the active one, on this page and in the stored
|
// network it ran on is still the active one, on this page and in the stored
|
||||||
// record; the background's switch, after the user approves a site's request,
|
// record; the background's switch, after the user approves a site's request,
|
||||||
// reaches this page only after the stored record (followNetworkSwitch() below).
|
// reaches this page only after the stored record (followStoredSettings()).
|
||||||
// A switch while the refresh ran cleared every address's balances
|
// A switch while the refresh ran cleared every address's balances
|
||||||
// (applyChainSwitchFields() in src/shared/chainSwitchFields.js), and the
|
// (applyChainSwitchFields() in src/shared/chainSwitchFields.js), and the
|
||||||
// previous network's must not come back over them
|
// previous network's must not come back over them
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/577). A dropped refresh is not
|
// (https://git.eeqj.de/sneak/AutistMask/issues/577). A dropped refresh is not
|
||||||
// recorded as done. Results are copied by address, so an address added or
|
// recorded as done. Results are copied by address, so an address added or
|
||||||
// removed meanwhile is left as it is.
|
// removed meanwhile is left as it is. If the stored "Use Blockscout" is off
|
||||||
|
// when the refresh saves, the save drops every token the user has not tracked.
|
||||||
async function doRefreshAndRender() {
|
async function doRefreshAndRender() {
|
||||||
if (refreshInFlight) return;
|
if (refreshInFlight) return;
|
||||||
refreshInFlight = true;
|
refreshInFlight = true;
|
||||||
try {
|
try {
|
||||||
|
await followStoredSettings();
|
||||||
const networkId = state.networkId;
|
const networkId = state.networkId;
|
||||||
const wallets = structuredClone(state.wallets);
|
const wallets = structuredClone(state.wallets);
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -94,6 +100,7 @@ async function doRefreshAndRender() {
|
|||||||
wallets,
|
wallets,
|
||||||
state.rpcUrl,
|
state.rpcUrl,
|
||||||
state.blockscoutUrl,
|
state.blockscoutUrl,
|
||||||
|
state.useBlockscout,
|
||||||
state.trackedTokens,
|
state.trackedTokens,
|
||||||
networkId,
|
networkId,
|
||||||
pageClosed.signal,
|
pageClosed.signal,
|
||||||
@@ -117,6 +124,9 @@ async function doRefreshAndRender() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!stored.useBlockscout) {
|
||||||
|
dropUntrackedTokens(state.wallets, stored.trackedTokens);
|
||||||
|
}
|
||||||
state.lastBalanceRefresh = Date.now();
|
state.lastBalanceRefresh = Date.now();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@@ -228,17 +238,36 @@ function fallbackView() {
|
|||||||
// src/shared/chainSwitch.js), so a record naming a network other than this
|
// src/shared/chainSwitch.js), so a record naming a network other than this
|
||||||
// page's always holds a switch saved elsewhere, and a switch the user picks
|
// page's always holds a switch saved elsewhere, and a switch the user picks
|
||||||
// here while this waits for the lock is made after it.
|
// here while this waits for the lock is made after it.
|
||||||
async function followNetworkSwitch() {
|
//
|
||||||
let switched = false;
|
// Another page may change "Use Blockscout" or "Check recipients on Etherscan".
|
||||||
|
// This page takes either change the same way and draws its screens again. Its
|
||||||
|
// own changes to them are made under the lock too (views/settings.js,
|
||||||
|
// views/welcome.js).
|
||||||
|
async function followStoredSettings() {
|
||||||
|
let changed = false;
|
||||||
// A failed save is reported by saveState() itself: see onSaveFailure().
|
// A failed save is reported by saveState() itself: see onSaveFailure().
|
||||||
await saveState((stored) => {
|
await saveState((stored) => {
|
||||||
if (stored.networkId === state.networkId) return false;
|
if (stored.networkId !== state.networkId) {
|
||||||
settings.cutViewStack();
|
settings.cutViewStack();
|
||||||
takeStoredNetwork(stored);
|
takeStoredNetwork(stored);
|
||||||
clearPopupCaches();
|
clearPopupCaches();
|
||||||
switched = true;
|
changed = true;
|
||||||
|
}
|
||||||
|
if (stored.useBlockscout !== state.useBlockscout) {
|
||||||
|
settings.setUseBlockscout(stored.useBlockscout);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
stored.checkRecipientsOnEtherscan !==
|
||||||
|
state.checkRecipientsOnEtherscan
|
||||||
|
) {
|
||||||
|
state.checkRecipientsOnEtherscan =
|
||||||
|
stored.checkRecipientsOnEtherscan;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (!changed) return false;
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
if (switched) settings.showNewNetwork();
|
if (changed) settings.showNewSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The background opens the approval window as wide as the page, but the
|
// The background opens the approval window as wide as the page, but the
|
||||||
@@ -385,19 +414,27 @@ async function init() {
|
|||||||
settingsAddToken.init(ctx);
|
settingsAddToken.init(ctx);
|
||||||
deleteAddress.init(ctx);
|
deleteAddress.init(ctx);
|
||||||
|
|
||||||
// Only a record naming a network other than this page's is followed: the
|
// Only a record whose network, "Use Blockscout" or "Check recipients on
|
||||||
// one this page's own switch in Settings saves names the network it
|
// Etherscan" differs from this page's is followed. A record this page's
|
||||||
// already has, and a record naming none is no switch.
|
// own save wrote matches this page. A record naming no network is no
|
||||||
|
// switch.
|
||||||
onStorageChanged((changes) => {
|
onStorageChanged((changes) => {
|
||||||
const written = changes.autistmask && changes.autistmask.newValue;
|
const written = changes.autistmask && changes.autistmask.newValue;
|
||||||
if (!written || !written.networkId) return;
|
if (!written || !written.networkId) return;
|
||||||
if (written.networkId !== state.networkId) followNetworkSwitch();
|
if (
|
||||||
|
written.networkId !== state.networkId ||
|
||||||
|
written.useBlockscout !== state.useBlockscout ||
|
||||||
|
written.checkRecipientsOnEtherscan !==
|
||||||
|
state.checkRecipientsOnEtherscan
|
||||||
|
) {
|
||||||
|
followStoredSettings();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// A switch saved since loadState() above, before there was a listener.
|
// A change saved since loadState() above, before there was a listener.
|
||||||
followNetworkSwitch();
|
followStoredSettings();
|
||||||
|
|
||||||
if (!state.hasWallet) {
|
if (!state.hasWallet) {
|
||||||
showView("welcome");
|
welcome.show();
|
||||||
} else {
|
} else {
|
||||||
renderWalletList();
|
renderWalletList();
|
||||||
restoreView();
|
restoreView();
|
||||||
|
|||||||
@@ -38,10 +38,15 @@ function markViewRendered(view) {
|
|||||||
if (view) renderedViews.add(view);
|
if (view) renderedViews.add(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Begin a fresh page-load scope. The popup gets one by being loaded, and
|
// Begin a fresh page-load scope. The popup gets one by being loaded. It also
|
||||||
// asks for one after a network switch, because every screen then on the page
|
// asks for one when every screen on the page is out of date:
|
||||||
// shows the previous network (showNewNetwork() in views/settings.js); the
|
//
|
||||||
// unit tests, which simulate several page loads against one module
|
// - after a network switch, since those screens show the previous network
|
||||||
|
// (showNewSettings() in views/settings.js)
|
||||||
|
// - after "Use Blockscout" or "Check recipients on Etherscan" changes, here
|
||||||
|
// or on another page, since those screens were drawn under the old setting
|
||||||
|
//
|
||||||
|
// The unit tests, which simulate several page loads against one module
|
||||||
// instance, ask for one too.
|
// instance, ask for one too.
|
||||||
function resetRenderedViews() {
|
function resetRenderedViews() {
|
||||||
renderedViews.clear();
|
renderedViews.clear();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const {
|
|||||||
showFlash,
|
showFlash,
|
||||||
balanceLinesForAddress,
|
balanceLinesForAddress,
|
||||||
txCounterpartyHtml,
|
txCounterpartyHtml,
|
||||||
|
HISTORY_OFF_TEXT,
|
||||||
addressTitle,
|
addressTitle,
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
displaySymbol,
|
displaySymbol,
|
||||||
@@ -83,6 +84,8 @@ function show() {
|
|||||||
ctx.showAddressToken();
|
ctx.showAddressToken();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
// Tokens the user has not tracked are Blockscout's to report.
|
||||||
|
$("address-tokens-note").classList.toggle("hidden", !!state.useBlockscout);
|
||||||
renderSendTokenSelect(addr);
|
renderSendTokenSelect(addr);
|
||||||
$("tx-list").innerHTML =
|
$("tx-list").innerHTML =
|
||||||
'<div class="text-muted text-xs py-1">Loading...</div>';
|
'<div class="text-muted text-xs py-1">Loading...</div>';
|
||||||
@@ -99,6 +102,7 @@ async function loadTransactions(address) {
|
|||||||
const rawTxs = await fetchRecentTransactions(
|
const rawTxs = await fetchRecentTransactions(
|
||||||
address,
|
address,
|
||||||
state.blockscoutUrl,
|
state.blockscoutUrl,
|
||||||
|
state.useBlockscout,
|
||||||
currentNetwork().chainId,
|
currentNetwork().chainId,
|
||||||
);
|
);
|
||||||
const result = filterTransactions(rawTxs, {
|
const result = filterTransactions(rawTxs, {
|
||||||
@@ -155,6 +159,10 @@ async function loadTransactions(address) {
|
|||||||
|
|
||||||
function renderTransactions(txs) {
|
function renderTransactions(txs) {
|
||||||
const list = $("tx-list");
|
const list = $("tx-list");
|
||||||
|
if (!state.useBlockscout) {
|
||||||
|
list.innerHTML = `<div class="text-muted text-xs py-1">${escapeHtml(HISTORY_OFF_TEXT)}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (txs.length === 0) {
|
if (txs.length === 0) {
|
||||||
list.innerHTML =
|
list.innerHTML =
|
||||||
'<div class="text-muted text-xs py-1">No transactions found.</div>';
|
'<div class="text-muted text-xs py-1">No transactions found.</div>';
|
||||||
@@ -229,7 +237,7 @@ function init(_ctx) {
|
|||||||
}
|
}
|
||||||
$("send-to").value = "";
|
$("send-to").value = "";
|
||||||
$("send-amount").value = "";
|
$("send-amount").value = "";
|
||||||
$("send-token").classList.remove("hidden");
|
$("send-token-dropdown").classList.remove("hidden");
|
||||||
$("send-token-static").classList.add("hidden");
|
$("send-token-static").classList.add("hidden");
|
||||||
updateSendBalance();
|
updateSendBalance();
|
||||||
resetSendValidation();
|
resetSendValidation();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const {
|
|||||||
showFlash,
|
showFlash,
|
||||||
flashCopyFeedback,
|
flashCopyFeedback,
|
||||||
txCounterpartyHtml,
|
txCounterpartyHtml,
|
||||||
|
HISTORY_OFF_TEXT,
|
||||||
addressTitle,
|
addressTitle,
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
displaySymbol,
|
displaySymbol,
|
||||||
@@ -173,6 +174,7 @@ async function loadTransactions(address, tokenId) {
|
|||||||
const rawTxs = await fetchRecentTransactions(
|
const rawTxs = await fetchRecentTransactions(
|
||||||
address,
|
address,
|
||||||
state.blockscoutUrl,
|
state.blockscoutUrl,
|
||||||
|
state.useBlockscout,
|
||||||
currentNetwork().chainId,
|
currentNetwork().chainId,
|
||||||
);
|
);
|
||||||
const result = filterTransactions(rawTxs, {
|
const result = filterTransactions(rawTxs, {
|
||||||
@@ -239,6 +241,10 @@ async function loadTransactions(address, tokenId) {
|
|||||||
|
|
||||||
function renderTransactions(txs) {
|
function renderTransactions(txs) {
|
||||||
const list = $("address-token-tx-list");
|
const list = $("address-token-tx-list");
|
||||||
|
if (!state.useBlockscout) {
|
||||||
|
list.innerHTML = `<div class="text-muted text-xs py-1">${escapeHtml(HISTORY_OFF_TEXT)}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (txs.length === 0) {
|
if (txs.length === 0) {
|
||||||
list.innerHTML =
|
list.innerHTML =
|
||||||
'<div class="text-muted text-xs py-1">No transactions found.</div>';
|
'<div class="text-muted text-xs py-1">No transactions found.</div>';
|
||||||
@@ -324,7 +330,7 @@ function init(_ctx) {
|
|||||||
$("send-token").value = tokenId;
|
$("send-token").value = tokenId;
|
||||||
}
|
}
|
||||||
// Hide dropdown, show static token display
|
// Hide dropdown, show static token display
|
||||||
$("send-token").classList.add("hidden");
|
$("send-token-dropdown").classList.add("hidden");
|
||||||
let staticHtml = `<div class="font-bold">${escapeHtml(displaySymbol(currentSymbol))}</div>`;
|
let staticHtml = `<div class="font-bold">${escapeHtml(displaySymbol(currentSymbol))}</div>`;
|
||||||
if (tokenId !== "ETH") {
|
if (tokenId !== "ETH") {
|
||||||
staticHtml += `<div class="text-xs">${renderAddressHtml(tokenId)}</div>`;
|
staticHtml += `<div class="text-xs">${renderAddressHtml(tokenId)}</div>`;
|
||||||
|
|||||||
@@ -334,6 +334,11 @@ function show(txInfo) {
|
|||||||
$("confirm-contract-warning").style.visibility = "hidden";
|
$("confirm-contract-warning").style.visibility = "hidden";
|
||||||
$("confirm-burn-warning").style.visibility = "hidden";
|
$("confirm-burn-warning").style.visibility = "hidden";
|
||||||
$("confirm-etherscan-warning").style.visibility = "hidden";
|
$("confirm-etherscan-warning").style.visibility = "hidden";
|
||||||
|
// While "Check recipients on Etherscan" is off, there is no Etherscan
|
||||||
|
// warning to reserve space for. A line in its place says so.
|
||||||
|
const etherscanOn = !!state.checkRecipientsOnEtherscan;
|
||||||
|
$("confirm-etherscan-warning").classList.toggle("hidden", !etherscanOn);
|
||||||
|
$("confirm-etherscan-note").classList.toggle("hidden", etherscanOn);
|
||||||
|
|
||||||
// Show burn warning via reserved element (in addition to inline warning)
|
// Show burn warning via reserved element (in addition to inline warning)
|
||||||
if (isBurnAddress(txInfo.to)) {
|
if (isBurnAddress(txInfo.to)) {
|
||||||
|
|||||||
@@ -119,6 +119,11 @@ function show(walletIdx, addrIdx) {
|
|||||||
|
|
||||||
$("delete-address-recovery").textContent = recoveryPathText(wallet);
|
$("delete-address-recovery").textContent = recoveryPathText(wallet);
|
||||||
$("delete-address-balance").innerHTML = balanceWarningHtml(addr);
|
$("delete-address-balance").innerHTML = balanceWarningHtml(addr);
|
||||||
|
// Tokens the user has not tracked are Blockscout's to report.
|
||||||
|
$("delete-address-tokens-note").classList.toggle(
|
||||||
|
"hidden",
|
||||||
|
!!state.useBlockscout,
|
||||||
|
);
|
||||||
|
|
||||||
setFlash("");
|
setFlash("");
|
||||||
showView("delete-address-confirm");
|
showView("delete-address-confirm");
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const {
|
|||||||
broadcastActiveChanged,
|
broadcastActiveChanged,
|
||||||
} = require("../../shared/walletDelete");
|
} = require("../../shared/walletDelete");
|
||||||
const { INVISIBLE_CHARACTERS } = require("../../shared/symbolSpoof");
|
const { INVISIBLE_CHARACTERS } = require("../../shared/symbolSpoof");
|
||||||
|
const welcome = require("./welcome");
|
||||||
|
|
||||||
let deleteWalletIndex = null;
|
let deleteWalletIndex = null;
|
||||||
let lostPasswordIndex = null;
|
let lostPasswordIndex = null;
|
||||||
@@ -135,7 +136,7 @@ async function finishDelete(walletIdx) {
|
|||||||
// Save before broadcasting: the background reads the active
|
// Save before broadcasting: the background reads the active
|
||||||
// address back out of storage to build accountsChanged.
|
// address back out of storage to build accountsChanged.
|
||||||
if (activeAddressChanged) broadcastActiveChanged();
|
if (activeAddressChanged) broadcastActiveChanged();
|
||||||
showView("welcome");
|
welcome.show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -534,6 +534,11 @@ function addressTitle(address, wallets) {
|
|||||||
const CONTRACT_CREATION_TEXT =
|
const CONTRACT_CREATION_TEXT =
|
||||||
"This transaction creates a new contract. It has no recipient.";
|
"This transaction creates a new contract. It has no recipient.";
|
||||||
|
|
||||||
|
// What the transaction lists on Home, AddressDetail and AddressToken say while
|
||||||
|
// the user has "Use Blockscout" off. Their history comes from Blockscout alone.
|
||||||
|
const HISTORY_OFF_TEXT =
|
||||||
|
'No transaction history is shown, because "Use Blockscout" is off in Settings.';
|
||||||
|
|
||||||
// The last two lines of a transaction history row: the counterparty's colour
|
// The last two lines of a transaction history row: the counterparty's colour
|
||||||
// dot and name beside the amount, then its full address. A contract creation
|
// dot and name beside the amount, then its full address. A contract creation
|
||||||
// the user sent has no counterparty (its `to` is ""), so its row has the
|
// the user sent has no counterparty (its `to` is ""), so its row has the
|
||||||
@@ -799,6 +804,7 @@ module.exports = {
|
|||||||
displaySymbol,
|
displaySymbol,
|
||||||
addressTitle,
|
addressTitle,
|
||||||
CONTRACT_CREATION_TEXT,
|
CONTRACT_CREATION_TEXT,
|
||||||
|
HISTORY_OFF_TEXT,
|
||||||
txCounterpartyHtml,
|
txCounterpartyHtml,
|
||||||
formatAddressHtml,
|
formatAddressHtml,
|
||||||
renderAddressHtml,
|
renderAddressHtml,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const {
|
|||||||
timeAgo,
|
timeAgo,
|
||||||
addressDotHtml,
|
addressDotHtml,
|
||||||
txCounterpartyHtml,
|
txCounterpartyHtml,
|
||||||
|
HISTORY_OFF_TEXT,
|
||||||
addressTitle,
|
addressTitle,
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
displaySymbol,
|
displaySymbol,
|
||||||
@@ -100,6 +101,10 @@ let homeTxs = [];
|
|||||||
function renderHomeTxList(ctx) {
|
function renderHomeTxList(ctx) {
|
||||||
const list = $("home-tx-list");
|
const list = $("home-tx-list");
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
|
if (!state.useBlockscout) {
|
||||||
|
list.innerHTML = `<div class="text-muted text-xs py-1">${escapeHtml(HISTORY_OFF_TEXT)}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (homeTxs.length === 0) {
|
if (homeTxs.length === 0) {
|
||||||
list.innerHTML =
|
list.innerHTML =
|
||||||
'<div class="text-muted text-xs py-1">No transactions found.</div>';
|
'<div class="text-muted text-xs py-1">No transactions found.</div>';
|
||||||
@@ -192,6 +197,7 @@ async function loadHomeTxs(ctx) {
|
|||||||
fetchRecentTransactions(
|
fetchRecentTransactions(
|
||||||
addr,
|
addr,
|
||||||
state.blockscoutUrl,
|
state.blockscoutUrl,
|
||||||
|
state.useBlockscout,
|
||||||
currentNetwork().chainId,
|
currentNetwork().chainId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -300,6 +306,8 @@ function walletListHtml() {
|
|||||||
|
|
||||||
function render(ctx) {
|
function render(ctx) {
|
||||||
const container = $("wallet-list");
|
const container = $("wallet-list");
|
||||||
|
// Tokens the user has not tracked are Blockscout's to report.
|
||||||
|
$("home-tokens-note").classList.toggle("hidden", !!state.useBlockscout);
|
||||||
if (state.wallets.length === 0) {
|
if (state.wallets.length === 0) {
|
||||||
container.innerHTML =
|
container.innerHTML =
|
||||||
'<p class="text-muted py-2">No wallets yet. Add one to get started.</p>';
|
'<p class="text-muted py-2">No wallets yet. Add one to get started.</p>';
|
||||||
@@ -447,7 +455,7 @@ function init(ctx) {
|
|||||||
}
|
}
|
||||||
$("send-to").value = "";
|
$("send-to").value = "";
|
||||||
$("send-amount").value = "";
|
$("send-amount").value = "";
|
||||||
$("send-token").classList.remove("hidden");
|
$("send-token-dropdown").classList.remove("hidden");
|
||||||
$("send-token-static").classList.add("hidden");
|
$("send-token-static").classList.add("hidden");
|
||||||
renderSendTokenSelect(addr);
|
renderSendTokenSelect(addr);
|
||||||
updateSendBalance();
|
updateSendBalance();
|
||||||
|
|||||||
@@ -141,6 +141,8 @@ function updateToValidation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderSendTokenSelect(addr) {
|
function renderSendTokenSelect(addr) {
|
||||||
|
// Tokens the user has not tracked are Blockscout's to report.
|
||||||
|
$("send-tokens-note").classList.toggle("hidden", !!state.useBlockscout);
|
||||||
const sel = $("send-token");
|
const sel = $("send-token");
|
||||||
sel.innerHTML = `<option value="ETH">${escapeHtml(nativeCurrency())}</option>`;
|
sel.innerHTML = `<option value="ETH">${escapeHtml(nativeCurrency())}</option>`;
|
||||||
const fraudSet = new Set(
|
const fraudSet = new Set(
|
||||||
@@ -426,7 +428,7 @@ function init(_ctx) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("btn-send-back").addEventListener("click", () => {
|
$("btn-send-back").addEventListener("click", () => {
|
||||||
$("send-token").classList.remove("hidden");
|
$("send-token-dropdown").classList.remove("hidden");
|
||||||
$("send-token-static").classList.add("hidden");
|
$("send-token-static").classList.add("hidden");
|
||||||
goBack();
|
goBack();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const {
|
|||||||
const { state, saveState, currentNetwork } = require("../../shared/state");
|
const { state, saveState, currentNetwork } = require("../../shared/state");
|
||||||
const { onChainSwitch } = require("../../shared/chainSwitch");
|
const { onChainSwitch } = require("../../shared/chainSwitch");
|
||||||
const { restorableStack } = require("../../shared/persistedState");
|
const { restorableStack } = require("../../shared/persistedState");
|
||||||
|
const { dropUntrackedTokens } = require("../../shared/balances");
|
||||||
const { resetRenderedViews, markViewRendered } = require("../viewRouter");
|
const { resetRenderedViews, markViewRendered } = require("../viewRouter");
|
||||||
const {
|
const {
|
||||||
log,
|
log,
|
||||||
@@ -201,6 +202,10 @@ function show() {
|
|||||||
$("settings-rpc").value = state.rpcUrl;
|
$("settings-rpc").value = state.rpcUrl;
|
||||||
$("settings-blockscout").value = state.blockscoutUrl;
|
$("settings-blockscout").value = state.blockscoutUrl;
|
||||||
$("settings-network").value = state.networkId;
|
$("settings-network").value = state.networkId;
|
||||||
|
// Set on every show, not only at init: Welcome can change these two.
|
||||||
|
$("settings-use-blockscout").checked = state.useBlockscout;
|
||||||
|
$("settings-check-recipients-on-etherscan").checked =
|
||||||
|
state.checkRecipientsOnEtherscan;
|
||||||
renderTrackedTokens();
|
renderTrackedTokens();
|
||||||
renderSiteLists();
|
renderSiteLists();
|
||||||
renderWalletListSettings();
|
renderWalletListSettings();
|
||||||
@@ -257,6 +262,8 @@ async function renderSiteLists() {
|
|||||||
// native token: any other token there was picked on the previous network, so
|
// native token: any other token there was picked on the previous network, so
|
||||||
// it is no longer selected either. Without a wallet the stack starts at
|
// it is no longer selected either. Without a wallet the stack starts at
|
||||||
// Welcome, which that cut would drop, and Welcome shows nothing from a network.
|
// Welcome, which that cut would drop, and Welcome shows nothing from a network.
|
||||||
|
// A change to "Use Blockscout" cuts the stack the same way (setUseBlockscout()
|
||||||
|
// below).
|
||||||
function cutViewStack() {
|
function cutViewStack() {
|
||||||
if (!state.hasWallet) return;
|
if (!state.hasWallet) return;
|
||||||
if (state.selectedToken !== "ETH") {
|
if (state.selectedToken !== "ETH") {
|
||||||
@@ -269,18 +276,39 @@ function cutViewStack() {
|
|||||||
state.viewStack = restorableStack(state.viewStack, state.currentView);
|
state.viewStack = restorableStack(state.viewStack, state.currentView);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every screen counts as not yet rendered, so "Back" renders the one it lands
|
// Sets "Use Blockscout" to `on` on this page. It is called inside a save,
|
||||||
// on for the new network. Settings on top is brought up to date in place;
|
// holding the lock, for the user's change here or another page's
|
||||||
// any other screen is drawn again as a reopened popup draws it, which is Home
|
// (followStoredSettings() in src/popup/index.js).
|
||||||
// for one that cannot be rendered from saved state. Without a wallet any other
|
//
|
||||||
// screen is Welcome or AddWallet, which show nothing from a network.
|
// Send's token dropdown was filled under the old setting, and Back cannot draw
|
||||||
function showNewNetwork() {
|
// Send again. So the stack is cut, as for a network switch. Turning the setting
|
||||||
|
// off also drops every token the user has not tracked.
|
||||||
|
function setUseBlockscout(on) {
|
||||||
|
state.useBlockscout = on;
|
||||||
|
cutViewStack();
|
||||||
|
if (!on) dropUntrackedTokens(state.wallets, state.trackedTokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draws the page again after a network switch, or after another page changed
|
||||||
|
// "Use Blockscout" or "Check recipients on Etherscan". Every screen counts as
|
||||||
|
// not yet rendered, so "Back" renders the one it lands on under the new
|
||||||
|
// settings. Settings on top is brought up to date in place; any other screen is
|
||||||
|
// drawn again as a reopened popup draws it, which is Home for one that cannot
|
||||||
|
// be rendered from saved state. Without a wallet any other screen is Welcome or
|
||||||
|
// AddWallet. Back only unhides Welcome, so its two checkboxes are set here.
|
||||||
|
function showNewSettings() {
|
||||||
resetRenderedViews();
|
resetRenderedViews();
|
||||||
|
$("welcome-use-blockscout").checked = state.useBlockscout;
|
||||||
|
$("welcome-check-recipients-on-etherscan").checked =
|
||||||
|
state.checkRecipientsOnEtherscan;
|
||||||
if (state.currentView === "settings") {
|
if (state.currentView === "settings") {
|
||||||
markViewRendered("settings");
|
markViewRendered("settings");
|
||||||
$("settings-network").value = state.networkId;
|
$("settings-network").value = state.networkId;
|
||||||
$("settings-rpc").value = state.rpcUrl;
|
$("settings-rpc").value = state.rpcUrl;
|
||||||
$("settings-blockscout").value = state.blockscoutUrl;
|
$("settings-blockscout").value = state.blockscoutUrl;
|
||||||
|
$("settings-use-blockscout").checked = state.useBlockscout;
|
||||||
|
$("settings-check-recipients-on-etherscan").checked =
|
||||||
|
state.checkRecipientsOnEtherscan;
|
||||||
renderTrackedTokens();
|
renderTrackedTokens();
|
||||||
} else if (state.hasWallet) {
|
} else if (state.hasWallet) {
|
||||||
ctx.restoreView();
|
ctx.restoreView();
|
||||||
@@ -363,6 +391,35 @@ function init(_ctx) {
|
|||||||
showFlash("Saved.");
|
showFlash("Saved.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Each change is made inside the save, holding the lock. Then
|
||||||
|
// followStoredSettings() in src/popup/index.js never takes this page's
|
||||||
|
// unsaved change for another page's.
|
||||||
|
//
|
||||||
|
// The screens under Settings were drawn under the old setting. So each
|
||||||
|
// change makes every screen but Settings count as not yet drawn, as a
|
||||||
|
// network switch does. Back then draws again any screen a reopened popup
|
||||||
|
// can draw. Back only unhides Welcome, so its boxes are set here too.
|
||||||
|
$("settings-use-blockscout").addEventListener("change", async () => {
|
||||||
|
const on = $("settings-use-blockscout").checked;
|
||||||
|
await saveState(() => setUseBlockscout(on));
|
||||||
|
$("welcome-use-blockscout").checked = on;
|
||||||
|
resetRenderedViews();
|
||||||
|
markViewRendered("settings");
|
||||||
|
});
|
||||||
|
|
||||||
|
$("settings-check-recipients-on-etherscan").addEventListener(
|
||||||
|
"change",
|
||||||
|
async () => {
|
||||||
|
const on = $("settings-check-recipients-on-etherscan").checked;
|
||||||
|
await saveState(() => {
|
||||||
|
state.checkRecipientsOnEtherscan = on;
|
||||||
|
});
|
||||||
|
$("welcome-check-recipients-on-etherscan").checked = on;
|
||||||
|
resetRenderedViews();
|
||||||
|
markViewRendered("settings");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const networkSelect = $("settings-network");
|
const networkSelect = $("settings-network");
|
||||||
networkSelect.addEventListener("change", async () => {
|
networkSelect.addEventListener("change", async () => {
|
||||||
// Whether `state` already has the network picked is asked only once
|
// Whether `state` already has the network picked is asked only once
|
||||||
@@ -376,7 +433,7 @@ function init(_ctx) {
|
|||||||
// Open pages are told by the background, as after a site's approved
|
// Open pages are told by the background, as after a site's approved
|
||||||
// switch request.
|
// switch request.
|
||||||
notify({ type: "AUTISTMASK_NETWORK_CHANGED", chainId: net.chainId });
|
notify({ type: "AUTISTMASK_NETWORK_CHANGED", chainId: net.chainId });
|
||||||
showNewNetwork();
|
showNewSettings();
|
||||||
showFlash("Switched to " + net.name + ".");
|
showFlash("Switched to " + net.name + ".");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -516,5 +573,6 @@ module.exports = {
|
|||||||
show,
|
show,
|
||||||
renderSiteLists,
|
renderSiteLists,
|
||||||
cutViewStack,
|
cutViewStack,
|
||||||
showNewNetwork,
|
setUseBlockscout,
|
||||||
|
showNewSettings,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -191,6 +191,11 @@ function render() {
|
|||||||
if (el) el.classList.add("hidden");
|
if (el) el.classList.add("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Those details are Blockscout's; this line says why there are none.
|
||||||
|
$("tx-detail-blockscout-note").classList.toggle(
|
||||||
|
"hidden",
|
||||||
|
!!state.useBlockscout,
|
||||||
|
);
|
||||||
loadFullTxDetails(tx.hash, tx.chainId);
|
loadFullTxDetails(tx.hash, tx.chainId);
|
||||||
|
|
||||||
const isoStr = isoDate(tx.timestamp);
|
const isoStr = isoDate(tx.timestamp);
|
||||||
@@ -303,7 +308,11 @@ function populateOnChainDetails(txData, chainId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reads the block, network details and calldata from Blockscout, and decodes
|
||||||
|
// the calldata. While "Use Blockscout" is off, Blockscout is not asked and none
|
||||||
|
// of them is shown.
|
||||||
async function loadFullTxDetails(txHash, chainId) {
|
async function loadFullTxDetails(txHash, chainId) {
|
||||||
|
if (!state.useBlockscout) return;
|
||||||
const section = $("tx-detail-calldata-section");
|
const section = $("tx-detail-calldata-section");
|
||||||
const actionEl = $("tx-detail-calldata-action");
|
const actionEl = $("tx-detail-calldata-action");
|
||||||
const detailsEl = $("tx-detail-calldata-details");
|
const detailsEl = $("tx-detail-calldata-details");
|
||||||
|
|||||||
@@ -1,7 +1,37 @@
|
|||||||
const { $ } = require("./helpers");
|
// The first screen, shown while there is no wallet. It offers "Use Blockscout"
|
||||||
|
// and "Check recipients on Etherscan", which Settings offers too. Both are off
|
||||||
|
// until the user ticks them.
|
||||||
|
|
||||||
function init(ctx) {
|
const { $, showView } = require("./helpers");
|
||||||
$("btn-welcome-add").addEventListener("click", ctx.showAddWalletView);
|
const { state, saveState } = require("../../shared/state");
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
$("welcome-use-blockscout").checked = state.useBlockscout;
|
||||||
|
$("welcome-check-recipients-on-etherscan").checked =
|
||||||
|
state.checkRecipientsOnEtherscan;
|
||||||
|
showView("welcome");
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { init };
|
// Each change is made inside the save, holding the lock, as Settings makes it.
|
||||||
|
function init(ctx) {
|
||||||
|
$("btn-welcome-add").addEventListener("click", ctx.showAddWalletView);
|
||||||
|
|
||||||
|
$("welcome-use-blockscout").addEventListener("change", async () => {
|
||||||
|
const on = $("welcome-use-blockscout").checked;
|
||||||
|
await saveState(() => {
|
||||||
|
state.useBlockscout = on;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("welcome-check-recipients-on-etherscan").addEventListener(
|
||||||
|
"change",
|
||||||
|
async () => {
|
||||||
|
const on = $("welcome-check-recipients-on-etherscan").checked;
|
||||||
|
await saveState(() => {
|
||||||
|
state.checkRecipientsOnEtherscan = on;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { init, show };
|
||||||
|
|||||||
+45
-7
@@ -1,6 +1,10 @@
|
|||||||
// Balance fetching: ETH balances and the balances of the tokens the user
|
// Balance fetching:
|
||||||
// tracks via RPC, the other ERC-20 token balances and the holder counts via
|
//
|
||||||
// Blockscout, ENS reverse lookup via RPC.
|
// - ETH balances, and the balances of the tokens the user tracks, via RPC
|
||||||
|
// - the other ERC-20 token balances and the holder counts via Blockscout,
|
||||||
|
// while the user has "Use Blockscout" on. Those balances are dropped when
|
||||||
|
// it is off.
|
||||||
|
// - ENS reverse lookup via RPC
|
||||||
|
|
||||||
const {
|
const {
|
||||||
JsonRpcProvider,
|
JsonRpcProvider,
|
||||||
@@ -37,9 +41,18 @@ const { resolveTokenDecimals } = require("./approvalAmount");
|
|||||||
// ethers knows chains this wallet does not, so an id that is not one of ours
|
// ethers knows chains this wallet does not, so an id that is not one of ours
|
||||||
// is a caller bug and must not resolve to a working provider for some other
|
// is a caller bug and must not resolve to a working provider for some other
|
||||||
// chain.
|
// chain.
|
||||||
|
//
|
||||||
|
// `disableCcipRead` keeps every ENS lookup on the RPC endpoint. A resolver can
|
||||||
|
// say a name is resolved off-chain and give a web address for it. By default
|
||||||
|
// ethers then fetches that address straight from the browser, sending the name
|
||||||
|
// being looked up. For the reverse lookup of the user's addresses, that is the
|
||||||
|
// user's own name (https://git.eeqj.de/sneak/AutistMask/issues/668). With the
|
||||||
|
// flag set, such a name does not resolve.
|
||||||
function getProvider(rpcUrl, networkId) {
|
function getProvider(rpcUrl, networkId) {
|
||||||
const net = Network.from(requireNetworkId(networkId).id);
|
const net = Network.from(requireNetworkId(networkId).id);
|
||||||
return new JsonRpcProvider(rpcUrl, net, { staticNetwork: net });
|
const provider = new JsonRpcProvider(rpcUrl, net, { staticNetwork: net });
|
||||||
|
provider.disableCcipRead = true;
|
||||||
|
return provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
function requireNetworkId(networkId) {
|
function requireNetworkId(networkId) {
|
||||||
@@ -81,6 +94,11 @@ function rawUnits(value) {
|
|||||||
|
|
||||||
// Fetch token balances for a single address from Blockscout.
|
// Fetch token balances for a single address from Blockscout.
|
||||||
// Returns [{ address, name, symbol, decimals, balance, holders }].
|
// Returns [{ address, name, symbol, decimals, balance, holders }].
|
||||||
|
//
|
||||||
|
// `useBlockscout` is the user's "Use Blockscout" setting. While it is off,
|
||||||
|
// Blockscout is not asked and the answer is an empty list. So no token the user
|
||||||
|
// has not tracked is listed.
|
||||||
|
//
|
||||||
// Filters out spam: only shows tokens that are in the known token list,
|
// Filters out spam: only shows tokens that are in the known token list,
|
||||||
// explicitly tracked by the user, or have >= 1000 holders. A tracked token's
|
// explicitly tracked by the user, or have >= 1000 holders. A tracked token's
|
||||||
// balance is read through the RPC endpoint instead, and its row here gives that
|
// balance is read through the RPC endpoint instead, and its row here gives that
|
||||||
@@ -96,9 +114,11 @@ function rawUnits(value) {
|
|||||||
async function fetchTokenBalances(
|
async function fetchTokenBalances(
|
||||||
address,
|
address,
|
||||||
blockscoutUrl,
|
blockscoutUrl,
|
||||||
|
useBlockscout,
|
||||||
trackedTokens,
|
trackedTokens,
|
||||||
signal,
|
signal,
|
||||||
) {
|
) {
|
||||||
|
if (!useBlockscout) return [];
|
||||||
try {
|
try {
|
||||||
const resp = await debugFetch(
|
const resp = await debugFetch(
|
||||||
blockscoutUrl + "/addresses/" + address + "/token-balances",
|
blockscoutUrl + "/addresses/" + address + "/token-balances",
|
||||||
@@ -296,12 +316,13 @@ async function fetchTrackedTokenBalances(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch ETH balances, ENS names, and ERC-20 token balances for all addresses.
|
// Fetch ETH balances, ENS names, and ERC-20 token balances for all addresses.
|
||||||
// `trackedTokens` are the tokens tracked on the active network. `signal` is as
|
// `trackedTokens` are the tokens tracked on the active network.
|
||||||
// for fetchTokenBalances().
|
// `useBlockscout` and `signal` are as for fetchTokenBalances().
|
||||||
async function refreshBalances(
|
async function refreshBalances(
|
||||||
wallets,
|
wallets,
|
||||||
rpcUrl,
|
rpcUrl,
|
||||||
blockscoutUrl,
|
blockscoutUrl,
|
||||||
|
useBlockscout,
|
||||||
trackedTokens,
|
trackedTokens,
|
||||||
networkId,
|
networkId,
|
||||||
signal,
|
signal,
|
||||||
@@ -362,13 +383,15 @@ async function refreshBalances(
|
|||||||
// ERC-20 token balances: the tracked tokens' through the RPC
|
// ERC-20 token balances: the tracked tokens' through the RPC
|
||||||
// endpoint, and from Blockscout the tokens the user has not
|
// endpoint, and from Blockscout the tokens the user has not
|
||||||
// tracked. When Blockscout fails, the untracked rows from before
|
// tracked. When Blockscout fails, the untracked rows from before
|
||||||
// this refresh stay. The Blockscout request is started first and
|
// this refresh stay. While "Use Blockscout" is off, there are no
|
||||||
|
// untracked rows. The Blockscout request is started first and
|
||||||
// handed to fetchTrackedTokenBalances() too, which waits for it
|
// handed to fetchTrackedTokenBalances() too, which waits for it
|
||||||
// after sending its balanceOf reads, so the two run side by side.
|
// after sending its balanceOf reads, so the two run side by side.
|
||||||
const previous = addr.tokenBalances || [];
|
const previous = addr.tokenBalances || [];
|
||||||
const explorerRequest = fetchTokenBalances(
|
const explorerRequest = fetchTokenBalances(
|
||||||
addr.address,
|
addr.address,
|
||||||
blockscoutUrl,
|
blockscoutUrl,
|
||||||
|
useBlockscout,
|
||||||
tracked,
|
tracked,
|
||||||
signal,
|
signal,
|
||||||
);
|
);
|
||||||
@@ -404,6 +427,20 @@ async function refreshBalances(
|
|||||||
log.debugf("refreshBalances done");
|
log.debugf("refreshBalances done");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drops the balance of each token not in `trackedTokens` from every address.
|
||||||
|
// Callers run it while "Use Blockscout" is off, since only Blockscout reports
|
||||||
|
// those tokens.
|
||||||
|
function dropUntrackedTokens(wallets, trackedTokens) {
|
||||||
|
const tracked = new Set(trackedTokens.map((t) => t.address.toLowerCase()));
|
||||||
|
for (const wallet of wallets) {
|
||||||
|
for (const addr of wallet.addresses || []) {
|
||||||
|
addr.tokenBalances = (addr.tokenBalances || []).filter((row) =>
|
||||||
|
tracked.has(String(row.address).toLowerCase()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Look up token metadata from its contract.
|
// Look up token metadata from its contract.
|
||||||
// Calls symbol() and decimals() to verify it implements ERC-20.
|
// Calls symbol() and decimals() to verify it implements ERC-20.
|
||||||
// `signal` is as for fetchTokenBalances().
|
// `signal` is as for fetchTokenBalances().
|
||||||
@@ -522,6 +559,7 @@ async function scanForAddresses(xpub, rpcUrl, networkId, signal, gapLimit = 5) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
fetchTokenBalances,
|
fetchTokenBalances,
|
||||||
refreshBalances,
|
refreshBalances,
|
||||||
|
dropUntrackedTokens,
|
||||||
lookupTokenInfo,
|
lookupTokenInfo,
|
||||||
getProvider,
|
getProvider,
|
||||||
scanForAddresses,
|
scanForAddresses,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
//
|
//
|
||||||
// - onChainSwitch() below makes the switch the user picks in Settings, with
|
// - onChainSwitch() below makes the switch the user picks in Settings, with
|
||||||
// applyChainSwitchFields() (chainSwitchFields.js).
|
// applyChainSwitchFields() (chainSwitchFields.js).
|
||||||
// - followNetworkSwitch() in src/popup/index.js takes a switch the background
|
// - followStoredSettings() in src/popup/index.js takes a switch the background
|
||||||
// or another popup page saved, from the stored record, with
|
// or another popup page saved, from the stored record, with
|
||||||
// takeStoredNetwork() (state.js), which copies it with
|
// takeStoredNetwork() (state.js), which copies it with
|
||||||
// copyChainSwitchFields() (chainSwitchFields.js).
|
// copyChainSwitchFields() (chainSwitchFields.js).
|
||||||
@@ -45,7 +45,7 @@ function clearPopupCaches() {
|
|||||||
//
|
//
|
||||||
// `state` is switched inside the save that stores the switch, holding the
|
// `state` is switched inside the save that stores the switch, holding the
|
||||||
// lock, so it is never switched while a follow of a switch saved elsewhere
|
// lock, so it is never switched while a follow of a switch saved elsewhere
|
||||||
// waits for the lock (followNetworkSwitch() in src/popup/index.js): that
|
// waits for the lock (followStoredSettings() in src/popup/index.js): that
|
||||||
// follow runs first, and this switch is made after it.
|
// follow runs first, and this switch is made after it.
|
||||||
async function onChainSwitch(newNetworkId) {
|
async function onChainSwitch(newNetworkId) {
|
||||||
const { state, saveState } = require("./state");
|
const { state, saveState } = require("./state");
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
// This is a best-effort enrichment: network failures return null silently.
|
// This is a best-effort enrichment: network failures return null silently.
|
||||||
|
|
||||||
const { networkByChainId } = require("./networks");
|
const { networkByChainId } = require("./networks");
|
||||||
|
const { storageGet } = require("./browserApi");
|
||||||
|
const { normalizePersisted } = require("./persistedState");
|
||||||
|
|
||||||
// Patterns in the page title that indicate a flagged address.
|
// Patterns in the page title that indicate a flagged address.
|
||||||
// Title format: "Fake_Phishing184810 | Address: 0x... | Etherscan"
|
// Title format: "Fake_Phishing184810 | Address: 0x... | Etherscan"
|
||||||
@@ -78,12 +80,22 @@ function parseEtherscanPage(html) {
|
|||||||
* when it is omitted: etherscan.io on mainnet, sepolia.etherscan.io on
|
* when it is omitted: etherscan.io on mainnet, sepolia.etherscan.io on
|
||||||
* Sepolia.
|
* Sepolia.
|
||||||
*
|
*
|
||||||
|
* Etherscan is asked only while "Check recipients on Etherscan" is on in the
|
||||||
|
* stored profile. The page's own copy of the setting is not used, because
|
||||||
|
* another page may have just changed it. The stored setting is read through
|
||||||
|
* normalizePersisted(), so it is on only when a load would find it on. While
|
||||||
|
* it is off, the answer is null.
|
||||||
|
*
|
||||||
* @param {string} address - Ethereum address to check.
|
* @param {string} address - Ethereum address to check.
|
||||||
* @param {string} [chainId] - Chain id of the network to look it up on.
|
* @param {string} [chainId] - Chain id of the network to look it up on.
|
||||||
* @returns {Promise<{type: string, message: string, severity: string}|null>}
|
* @returns {Promise<{type: string, message: string, severity: string}|null>}
|
||||||
*/
|
*/
|
||||||
async function checkEtherscanLabel(address, chainId) {
|
async function checkEtherscanLabel(address, chainId) {
|
||||||
try {
|
try {
|
||||||
|
const { autistmask } = await storageGet("autistmask");
|
||||||
|
if (!normalizePersisted(autistmask).checkRecipientsOnEtherscan) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
// Lazy require to avoid pulling in chrome.storage at module scope
|
// Lazy require to avoid pulling in chrome.storage at module scope
|
||||||
// (which breaks unit tests that only exercise parseEtherscanPage).
|
// (which breaks unit tests that only exercise parseEtherscanPage).
|
||||||
const { currentNetwork } = require("./state");
|
const { currentNetwork } = require("./state");
|
||||||
@@ -105,6 +117,7 @@ async function checkEtherscanLabel(address, chainId) {
|
|||||||
return null;
|
return null;
|
||||||
} catch {
|
} catch {
|
||||||
// Network errors are expected — Etherscan may rate-limit or block.
|
// Network errors are expected — Etherscan may rate-limit or block.
|
||||||
|
// A failed storage read lands here too, before Etherscan is asked.
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,14 @@ const DEFAULT_STATE = {
|
|||||||
hideDustTransactions: true,
|
hideDustTransactions: true,
|
||||||
dustThresholdGwei: 100000,
|
dustThresholdGwei: 100000,
|
||||||
utcTimestamps: false,
|
utcTimestamps: false,
|
||||||
|
// "Use Blockscout" and "Check recipients on Etherscan" in Settings. Each
|
||||||
|
// setting applies to every network. Both are off until the user turns them
|
||||||
|
// on. With "Use Blockscout" on, Blockscout is sent the user's addresses and
|
||||||
|
// the hash of each transaction opened. With "Check recipients on
|
||||||
|
// Etherscan" on, Etherscan is sent the recipient's address and nothing
|
||||||
|
// else.
|
||||||
|
useBlockscout: false,
|
||||||
|
checkRecipientsOnEtherscan: false,
|
||||||
fraudContracts: [],
|
fraudContracts: [],
|
||||||
tokenHolderCache: {},
|
tokenHolderCache: {},
|
||||||
theme: "system",
|
theme: "system",
|
||||||
@@ -406,6 +414,10 @@ function normalizePersisted(saved) {
|
|||||||
: 100000;
|
: 100000;
|
||||||
out.utcTimestamps =
|
out.utcTimestamps =
|
||||||
saved.utcTimestamps !== undefined ? saved.utcTimestamps : false;
|
saved.utcTimestamps !== undefined ? saved.utcTimestamps : false;
|
||||||
|
// Anything but true is off: each setting lets a request reach another
|
||||||
|
// service.
|
||||||
|
out.useBlockscout = saved.useBlockscout === true;
|
||||||
|
out.checkRecipientsOnEtherscan = saved.checkRecipientsOnEtherscan === true;
|
||||||
// A list of contract addresses, floored the same way: send.js builds its
|
// A list of contract addresses, floored the same way: send.js builds its
|
||||||
// fraud set as `(state.fraudContracts || []).map((a) => a.toLowerCase())`
|
// fraud set as `(state.fraudContracts || []).map((a) => a.toLowerCase())`
|
||||||
// and filterTransactions() maps the same list through normalizeAddress(),
|
// and filterTransactions() maps the same list through normalizeAddress(),
|
||||||
|
|||||||
+4
-3
@@ -470,8 +470,9 @@ function mergeNetworkEndpoints(base, ours, theirs) {
|
|||||||
// (caught by tests/txStatus.test.js). A page's live picture of a field it
|
// (caught by tests/txStatus.test.js). A page's live picture of a field it
|
||||||
// does not own goes on being whatever its last loadState() saw, same as
|
// does not own goes on being whatever its last loadState() saw, same as
|
||||||
// before this fix; only the persisted record is guaranteed current. The
|
// before this fix; only the persisted record is guaranteed current. The
|
||||||
// network is the exception: the popup takes a switch another context saved
|
// network, "Use Blockscout" and "Check recipients on Etherscan" are the
|
||||||
// through `beforeWrite` (followNetworkSwitch() in src/popup/index.js).
|
// exceptions: the popup takes a change another context saved through
|
||||||
|
// `beforeWrite` (followStoredSettings() in src/popup/index.js).
|
||||||
async function saveStateOnce(beforeWrite) {
|
async function saveStateOnce(beforeWrite) {
|
||||||
const result = await storageGet("autistmask");
|
const result = await storageGet("autistmask");
|
||||||
// The record in storage right now is about to be merged into and written
|
// The record in storage right now is about to be merged into and written
|
||||||
@@ -603,7 +604,7 @@ function saveState(beforeWrite) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Give this page the network switch held by `stored`, a record a save of this
|
// Give this page the network switch held by `stored`, a record a save of this
|
||||||
// page has just read (followNetworkSwitch() in src/popup/index.js calls this
|
// page has just read (followStoredSettings() in src/popup/index.js calls this
|
||||||
// from `beforeWrite`), in `state` and in `baseline` alike, as loadState() does
|
// from `beforeWrite`), in `state` and in `baseline` alike, as loadState() does
|
||||||
// with a whole record. The switch is then not this page's own change, even when
|
// with a whole record. The switch is then not this page's own change, even when
|
||||||
// that save fails to write: a later switch back to the previous network still
|
// that save fails to write: a later switch back to the previous network still
|
||||||
|
|||||||
@@ -235,12 +235,17 @@ function mergeTransactions(txs, tokenTransfers) {
|
|||||||
// `chainId` is the chain id of the network `blockscoutUrl` serves. Every entry
|
// `chainId` is the chain id of the network `blockscoutUrl` serves. Every entry
|
||||||
// carries it, and a native entry is labelled with that network's
|
// carries it, and a native entry is labelled with that network's
|
||||||
// `nativeCurrency` from networks.js (`ETH`, `SepoliaETH`).
|
// `nativeCurrency` from networks.js (`ETH`, `SepoliaETH`).
|
||||||
|
//
|
||||||
|
// `useBlockscout` is the user's "Use Blockscout" setting. While it is off,
|
||||||
|
// Blockscout is not asked and the answer is an empty list.
|
||||||
async function fetchRecentTransactions(
|
async function fetchRecentTransactions(
|
||||||
address,
|
address,
|
||||||
blockscoutUrl,
|
blockscoutUrl,
|
||||||
|
useBlockscout,
|
||||||
chainId,
|
chainId,
|
||||||
count = 25,
|
count = 25,
|
||||||
) {
|
) {
|
||||||
|
if (!useBlockscout) return [];
|
||||||
log.debugf("fetchRecentTransactions", address);
|
log.debugf("fetchRecentTransactions", address);
|
||||||
const addrLower = normalizeAddress(address);
|
const addrLower = normalizeAddress(address);
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ jest.mock("../src/shared/balances", () => ({
|
|||||||
refreshBalances: jest.fn(async () => {
|
refreshBalances: jest.fn(async () => {
|
||||||
mockBalanceRefreshAt.push(Date.now());
|
mockBalanceRefreshAt.push(Date.now());
|
||||||
}),
|
}),
|
||||||
|
dropUntrackedTokens: jest.requireActual("../src/shared/balances")
|
||||||
|
.dropUntrackedTokens,
|
||||||
getProvider: jest.fn(() => ({})),
|
getProvider: jest.fn(() => ({})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -142,6 +142,8 @@ function loadWorker(networkId, opts) {
|
|||||||
},
|
},
|
||||||
refreshBalances:
|
refreshBalances:
|
||||||
options.refreshBalances || jest.fn(async () => undefined),
|
options.refreshBalances || jest.fn(async () => undefined),
|
||||||
|
dropUntrackedTokens: jest.requireActual("../src/shared/balances")
|
||||||
|
.dropUntrackedTokens,
|
||||||
}));
|
}));
|
||||||
jest.doMock("../src/shared/phishingDomains", () => ({
|
jest.doMock("../src/shared/phishingDomains", () => ({
|
||||||
isPhishingDomain: () => false,
|
isPhishingDomain: () => false,
|
||||||
@@ -410,7 +412,14 @@ describe("a balance refresh under a network switch", () => {
|
|||||||
const ranOn = [];
|
const ranOn = [];
|
||||||
|
|
||||||
const bg = loadWorker("sepolia", {
|
const bg = loadWorker("sepolia", {
|
||||||
refreshBalances: async (wallets, rpcUrl, explorer, tokens, id) => {
|
refreshBalances: async (
|
||||||
|
wallets,
|
||||||
|
rpcUrl,
|
||||||
|
explorer,
|
||||||
|
useBlockscout,
|
||||||
|
tokens,
|
||||||
|
id,
|
||||||
|
) => {
|
||||||
ranOn.push(id);
|
ranOn.push(id);
|
||||||
reachedNetwork.resolve();
|
reachedNetwork.resolve();
|
||||||
await roundTrip.promise;
|
await roundTrip.promise;
|
||||||
@@ -443,3 +452,61 @@ describe("a balance refresh under a network switch", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("a balance refresh under a change of Use Blockscout", () => {
|
||||||
|
// Turning "Use Blockscout" off drops the tokens the user has not tracked
|
||||||
|
// (src/popup/views/settings.js). A refresh that started while it was on
|
||||||
|
// still saves its balances when it returns, but not those tokens. The same
|
||||||
|
// refresh in the popup is in tests/blockscoutEtherscanSettings.test.js.
|
||||||
|
test("a refresh that returns after Use Blockscout was turned off saves its balances without the untracked token", async () => {
|
||||||
|
const roundTrip = deferred();
|
||||||
|
const started = deferred();
|
||||||
|
const handed = [];
|
||||||
|
|
||||||
|
const bg = loadWorker("mainnet", {
|
||||||
|
refreshBalances: async (
|
||||||
|
wallets,
|
||||||
|
rpcUrl,
|
||||||
|
explorer,
|
||||||
|
useBlockscout,
|
||||||
|
) => {
|
||||||
|
handed.push(useBlockscout);
|
||||||
|
started.resolve();
|
||||||
|
await roundTrip.promise;
|
||||||
|
wallets[0].addresses[0].balance = "7.0";
|
||||||
|
wallets[0].addresses[0].tokenBalances = [
|
||||||
|
{
|
||||||
|
address: "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
name: "Test Token",
|
||||||
|
symbol: "TST",
|
||||||
|
decimals: 18,
|
||||||
|
balance: "5.0",
|
||||||
|
holders: 50000,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
global.chrome.storage.write("autistmask", {
|
||||||
|
...bg.persisted(),
|
||||||
|
useBlockscout: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const refresh = bg.fireBalanceAlarm();
|
||||||
|
await started.promise;
|
||||||
|
|
||||||
|
// The user unticks "Use Blockscout" in Settings while the round trip
|
||||||
|
// is out.
|
||||||
|
global.chrome.storage.write("autistmask", {
|
||||||
|
...bg.persisted(),
|
||||||
|
useBlockscout: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
roundTrip.resolve();
|
||||||
|
await refresh;
|
||||||
|
|
||||||
|
expect(handed).toEqual([true]);
|
||||||
|
const after = bg.persisted();
|
||||||
|
expect(after.wallets[0].addresses[0].balance).toBe("7.0");
|
||||||
|
expect(after.wallets[0].addresses[0].tokenBalances).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ function refresh(signal) {
|
|||||||
wallets,
|
wallets,
|
||||||
RPC_URL,
|
RPC_URL,
|
||||||
EXPLORER_URL,
|
EXPLORER_URL,
|
||||||
|
true,
|
||||||
[TRACKED],
|
[TRACKED],
|
||||||
"mainnet",
|
"mainnet",
|
||||||
signal,
|
signal,
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ test("a refresh that returns after a switch in Settings writes nothing, and the
|
|||||||
|
|
||||||
await page.tick();
|
await page.tick();
|
||||||
|
|
||||||
expect(refreshBalances.mock.lastCall[4]).toBe("sepolia");
|
expect(refreshBalances.mock.lastCall[5]).toBe("sepolia");
|
||||||
expect(page.storage.read("autistmask").lastBalanceRefresh).toBeGreaterThan(
|
expect(page.storage.read("autistmask").lastBalanceRefresh).toBeGreaterThan(
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,786 @@
|
|||||||
|
// Blockscout and Etherscan are sent none of the user's data until the user
|
||||||
|
// turns them on (https://git.eeqj.de/sneak/AutistMask/issues/668). A new
|
||||||
|
// profile has "Use Blockscout" and "Check recipients on Etherscan" both off.
|
||||||
|
// While a setting is off, the code that would ask its service asks nothing.
|
||||||
|
//
|
||||||
|
// The one request made with the setting off is the check behind "Save" beside
|
||||||
|
// the Blockscout URL in Settings. It asks that URL only for `/stats` and sends
|
||||||
|
// none of the user's data.
|
||||||
|
//
|
||||||
|
// The balance refresh and the Etherscan check follow the stored setting, not
|
||||||
|
// the page's copy of it. So a change another page saved applies to them at
|
||||||
|
// once. An open page also takes such a change and draws its screens again.
|
||||||
|
//
|
||||||
|
// Only Blockscout reports the tokens the user has not tracked. While "Use
|
||||||
|
// Blockscout" is off, no address keeps them and no screen lists them.
|
||||||
|
//
|
||||||
|
// The functions that make the requests are driven directly, against a fetch()
|
||||||
|
// that records every URL it is asked for. The screens are driven through the
|
||||||
|
// real popup over a stored profile (tests/support/popupBoot.js). Its fetch()
|
||||||
|
// stub records the same way.
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const { FetchRequest } = require("ethers");
|
||||||
|
const {
|
||||||
|
bootPopup,
|
||||||
|
cleanupPopup,
|
||||||
|
unversionedValidProfile,
|
||||||
|
ADDRESS,
|
||||||
|
TOKEN_ADDRESS,
|
||||||
|
} = require("./support/popupBoot");
|
||||||
|
const { makeStorageStub } = require("./support/storageStub");
|
||||||
|
const {
|
||||||
|
DEFAULT_STATE,
|
||||||
|
normalizePersisted,
|
||||||
|
} = require("../src/shared/persistedState");
|
||||||
|
const {
|
||||||
|
fetchTokenBalances,
|
||||||
|
refreshBalances,
|
||||||
|
} = require("../src/shared/balances");
|
||||||
|
const { fetchRecentTransactions } = require("../src/shared/transactions");
|
||||||
|
const { getFullWarnings } = require("../src/shared/addressWarnings");
|
||||||
|
const { HISTORY_OFF_TEXT, escapeHtml } = require("../src/popup/views/helpers");
|
||||||
|
|
||||||
|
const RPC_URL = "https://rpc.example.invalid";
|
||||||
|
const BLOCKSCOUT = "https://eth.blockscout.com/api/v2";
|
||||||
|
const RECIPIENT = "0x2222222222222222222222222222222222222222";
|
||||||
|
const TX_HASH = "0x" + "ab".repeat(32);
|
||||||
|
const ZERO_WORD = "0x" + "0".repeat(64);
|
||||||
|
|
||||||
|
// USDC, from the bundled list, as Blockscout reports a holding of it.
|
||||||
|
const USDC_ROW = {
|
||||||
|
value: "5000000",
|
||||||
|
token: {
|
||||||
|
type: "ERC-20",
|
||||||
|
address_hash: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
|
||||||
|
symbol: "USDC",
|
||||||
|
name: "USD Coin",
|
||||||
|
decimals: "6",
|
||||||
|
holders_count: "50000",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const realFetch = globalThis.fetch;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupPopup();
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("a new profile", () => {
|
||||||
|
test("has both settings off", () => {
|
||||||
|
expect(DEFAULT_STATE.useBlockscout).toBe(false);
|
||||||
|
expect(DEFAULT_STATE.checkRecipientsOnEtherscan).toBe(false);
|
||||||
|
const loaded = normalizePersisted(undefined);
|
||||||
|
expect(loaded.useBlockscout).toBe(false);
|
||||||
|
expect(loaded.checkRecipientsOnEtherscan).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keeps either setting the user turned on", () => {
|
||||||
|
const loaded = normalizePersisted({
|
||||||
|
useBlockscout: true,
|
||||||
|
checkRecipientsOnEtherscan: true,
|
||||||
|
});
|
||||||
|
expect(loaded.useBlockscout).toBe(true);
|
||||||
|
expect(loaded.checkRecipientsOnEtherscan).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("where each request is made", () => {
|
||||||
|
// Every URL fetch() was asked for, in order.
|
||||||
|
let asked;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
asked = [];
|
||||||
|
globalThis.fetch = jest.fn(async (url) => {
|
||||||
|
asked.push(String(url));
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () =>
|
||||||
|
String(url).endsWith("/token-balances")
|
||||||
|
? [USDC_ROW]
|
||||||
|
: { items: [] },
|
||||||
|
text: async () => "<html><body></body></html>",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
globalThis.fetch = realFetch;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Blockscout's token balances are asked for only while Use Blockscout is on", async () => {
|
||||||
|
expect(
|
||||||
|
await fetchTokenBalances(ADDRESS, BLOCKSCOUT, false, []),
|
||||||
|
).toEqual([]);
|
||||||
|
expect(asked).toEqual([]);
|
||||||
|
|
||||||
|
const rows = await fetchTokenBalances(ADDRESS, BLOCKSCOUT, true, []);
|
||||||
|
expect(rows.map((row) => row.symbol)).toEqual(["USDC"]);
|
||||||
|
expect(asked).toEqual([
|
||||||
|
BLOCKSCOUT + "/addresses/" + ADDRESS + "/token-balances",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the transaction history is asked for only while Use Blockscout is on", async () => {
|
||||||
|
expect(
|
||||||
|
await fetchRecentTransactions(ADDRESS, BLOCKSCOUT, false, "0x1"),
|
||||||
|
).toEqual([]);
|
||||||
|
expect(asked).toEqual([]);
|
||||||
|
|
||||||
|
await fetchRecentTransactions(ADDRESS, BLOCKSCOUT, true, "0x1");
|
||||||
|
expect(asked.sort()).toEqual([
|
||||||
|
BLOCKSCOUT +
|
||||||
|
"/addresses/" +
|
||||||
|
ADDRESS +
|
||||||
|
"/token-transfers?type=ERC-20",
|
||||||
|
BLOCKSCOUT + "/addresses/" + ADDRESS + "/transactions",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the Etherscan label check is made only while the stored profile has Check recipients on Etherscan on", async () => {
|
||||||
|
const provider = {
|
||||||
|
getCode: async () => "0x",
|
||||||
|
getTransactionCount: async () => 1,
|
||||||
|
getBalance: async () => 0n,
|
||||||
|
};
|
||||||
|
const checkWith = async (stored) => {
|
||||||
|
globalThis.chrome = { storage: makeStorageStub(stored) };
|
||||||
|
try {
|
||||||
|
await getFullWarnings(RECIPIENT, provider, { chainId: "0x1" });
|
||||||
|
} finally {
|
||||||
|
delete globalThis.chrome;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await checkWith({});
|
||||||
|
await checkWith({ autistmask: { checkRecipientsOnEtherscan: false } });
|
||||||
|
// Only a stored true is on, as when the profile is loaded.
|
||||||
|
await checkWith({ autistmask: { checkRecipientsOnEtherscan: "true" } });
|
||||||
|
await checkWith({ autistmask: { checkRecipientsOnEtherscan: 42 } });
|
||||||
|
expect(asked).toEqual([]);
|
||||||
|
|
||||||
|
await checkWith({ autistmask: { checkRecipientsOnEtherscan: true } });
|
||||||
|
expect(asked).toEqual(["https://etherscan.io/address/" + RECIPIENT]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("a balance refresh", () => {
|
||||||
|
// The RPC endpoint answers every balance with zero, and every other
|
||||||
|
// call, the ENS reverse lookup's, with no resolver.
|
||||||
|
beforeEach(() => {
|
||||||
|
FetchRequest.registerGetUrl(async (req) => {
|
||||||
|
const body = JSON.parse(Buffer.from(req.body).toString("utf8"));
|
||||||
|
const reply = (r) => ({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: r.id,
|
||||||
|
result: r.method === "eth_getBalance" ? "0x0" : ZERO_WORD,
|
||||||
|
});
|
||||||
|
const answer = Array.isArray(body)
|
||||||
|
? body.map(reply)
|
||||||
|
: reply(body);
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
statusMessage: "OK",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: Buffer.from(JSON.stringify(answer)),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
FetchRequest.registerGetUrl(FetchRequest.createGetUrlFunc());
|
||||||
|
});
|
||||||
|
|
||||||
|
// An address that was refreshed while Use Blockscout was on, and holds
|
||||||
|
// the USDC Blockscout reported then.
|
||||||
|
function addressWithUsdc() {
|
||||||
|
return {
|
||||||
|
address: ADDRESS,
|
||||||
|
tokenBalances: [
|
||||||
|
{
|
||||||
|
address: USDC_ROW.token.address_hash,
|
||||||
|
name: "USD Coin",
|
||||||
|
symbol: "USDC",
|
||||||
|
decimals: 6,
|
||||||
|
balance: "5.0",
|
||||||
|
holders: 50000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh(addr, useBlockscout) {
|
||||||
|
await refreshBalances(
|
||||||
|
[{ addresses: [addr] }],
|
||||||
|
RPC_URL,
|
||||||
|
BLOCKSCOUT,
|
||||||
|
useBlockscout,
|
||||||
|
[],
|
||||||
|
"mainnet",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test("asks Blockscout nothing while Use Blockscout is off, and then lists no token the user has not tracked", async () => {
|
||||||
|
const addr = addressWithUsdc();
|
||||||
|
await refresh(addr, false);
|
||||||
|
expect(asked).toEqual([]);
|
||||||
|
expect(addr.tokenBalances).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("asks Blockscout for the token balances while Use Blockscout is on", async () => {
|
||||||
|
const addr = addressWithUsdc();
|
||||||
|
await refresh(addr, true);
|
||||||
|
expect(asked).toEqual([
|
||||||
|
BLOCKSCOUT + "/addresses/" + ADDRESS + "/token-balances",
|
||||||
|
]);
|
||||||
|
expect(addr.tokenBalances.map((row) => row.symbol)).toEqual([
|
||||||
|
"USDC",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("the popup", () => {
|
||||||
|
// Every URL the booted page handed to fetch().
|
||||||
|
function fetched() {
|
||||||
|
return globalThis.fetch.mock.calls.map((call) => String(call[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function blockscoutOrEtherscan(url) {
|
||||||
|
return url.includes("blockscout") || url.includes("etherscan");
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// The screens below log their failed RPC reads; the stub node in
|
||||||
|
// popupBoot.js answers none.
|
||||||
|
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("hands its balance refresh and Home's history the stored Use Blockscout setting", async () => {
|
||||||
|
await bootPopup(unversionedValidProfile());
|
||||||
|
const {
|
||||||
|
refreshBalances: popupRefresh,
|
||||||
|
} = require("../src/shared/balances");
|
||||||
|
const {
|
||||||
|
fetchRecentTransactions: popupHistory,
|
||||||
|
} = require("../src/shared/transactions");
|
||||||
|
expect(popupRefresh.mock.calls[0][3]).toBe(false);
|
||||||
|
expect(popupHistory.mock.calls[0][2]).toBe(false);
|
||||||
|
cleanupPopup();
|
||||||
|
|
||||||
|
await bootPopup(unversionedValidProfile({ useBlockscout: true }));
|
||||||
|
const {
|
||||||
|
refreshBalances: onRefresh,
|
||||||
|
} = require("../src/shared/balances");
|
||||||
|
const {
|
||||||
|
fetchRecentTransactions: onHistory,
|
||||||
|
} = require("../src/shared/transactions");
|
||||||
|
expect(onRefresh.mock.calls[0][3]).toBe(true);
|
||||||
|
expect(onHistory.mock.calls[0][2]).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Home says why there is no history and only tracked tokens while Use Blockscout is off", async () => {
|
||||||
|
const page = await bootPopup(unversionedValidProfile());
|
||||||
|
expect(page.node("home-tx-list").innerHTML).toContain(
|
||||||
|
escapeHtml(HISTORY_OFF_TEXT),
|
||||||
|
);
|
||||||
|
expect(page.hidden("home-tokens-note")).toBe(false);
|
||||||
|
cleanupPopup();
|
||||||
|
|
||||||
|
const on = await bootPopup(
|
||||||
|
unversionedValidProfile({ useBlockscout: true }),
|
||||||
|
);
|
||||||
|
expect(on.node("home-tx-list").innerHTML).toContain(
|
||||||
|
"No transactions found.",
|
||||||
|
);
|
||||||
|
expect(on.hidden("home-tokens-note")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The transaction screen, as a reopened popup draws it.
|
||||||
|
function onTransactionScreen(extra) {
|
||||||
|
return unversionedValidProfile({
|
||||||
|
currentView: "transaction",
|
||||||
|
viewStack: ["main"],
|
||||||
|
selectedWallet: 0,
|
||||||
|
selectedAddress: 0,
|
||||||
|
viewData: {
|
||||||
|
tx: {
|
||||||
|
hash: TX_HASH,
|
||||||
|
from: ADDRESS,
|
||||||
|
to: RECIPIENT,
|
||||||
|
value: "1.0000",
|
||||||
|
exactValue: "1.0",
|
||||||
|
symbol: "ETH",
|
||||||
|
timestamp: 1790000000,
|
||||||
|
isError: false,
|
||||||
|
direction: "sent",
|
||||||
|
directionLabel: "Sent",
|
||||||
|
contractAddress: null,
|
||||||
|
chainId: "0x1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...extra,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("TransactionDetail asks Blockscout for the transaction only while Use Blockscout is on", async () => {
|
||||||
|
const page = await bootPopup(onTransactionScreen());
|
||||||
|
expect(page.visibleViews()).toEqual(["transaction"]);
|
||||||
|
expect(fetched().filter(blockscoutOrEtherscan)).toEqual([]);
|
||||||
|
expect(page.hidden("tx-detail-blockscout-note")).toBe(false);
|
||||||
|
cleanupPopup();
|
||||||
|
|
||||||
|
const on = await bootPopup(
|
||||||
|
onTransactionScreen({ useBlockscout: true }),
|
||||||
|
);
|
||||||
|
expect(fetched().filter(blockscoutOrEtherscan)).toEqual([
|
||||||
|
BLOCKSCOUT + "/transactions/" + TX_HASH,
|
||||||
|
]);
|
||||||
|
expect(on.hidden("tx-detail-blockscout-note")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The confirmation screen for a send of 0.1 ETH to RECIPIENT, as a
|
||||||
|
// reopened popup draws it.
|
||||||
|
function onConfirmScreen(extra) {
|
||||||
|
return unversionedValidProfile({
|
||||||
|
currentView: "confirm-tx",
|
||||||
|
viewStack: ["main"],
|
||||||
|
selectedWallet: 0,
|
||||||
|
selectedAddress: 0,
|
||||||
|
viewData: {
|
||||||
|
pendingTx: {
|
||||||
|
token: "ETH",
|
||||||
|
from: ADDRESS,
|
||||||
|
to: RECIPIENT,
|
||||||
|
amount: "0.1",
|
||||||
|
chainId: "0x1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...extra,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("ConfirmTx asks Etherscan about the recipient only while Check recipients on Etherscan is on", async () => {
|
||||||
|
const page = await bootPopup(onConfirmScreen());
|
||||||
|
expect(page.visibleViews()).toEqual(["confirm-tx"]);
|
||||||
|
expect(fetched().filter(blockscoutOrEtherscan)).toEqual([]);
|
||||||
|
expect(page.hidden("confirm-etherscan-note")).toBe(false);
|
||||||
|
expect(page.hidden("confirm-etherscan-warning")).toBe(true);
|
||||||
|
cleanupPopup();
|
||||||
|
|
||||||
|
const on = await bootPopup(
|
||||||
|
onConfirmScreen({ checkRecipientsOnEtherscan: true }),
|
||||||
|
);
|
||||||
|
expect(fetched().filter(blockscoutOrEtherscan)).toEqual([
|
||||||
|
"https://etherscan.io/address/" + RECIPIENT,
|
||||||
|
]);
|
||||||
|
expect(on.hidden("confirm-etherscan-note")).toBe(true);
|
||||||
|
expect(on.hidden("confirm-etherscan-warning")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ticks or unticks a checkbox on the page, as the user does.
|
||||||
|
async function setBox(page, id, checked) {
|
||||||
|
const box = page.node(id);
|
||||||
|
box.checked = checked;
|
||||||
|
for (const fn of box.listeners.change) await fn();
|
||||||
|
await page.settle();
|
||||||
|
}
|
||||||
|
|
||||||
|
test("Welcome offers both settings unticked, and ticking one there stores it", async () => {
|
||||||
|
const page = await bootPopup(undefined);
|
||||||
|
expect(page.visibleViews()).toEqual(["welcome"]);
|
||||||
|
expect(page.node("welcome-use-blockscout").checked).toBe(false);
|
||||||
|
expect(page.node("welcome-check-recipients-on-etherscan").checked).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
await setBox(page, "welcome-use-blockscout", true);
|
||||||
|
expect(page.storage.read("autistmask").useBlockscout).toBe(true);
|
||||||
|
await setBox(page, "welcome-check-recipients-on-etherscan", true);
|
||||||
|
expect(page.storage.read("autistmask").checkRecipientsOnEtherscan).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Settings shows both settings unticked, and ticking one there stores it", async () => {
|
||||||
|
const page = await bootPopup(unversionedValidProfile());
|
||||||
|
await page.click("btn-settings");
|
||||||
|
expect(page.node("settings-use-blockscout").checked).toBe(false);
|
||||||
|
expect(
|
||||||
|
page.node("settings-check-recipients-on-etherscan").checked,
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
await setBox(page, "settings-use-blockscout", true);
|
||||||
|
expect(page.storage.read("autistmask").useBlockscout).toBe(true);
|
||||||
|
await setBox(page, "settings-check-recipients-on-etherscan", true);
|
||||||
|
expect(page.storage.read("autistmask").checkRecipientsOnEtherscan).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// A token the user tracks, and one Blockscout reported that the user has
|
||||||
|
// not tracked. Both are written in lower case, as the balance rows' and
|
||||||
|
// the dropdown's token ids are compared below.
|
||||||
|
const TRACKED = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
|
||||||
|
const UNTRACKED = TOKEN_ADDRESS.toLowerCase();
|
||||||
|
const SECOND_ADDRESS = "0x3333333333333333333333333333333333333333";
|
||||||
|
|
||||||
|
function tokenRow(address, symbol) {
|
||||||
|
return {
|
||||||
|
address,
|
||||||
|
name: symbol,
|
||||||
|
symbol,
|
||||||
|
decimals: 18,
|
||||||
|
balance: "5.0",
|
||||||
|
holders: 50000,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddressDetail of the first of two addresses, as a reopened popup draws
|
||||||
|
// it. Both addresses hold the tracked token. While "Use Blockscout" is on,
|
||||||
|
// both hold the untracked one too.
|
||||||
|
function onAddressDetail(useBlockscout) {
|
||||||
|
const profile = unversionedValidProfile({
|
||||||
|
useBlockscout,
|
||||||
|
currentView: "address",
|
||||||
|
viewStack: ["main"],
|
||||||
|
selectedWallet: 0,
|
||||||
|
selectedAddress: 0,
|
||||||
|
trackedTokens: [
|
||||||
|
{ address: TRACKED, name: "TRK", symbol: "TRK", decimals: 18 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const wallet = profile.wallets[0];
|
||||||
|
wallet.nextIndex = 2;
|
||||||
|
wallet.addresses.push({ address: SECOND_ADDRESS, balance: "0.0" });
|
||||||
|
for (const addr of wallet.addresses) {
|
||||||
|
addr.tokenBalances = useBlockscout
|
||||||
|
? [tokenRow(TRACKED, "TRK"), tokenRow(UNTRACKED, "TST")]
|
||||||
|
: [tokenRow(TRACKED, "TRK")];
|
||||||
|
}
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The token ids of the options created after the first `from` elements.
|
||||||
|
// The page stub keeps no children, so this is where the options Send's
|
||||||
|
// dropdown was filled with are read.
|
||||||
|
function optionsSince(page, from) {
|
||||||
|
return page.document.created
|
||||||
|
.slice(from)
|
||||||
|
.filter((el) => el.tagName === "OPTION")
|
||||||
|
.map((el) => el.value.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
test("unticking Use Blockscout from AddressDetail's gear takes the untracked token off AddressDetail, Send's dropdown and Home on Back", async () => {
|
||||||
|
const page = await bootPopup(onAddressDetail(true));
|
||||||
|
expect(page.visibleViews()).toEqual(["address"]);
|
||||||
|
expect(page.node("address-balances").innerHTML).toContain(UNTRACKED);
|
||||||
|
|
||||||
|
await page.click("btn-settings");
|
||||||
|
await setBox(page, "settings-use-blockscout", false);
|
||||||
|
const from = page.document.created.length;
|
||||||
|
await page.click("btn-settings-back");
|
||||||
|
|
||||||
|
expect(page.visibleViews()).toEqual(["address"]);
|
||||||
|
const balances = page.node("address-balances").innerHTML;
|
||||||
|
expect(balances).toContain(TRACKED);
|
||||||
|
expect(balances).not.toContain(UNTRACKED);
|
||||||
|
expect(page.hidden("address-tokens-note")).toBe(false);
|
||||||
|
expect(optionsSince(page, from)).toEqual([TRACKED]);
|
||||||
|
|
||||||
|
await page.click("btn-send");
|
||||||
|
expect(page.visibleViews()).toEqual(["send"]);
|
||||||
|
expect(page.hidden("send-token-dropdown")).toBe(false);
|
||||||
|
expect(page.hidden("send-tokens-note")).toBe(false);
|
||||||
|
|
||||||
|
await page.click("btn-send-back");
|
||||||
|
await page.click("btn-address-back");
|
||||||
|
expect(page.visibleViews()).toEqual(["main"]);
|
||||||
|
const home = page.node("wallet-list").innerHTML;
|
||||||
|
expect(home).toContain(TRACKED);
|
||||||
|
expect(home).not.toContain(UNTRACKED);
|
||||||
|
|
||||||
|
const stored = page.storage.read("autistmask");
|
||||||
|
for (const addr of stored.wallets[0].addresses) {
|
||||||
|
expect(
|
||||||
|
addr.tokenBalances.map((row) => row.address.toLowerCase()),
|
||||||
|
).toEqual([TRACKED]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send's dropdown was filled under the old setting, and Back cannot draw
|
||||||
|
// Send again. So Back lands on the screen under it.
|
||||||
|
test.each([
|
||||||
|
["unticking", true],
|
||||||
|
["ticking", false],
|
||||||
|
])(
|
||||||
|
"%s Use Blockscout from Send's gear lands Back on AddressDetail",
|
||||||
|
async (_how, before) => {
|
||||||
|
const page = await bootPopup(onAddressDetail(before));
|
||||||
|
await page.click("btn-send");
|
||||||
|
expect(page.visibleViews()).toEqual(["send"]);
|
||||||
|
|
||||||
|
await page.click("btn-settings");
|
||||||
|
await setBox(page, "settings-use-blockscout", !before);
|
||||||
|
await page.click("btn-settings-back");
|
||||||
|
expect(page.visibleViews()).toEqual(["address"]);
|
||||||
|
expect(page.hidden("address-tokens-note")).toBe(!before);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test("Send says under its token dropdown that only tracked tokens are listed while Use Blockscout is off", async () => {
|
||||||
|
const off = await bootPopup(onAddressDetail(false));
|
||||||
|
await off.click("btn-send");
|
||||||
|
expect(off.hidden("send-token-dropdown")).toBe(false);
|
||||||
|
expect(off.hidden("send-tokens-note")).toBe(false);
|
||||||
|
cleanupPopup();
|
||||||
|
|
||||||
|
const on = await bootPopup(onAddressDetail(true));
|
||||||
|
await on.click("btn-send");
|
||||||
|
expect(on.hidden("send-token-dropdown")).toBe(false);
|
||||||
|
expect(on.hidden("send-tokens-note")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Send opened from AddressToken shows neither the dropdown nor its line", async () => {
|
||||||
|
const page = await bootPopup({
|
||||||
|
...onAddressDetail(false),
|
||||||
|
currentView: "address-token",
|
||||||
|
viewStack: ["main", "address"],
|
||||||
|
selectedToken: "ETH",
|
||||||
|
});
|
||||||
|
expect(page.visibleViews()).toEqual(["address-token"]);
|
||||||
|
await page.click("btn-address-token-send");
|
||||||
|
expect(page.visibleViews()).toEqual(["send"]);
|
||||||
|
expect(page.hidden("send-token-dropdown")).toBe(true);
|
||||||
|
expect(page.hidden("send-token-static")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("DeleteAddress says only tracked tokens are listed while Use Blockscout is off", async () => {
|
||||||
|
for (const useBlockscout of [false, true]) {
|
||||||
|
const page = await bootPopup(onAddressDetail(useBlockscout));
|
||||||
|
require("../src/popup/views/deleteAddress").show(0, 1);
|
||||||
|
expect(page.visibleViews()).toEqual(["delete-address-confirm"]);
|
||||||
|
expect(page.hidden("delete-address-tokens-note")).toBe(
|
||||||
|
useBlockscout,
|
||||||
|
);
|
||||||
|
cleanupPopup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// The token ids of the first address's balances in the stored profile.
|
||||||
|
function storedTokens(page) {
|
||||||
|
return page.storage
|
||||||
|
.read("autistmask")
|
||||||
|
.wallets[0].addresses[0].tokenBalances.map((row) =>
|
||||||
|
row.address.toLowerCase(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turning the setting off dropped the untracked token. A refresh that
|
||||||
|
// started while it was on still saves its balances when it returns, but
|
||||||
|
// not that token. The same refresh in the background is in
|
||||||
|
// tests/backgroundStateIsolation.test.js.
|
||||||
|
test("a refresh that returns after Use Blockscout was turned off saves its balances without the untracked token", async () => {
|
||||||
|
const page = await bootPopup(onAddressDetail(true));
|
||||||
|
const { refreshBalances: held } = require("../src/shared/balances");
|
||||||
|
let answer;
|
||||||
|
held.mockImplementationOnce(async (wallets) => {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
answer = resolve;
|
||||||
|
});
|
||||||
|
wallets[0].addresses[0].balance = "7.0";
|
||||||
|
wallets[0].addresses[0].tokenBalances = [
|
||||||
|
tokenRow(TRACKED, "TRK"),
|
||||||
|
tokenRow(UNTRACKED, "TST"),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
const ticking = page.tick();
|
||||||
|
await page.settle();
|
||||||
|
|
||||||
|
await page.click("btn-settings");
|
||||||
|
await setBox(page, "settings-use-blockscout", false);
|
||||||
|
answer();
|
||||||
|
await ticking;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
page.storage.read("autistmask").wallets[0].addresses[0].balance,
|
||||||
|
).toBe("7.0");
|
||||||
|
expect(storedTokens(page)).toEqual([TRACKED]);
|
||||||
|
await page.click("btn-settings-back");
|
||||||
|
expect(page.node("address-balances").innerHTML).not.toContain(
|
||||||
|
UNTRACKED,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Another page saves `change` over the stored profile. The browser is not
|
||||||
|
// made to tell this page, so this page sees the change only by reading the
|
||||||
|
// stored profile.
|
||||||
|
function anotherPageSaves(page, change) {
|
||||||
|
page.storage.write("autistmask", {
|
||||||
|
...page.storage.read("autistmask"),
|
||||||
|
...change,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
["off", true, false],
|
||||||
|
["on", false, true],
|
||||||
|
])(
|
||||||
|
"after another page turns Use Blockscout %s, this page's next refresh follows the stored setting and is saved",
|
||||||
|
async (_how, before, after) => {
|
||||||
|
const page = await bootPopup(onAddressDetail(before));
|
||||||
|
const { refreshBalances: held } = require("../src/shared/balances");
|
||||||
|
held.mockImplementationOnce(
|
||||||
|
async (wallets, rpcUrl, blockscoutUrl, useBlockscout) => {
|
||||||
|
const addr = wallets[0].addresses[0];
|
||||||
|
addr.balance = "7.0";
|
||||||
|
addr.tokenBalances = useBlockscout
|
||||||
|
? [tokenRow(TRACKED, "TRK"), tokenRow(UNTRACKED, "TST")]
|
||||||
|
: [tokenRow(TRACKED, "TRK")];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
anotherPageSaves(page, { useBlockscout: after });
|
||||||
|
await page.tick();
|
||||||
|
|
||||||
|
expect(held.mock.calls.at(-1)[3]).toBe(after);
|
||||||
|
expect(
|
||||||
|
page.storage.read("autistmask").wallets[0].addresses[0].balance,
|
||||||
|
).toBe("7.0");
|
||||||
|
expect(storedTokens(page)).toEqual(
|
||||||
|
after ? [TRACKED, UNTRACKED] : [TRACKED],
|
||||||
|
);
|
||||||
|
expect(page.hidden("address-tokens-note")).toBe(after);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
["off", true, false],
|
||||||
|
["on", false, true],
|
||||||
|
])(
|
||||||
|
"after another page turns Check recipients on Etherscan %s, this page's next Etherscan check follows the stored setting",
|
||||||
|
async (_how, before, after) => {
|
||||||
|
const page = await bootPopup(
|
||||||
|
onConfirmScreen({ checkRecipientsOnEtherscan: before }),
|
||||||
|
);
|
||||||
|
anotherPageSaves(page, { checkRecipientsOnEtherscan: after });
|
||||||
|
const from = fetched().length;
|
||||||
|
require("../src/popup/views/confirmTx").show({
|
||||||
|
token: "ETH",
|
||||||
|
from: ADDRESS,
|
||||||
|
to: RECIPIENT,
|
||||||
|
amount: "0.1",
|
||||||
|
chainId: "0x1",
|
||||||
|
});
|
||||||
|
await page.settle();
|
||||||
|
|
||||||
|
// The page also takes the change and draws ConfirmTx again, which
|
||||||
|
// checks once more. So each URL asked for is counted once.
|
||||||
|
const asked = new Set(
|
||||||
|
fetched().slice(from).filter(blockscoutOrEtherscan),
|
||||||
|
);
|
||||||
|
expect([...asked]).toEqual(
|
||||||
|
after ? ["https://etherscan.io/address/" + RECIPIENT] : [],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Another page's save of the whole profile with `change`, which the
|
||||||
|
// browser announces to this page as it announces any write.
|
||||||
|
async function anotherPageAnnounces(page, change) {
|
||||||
|
await page.storage.local.set({
|
||||||
|
autistmask: { ...page.storage.read("autistmask"), ...change },
|
||||||
|
});
|
||||||
|
await page.settle();
|
||||||
|
}
|
||||||
|
|
||||||
|
test("a page open on AddressDetail draws it again when another page turns Use Blockscout off", async () => {
|
||||||
|
const page = await bootPopup(onAddressDetail(true));
|
||||||
|
expect(page.hidden("address-tokens-note")).toBe(true);
|
||||||
|
|
||||||
|
await anotherPageAnnounces(page, { useBlockscout: false });
|
||||||
|
|
||||||
|
expect(page.visibleViews()).toEqual(["address"]);
|
||||||
|
expect(page.hidden("address-tokens-note")).toBe(false);
|
||||||
|
expect(page.node("address-balances").innerHTML).not.toContain(
|
||||||
|
UNTRACKED,
|
||||||
|
);
|
||||||
|
expect(storedTokens(page)).toEqual([TRACKED]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a page open on ConfirmTx draws it again when another page turns Check recipients on Etherscan off", async () => {
|
||||||
|
const page = await bootPopup(
|
||||||
|
onConfirmScreen({ checkRecipientsOnEtherscan: true }),
|
||||||
|
);
|
||||||
|
expect(page.hidden("confirm-etherscan-note")).toBe(true);
|
||||||
|
|
||||||
|
await anotherPageAnnounces(page, { checkRecipientsOnEtherscan: false });
|
||||||
|
|
||||||
|
expect(page.visibleViews()).toEqual(["confirm-tx"]);
|
||||||
|
expect(page.hidden("confirm-etherscan-note")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("the background", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
delete globalThis.chrome;
|
||||||
|
jest.dontMock("../src/shared/balances");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Starts the background over `profile`, fires its balance alarm, and
|
||||||
|
// answers with the Use Blockscout setting its refresh was handed.
|
||||||
|
async function settingTheRefreshIsHanded(profile) {
|
||||||
|
jest.resetModules();
|
||||||
|
let handed;
|
||||||
|
const called = new Promise((resolve) => {
|
||||||
|
handed = resolve;
|
||||||
|
});
|
||||||
|
jest.doMock("../src/shared/balances", () => ({
|
||||||
|
getProvider: () => ({}),
|
||||||
|
refreshBalances: async (...args) => handed(args[3]),
|
||||||
|
dropUntrackedTokens: () => {},
|
||||||
|
}));
|
||||||
|
let onAlarm = null;
|
||||||
|
globalThis.chrome = {
|
||||||
|
storage: makeStorageStub({ autistmask: profile }),
|
||||||
|
alarms: {
|
||||||
|
create: () => {},
|
||||||
|
get: async () => undefined,
|
||||||
|
clear: async () => false,
|
||||||
|
onAlarm: {
|
||||||
|
addListener: (fn) => {
|
||||||
|
onAlarm = fn;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
onMessage: { addListener: () => {} },
|
||||||
|
onConnect: { addListener: () => {} },
|
||||||
|
onInstalled: { addListener: () => {} },
|
||||||
|
onStartup: { addListener: () => {} },
|
||||||
|
getURL: (p) => "chrome-extension://test/" + p,
|
||||||
|
lastError: null,
|
||||||
|
},
|
||||||
|
windows: { onRemoved: { addListener: () => {} } },
|
||||||
|
tabs: { query: () => {}, sendMessage: () => {} },
|
||||||
|
action: { setPopup: () => {} },
|
||||||
|
};
|
||||||
|
require("../src/background/index");
|
||||||
|
const { BALANCE_REFRESH_ALARM } = require("../src/shared/alarms");
|
||||||
|
onAlarm({ name: BALANCE_REFRESH_ALARM });
|
||||||
|
return called;
|
||||||
|
}
|
||||||
|
|
||||||
|
test("hands its balance refresh the stored Use Blockscout setting", async () => {
|
||||||
|
const profile = { ...unversionedValidProfile(), lastBalanceRefresh: 0 };
|
||||||
|
expect(await settingTheRefreshIsHanded(profile)).toBe(false);
|
||||||
|
expect(
|
||||||
|
await settingTheRefreshIsHanded({
|
||||||
|
...profile,
|
||||||
|
useBlockscout: true,
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -73,6 +73,8 @@ function loadColdWorker(networkId, opts) {
|
|||||||
jest.doMock("../src/shared/balances", () => ({
|
jest.doMock("../src/shared/balances", () => ({
|
||||||
getProvider: () => ({}),
|
getProvider: () => ({}),
|
||||||
refreshBalances: options.refreshBalances || jest.fn(async () => {}),
|
refreshBalances: options.refreshBalances || jest.fn(async () => {}),
|
||||||
|
dropUntrackedTokens: jest.requireActual("../src/shared/balances")
|
||||||
|
.dropUntrackedTokens,
|
||||||
}));
|
}));
|
||||||
jest.doMock("../src/shared/phishingDomains", () => ({
|
jest.doMock("../src/shared/phishingDomains", () => ({
|
||||||
isPhishingDomain: () => false,
|
isPhishingDomain: () => false,
|
||||||
|
|||||||
@@ -247,6 +247,7 @@ async function openSend(token, tokenUnits = null) {
|
|||||||
state.wallets,
|
state.wallets,
|
||||||
"https://rpc.example.invalid",
|
"https://rpc.example.invalid",
|
||||||
"https://blockscout.example/api/v2",
|
"https://blockscout.example/api/v2",
|
||||||
|
true,
|
||||||
state.trackedTokens,
|
state.trackedTokens,
|
||||||
"mainnet",
|
"mainnet",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -50,8 +50,14 @@ function explorerPage(title) {
|
|||||||
// The explorer labels no address.
|
// The explorer labels no address.
|
||||||
global.fetch = jest.fn(async () => explorerPage("Etherscan"));
|
global.fetch = jest.fn(async () => explorerPage("Etherscan"));
|
||||||
|
|
||||||
|
// The Etherscan label check is made, so its box can be shown.
|
||||||
const { makeStorageStub } = require("./support/storageStub");
|
const { makeStorageStub } = require("./support/storageStub");
|
||||||
global.chrome = { storage: makeStorageStub(), runtime: { sendMessage() {} } };
|
global.chrome = {
|
||||||
|
storage: makeStorageStub({
|
||||||
|
autistmask: { checkRecipientsOnEtherscan: true },
|
||||||
|
}),
|
||||||
|
runtime: { sendMessage() {} },
|
||||||
|
};
|
||||||
|
|
||||||
// A stub DOM: every id resolves to a recording element.
|
// A stub DOM: every id resolves to a recording element.
|
||||||
const elements = new Map();
|
const elements = new Map();
|
||||||
|
|||||||
@@ -142,13 +142,19 @@ jest.mock("../src/popup/views/txStatus", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// The Etherscan label check is its only caller here: the explorer pages it
|
// The Etherscan label check is its only caller here: the explorer pages it
|
||||||
// asks for are recorded, and nothing is fetched.
|
// asks for are recorded, and nothing is fetched. The stored profile has the
|
||||||
|
// check turned on.
|
||||||
global.fetch = jest.fn(() => {
|
global.fetch = jest.fn(() => {
|
||||||
throw new Error("tests must not perform network requests");
|
throw new Error("tests must not perform network requests");
|
||||||
});
|
});
|
||||||
|
|
||||||
const { makeStorageStub } = require("./support/storageStub");
|
const { makeStorageStub } = require("./support/storageStub");
|
||||||
global.chrome = { storage: makeStorageStub(), runtime: { sendMessage() {} } };
|
global.chrome = {
|
||||||
|
storage: makeStorageStub({
|
||||||
|
autistmask: { checkRecipientsOnEtherscan: true },
|
||||||
|
}),
|
||||||
|
runtime: { sendMessage() {} },
|
||||||
|
};
|
||||||
|
|
||||||
// A stub DOM: every id resolves to a recording element.
|
// A stub DOM: every id resolves to a recording element.
|
||||||
const elements = new Map();
|
const elements = new Map();
|
||||||
|
|||||||
@@ -297,6 +297,8 @@ describe.each([
|
|||||||
state.selectedWallet = 0;
|
state.selectedWallet = 0;
|
||||||
state.selectedAddress = 0;
|
state.selectedAddress = 0;
|
||||||
state.selectedToken = "ETH";
|
state.selectedToken = "ETH";
|
||||||
|
// The history is Blockscout's.
|
||||||
|
state.useBlockscout = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
test("a contract creation's row says so, with no colour dot and no address line", async () => {
|
test("a contract creation's row says so, with no colour dot and no address line", async () => {
|
||||||
|
|||||||
+23
-2
@@ -653,6 +653,12 @@ function traceEnabled(raw) {
|
|||||||
* confirmed receipt instead of null, so a wait screen resolves.
|
* confirmed receipt instead of null, so a wait screen resolves.
|
||||||
* @param {boolean} [opts.failReceiptLookup] answer eth_getTransactionReceipt
|
* @param {boolean} [opts.failReceiptLookup] answer eth_getTransactionReceipt
|
||||||
* with an error, so every receipt lookup fails; read at request time.
|
* with an error, so every receipt lookup fails; read at request time.
|
||||||
|
* @param {string} [opts.etherscanLabel] the label Etherscan's page gives the
|
||||||
|
* address asked about, such as "Fake_Phishing1"; none when unset. Read at
|
||||||
|
* request time.
|
||||||
|
* @param {object[]} [opts.requests] every request this handler sees, from the
|
||||||
|
* popup's pages and the background worker alike, appended in order as
|
||||||
|
* { method, url, fromWorker }.
|
||||||
* @returns {Promise<{waitForServiceWorkerTraffic: (ms: number) =>
|
* @returns {Promise<{waitForServiceWorkerTraffic: (ms: number) =>
|
||||||
* Promise<string|null>}>}
|
* Promise<string|null>}>}
|
||||||
*/
|
*/
|
||||||
@@ -691,6 +697,13 @@ async function installNetworkStubs(ctx, opts) {
|
|||||||
const origin = fromWorker ? "[sw] " : "[page] ";
|
const origin = fromWorker ? "[sw] " : "[page] ";
|
||||||
console.log("# routed " + origin + req.method() + " " + req.url());
|
console.log("# routed " + origin + req.method() + " " + req.url());
|
||||||
}
|
}
|
||||||
|
if (Array.isArray(opts.requests)) {
|
||||||
|
opts.requests.push({
|
||||||
|
method: req.method(),
|
||||||
|
url: req.url(),
|
||||||
|
fromWorker,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// JSON-RPC endpoint (any host): a POST with a JSON-RPC body.
|
// JSON-RPC endpoint (any host): a POST with a JSON-RPC body.
|
||||||
if (req.method() === "POST") {
|
if (req.method() === "POST") {
|
||||||
@@ -762,12 +775,20 @@ async function installNetworkStubs(ctx, opts) {
|
|||||||
return route.fulfill({ status: 204, body: "" });
|
return route.fulfill({ status: 204, body: "" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Best-effort Etherscan address labels: served as an empty page.
|
// Best-effort Etherscan address labels: served as an empty page, or
|
||||||
|
// as a page labelling the address with opts.etherscanLabel.
|
||||||
if (url.hostname.endsWith("etherscan.io")) {
|
if (url.hostname.endsWith("etherscan.io")) {
|
||||||
|
const title = opts.etherscanLabel
|
||||||
|
? "<title>" +
|
||||||
|
opts.etherscanLabel +
|
||||||
|
" | Address: " +
|
||||||
|
p.split("/").pop() +
|
||||||
|
" | Etherscan</title>"
|
||||||
|
: "";
|
||||||
return route.fulfill({
|
return route.fulfill({
|
||||||
status: 200,
|
status: 200,
|
||||||
contentType: "text/html",
|
contentType: "text/html",
|
||||||
body: "<html><body></body></html>",
|
body: "<html><head>" + title + "</head><body></body></html>",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -269,6 +269,65 @@ test("the exact confirmation phrase erases the record and reloads into Welcome (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ------------------ Blockscout and Etherscan are sent no user data (#668)
|
||||||
|
//
|
||||||
|
// A fresh install sends Blockscout and Etherscan none of the user's data until
|
||||||
|
// the user turns on "Use Blockscout" or "Check recipients on Etherscan". One
|
||||||
|
// request is made with the setting off: pressing "Save" beside the Blockscout
|
||||||
|
// URL asks that URL for `/stats`. It carries no user data.
|
||||||
|
//
|
||||||
|
// The route handler in tests/e2e/network.js records every request the
|
||||||
|
// extension makes in env.routeOpts.requests, from its pages and its background
|
||||||
|
// worker. Recording starts when launch() in tests/e2e/harness.js installs that
|
||||||
|
// handler, before the first popup page opens. A request the background worker
|
||||||
|
// makes while it starts up comes earlier and is not recorded.
|
||||||
|
//
|
||||||
|
// Before either setting is changed, these tests drive Home, AddressDetail,
|
||||||
|
// AddressToken and ConfirmTx, and the background's own balance refresh. Then
|
||||||
|
// they turn each setting on and watch its requests start. "Use Blockscout" is
|
||||||
|
// also turned off once and on again, to see a token the user has not tracked
|
||||||
|
// leave every list. TransactionDetail, which also asks Blockscout, is left to
|
||||||
|
// the unit tests in tests/blockscoutEtherscanSettings.test.js.
|
||||||
|
|
||||||
|
// What the three transaction lists say while Use Blockscout is off. Written
|
||||||
|
// out here rather than imported, so the assertion does not compare the popup
|
||||||
|
// with itself.
|
||||||
|
const HISTORY_OFF =
|
||||||
|
'No transaction history is shown, because "Use Blockscout" is off in Settings.';
|
||||||
|
|
||||||
|
// The requests made so far to Blockscout or Etherscan.
|
||||||
|
function explorerRequests(env) {
|
||||||
|
return env.routeOpts.requests.filter(({ url }) => {
|
||||||
|
const host = new URL(url).hostname;
|
||||||
|
return host.endsWith("blockscout.com") || host.endsWith("etherscan.io");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Waits until the list `listId` shows the line saying "Use Blockscout" is off.
|
||||||
|
async function waitForHistoryOff(page, listId) {
|
||||||
|
await page.waitForFunction(
|
||||||
|
([id, want]) => document.getElementById(id).textContent.trim() === want,
|
||||||
|
[listId, HISTORY_OFF],
|
||||||
|
{ timeout: 15000 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test("Welcome offers Use Blockscout and Check recipients on Etherscan, both unticked (#668)", async (env) => {
|
||||||
|
await visible(env.page, "#view-welcome");
|
||||||
|
for (const id of [
|
||||||
|
"welcome-use-blockscout",
|
||||||
|
"welcome-check-recipients-on-etherscan",
|
||||||
|
]) {
|
||||||
|
const st = await controlState(env.page, id);
|
||||||
|
assert(st !== null, "Welcome has no #" + id);
|
||||||
|
assert(
|
||||||
|
st.tag === "input" && st.type === "checkbox",
|
||||||
|
"#" + id + " is a <" + st.tag + " type=" + st.type + ">",
|
||||||
|
);
|
||||||
|
assert(st.checked === false, "#" + id + " is ticked on a new install");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// The address scan that follows creating the wallet is held, so the popup
|
// The address scan that follows creating the wallet is held, so the popup
|
||||||
// reloads while it is in flight. As for the reloads mid-refresh further down,
|
// reloads while it is in flight. As for the reloads mid-refresh further down,
|
||||||
// the assertion for that is the harness's own: a console.error from the page
|
// the assertion for that is the harness's own: a console.error from the page
|
||||||
@@ -296,6 +355,261 @@ test("wallet creation through the UI reaches the main view, and reloading mid-sc
|
|||||||
assert(addrCount > 0, "no addresses rendered in the wallet list");
|
assert(addrCount > 0, "no addresses rendered in the wallet list");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("with neither setting changed, Home, AddressDetail, AddressToken and ConfirmTx say what is off (#668)", async (env) => {
|
||||||
|
// Funded, so Send leads to the confirmation screen.
|
||||||
|
env.routeOpts.ethBalanceWei = toHexWei(FUNDED_ETH_WEI);
|
||||||
|
try {
|
||||||
|
await visible(env.page, "#view-main");
|
||||||
|
await waitForHistoryOff(env.page, "home-tx-list");
|
||||||
|
await visible(env.page, "#home-tokens-note");
|
||||||
|
await env.page.waitForFunction(
|
||||||
|
(want) =>
|
||||||
|
document
|
||||||
|
.getElementById("wallet-list")
|
||||||
|
.textContent.includes(want),
|
||||||
|
quantity(FUNDED_ETH_WEI),
|
||||||
|
{ timeout: 60000 },
|
||||||
|
);
|
||||||
|
|
||||||
|
await openAddressDetail(env.page);
|
||||||
|
await waitForHistoryOff(env.page, "tx-list");
|
||||||
|
await visible(env.page, "#address-tokens-note");
|
||||||
|
|
||||||
|
await env.page.locator('#address-balances [data-token="ETH"]').click();
|
||||||
|
await visible(env.page, "#view-address-token");
|
||||||
|
await waitForHistoryOff(env.page, "address-token-tx-list");
|
||||||
|
await env.page.click("#btn-address-token-back");
|
||||||
|
await visible(env.page, "#view-address");
|
||||||
|
|
||||||
|
await goToConfirm(env.page, {
|
||||||
|
token: "ETH",
|
||||||
|
balance: FUNDED_ETH_TEXT + " ETH",
|
||||||
|
amount: COMFORTABLE_AMOUNT,
|
||||||
|
});
|
||||||
|
await waitForEstimate(env.page);
|
||||||
|
await visible(env.page, "#confirm-etherscan-note");
|
||||||
|
assert(
|
||||||
|
!(await env.page.isVisible("#confirm-etherscan-warning")),
|
||||||
|
"ConfirmTx keeps the Etherscan warning's place while no Etherscan check is made",
|
||||||
|
);
|
||||||
|
await backToAddress(env.page);
|
||||||
|
} finally {
|
||||||
|
env.routeOpts.ethBalanceWei = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// The background refreshes balances on its alarm, once a minute. It skips a
|
||||||
|
// refresh when the stored record says one ran in the last 30 seconds, and the
|
||||||
|
// open popup's own refresh runs every 10. So the popup is closed first. The
|
||||||
|
// background worker then writes that record back to say no refresh has run, as
|
||||||
|
// another extension page saving it would.
|
||||||
|
test("with neither setting changed, nothing has reached Blockscout or Etherscan, the background's refresh included (#668)", async (env) => {
|
||||||
|
await waitForPersisted(
|
||||||
|
env.page,
|
||||||
|
"currentView",
|
||||||
|
"address",
|
||||||
|
"before the popup is closed",
|
||||||
|
);
|
||||||
|
await env.page.close();
|
||||||
|
const [worker] = env.ctx.serviceWorkers();
|
||||||
|
await worker.evaluate(
|
||||||
|
() =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
chrome.storage.local.get("autistmask", (r) => {
|
||||||
|
const record = r.autistmask;
|
||||||
|
record.lastBalanceRefresh = 0;
|
||||||
|
chrome.storage.local.set({ autistmask: record }, resolve);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const before = env.routeOpts.requests.length;
|
||||||
|
const deadline = Date.now() + 75000;
|
||||||
|
while (
|
||||||
|
!env.routeOpts.requests
|
||||||
|
.slice(before)
|
||||||
|
.some((r) => r.fromWorker && r.method === "POST")
|
||||||
|
) {
|
||||||
|
assert(
|
||||||
|
Date.now() < deadline,
|
||||||
|
"the background's balance refresh never asked the RPC endpoint",
|
||||||
|
);
|
||||||
|
await sleep(250);
|
||||||
|
}
|
||||||
|
// Its RPC reads and any Blockscout request start together.
|
||||||
|
await sleep(3000);
|
||||||
|
|
||||||
|
const seen = explorerRequests(env);
|
||||||
|
assert(
|
||||||
|
seen.length === 0,
|
||||||
|
"a fresh install asked Blockscout or Etherscan: " +
|
||||||
|
JSON.stringify(seen),
|
||||||
|
);
|
||||||
|
// The watch saw the popup's own traffic as well as the worker's.
|
||||||
|
assert(
|
||||||
|
env.routeOpts.requests.some((r) => !r.fromWorker),
|
||||||
|
"no request from the popup was recorded",
|
||||||
|
);
|
||||||
|
// It reopens on the address screen the test above left it on.
|
||||||
|
env.page = await openPopup(env.ctx, env.popupUrl);
|
||||||
|
await visible(env.page, "#view-address");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ticked from AddressDetail's gear, so Back lands on the screen that said the
|
||||||
|
// setting is off.
|
||||||
|
test("turning on Use Blockscout from AddressDetail's gear asks Blockscout for that screen's history on Back, then for the tokens (#668)", async (env) => {
|
||||||
|
await openAddressDetail(env.page);
|
||||||
|
await waitForHistoryOff(env.page, "tx-list");
|
||||||
|
await openSettings(env.page);
|
||||||
|
await env.page.check("#settings-use-blockscout");
|
||||||
|
await waitForPersisted(env.page, "useBlockscout", true, "once ticked");
|
||||||
|
const historyAsked = env.page.waitForRequest(
|
||||||
|
(r) =>
|
||||||
|
r.url().includes("blockscout.com") &&
|
||||||
|
r.url().endsWith("/transactions"),
|
||||||
|
);
|
||||||
|
const tokensAsked = env.page.waitForRequest(
|
||||||
|
(r) => r.url().endsWith("/token-balances"),
|
||||||
|
{ timeout: 30000 },
|
||||||
|
);
|
||||||
|
await env.page.click("#btn-settings-back");
|
||||||
|
await visible(env.page, "#view-address");
|
||||||
|
await historyAsked;
|
||||||
|
await env.page.waitForFunction(
|
||||||
|
() =>
|
||||||
|
document
|
||||||
|
.getElementById("tx-list")
|
||||||
|
.textContent.includes("No transactions found."),
|
||||||
|
null,
|
||||||
|
{ timeout: 15000 },
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
!(await env.page.isVisible("#address-tokens-note")),
|
||||||
|
"AddressDetail still says only tracked tokens are listed",
|
||||||
|
);
|
||||||
|
|
||||||
|
await goHome(env.page);
|
||||||
|
await tokensAsked;
|
||||||
|
await env.page.waitForFunction(
|
||||||
|
() =>
|
||||||
|
document
|
||||||
|
.getElementById("home-tx-list")
|
||||||
|
.textContent.includes("No transactions found."),
|
||||||
|
null,
|
||||||
|
{ timeout: 15000 },
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
!(await env.page.isVisible("#home-tokens-note")),
|
||||||
|
"Home still says only tracked tokens are listed",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unticked from AddressDetail's gear while the address holds the stub token,
|
||||||
|
// which the user has not tracked and only Blockscout reports. Ticked again at
|
||||||
|
// the end, so the rest of the run has Blockscout as before.
|
||||||
|
test("turning off Use Blockscout from AddressDetail's gear takes an untracked token off AddressDetail, Send and Home on Back (#668)", async (env) => {
|
||||||
|
env.routeOpts.ethBalanceWei = toHexWei(FUNDED_ETH_WEI);
|
||||||
|
env.routeOpts.seedTokenBalance = true;
|
||||||
|
const tokenRow = '[data-token="' + STUB_TOKEN.address + '"]';
|
||||||
|
try {
|
||||||
|
await settleOnMain(env, { ethWei: FUNDED_ETH_WEI, expectToken: true });
|
||||||
|
await openAddressDetail(env.page);
|
||||||
|
await visible(env.page, "#address-balances " + tokenRow);
|
||||||
|
|
||||||
|
await openSettings(env.page);
|
||||||
|
await env.page.uncheck("#settings-use-blockscout");
|
||||||
|
await waitForPersisted(
|
||||||
|
env.page,
|
||||||
|
"useBlockscout",
|
||||||
|
false,
|
||||||
|
"once unticked",
|
||||||
|
);
|
||||||
|
await env.page.click("#btn-settings-back");
|
||||||
|
await visible(env.page, "#view-address");
|
||||||
|
await visible(env.page, "#address-tokens-note");
|
||||||
|
assert(
|
||||||
|
(await env.page
|
||||||
|
.locator("#address-balances " + tokenRow)
|
||||||
|
.count()) === 0,
|
||||||
|
"AddressDetail still lists the untracked token",
|
||||||
|
);
|
||||||
|
|
||||||
|
await env.page.click("#btn-send");
|
||||||
|
await visible(env.page, "#view-send");
|
||||||
|
await visible(env.page, "#send-tokens-note");
|
||||||
|
const offered = await env.page.$$eval("#send-token option", (opts) =>
|
||||||
|
opts.map((opt) => opt.value.toLowerCase()),
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
!offered.includes(STUB_TOKEN.address),
|
||||||
|
"Send's dropdown still offers the untracked token",
|
||||||
|
);
|
||||||
|
await env.page.click("#btn-send-back");
|
||||||
|
await visible(env.page, "#view-address");
|
||||||
|
|
||||||
|
await goHome(env.page);
|
||||||
|
await visible(env.page, "#home-tokens-note");
|
||||||
|
assert(
|
||||||
|
(await env.page.locator("#wallet-list " + tokenRow).count()) === 0,
|
||||||
|
"Home still lists the untracked token",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await openSettings(env.page);
|
||||||
|
await env.page.check("#settings-use-blockscout");
|
||||||
|
await waitForPersisted(env.page, "useBlockscout", true, "ticked again");
|
||||||
|
await env.page.click("#btn-settings-back");
|
||||||
|
env.routeOpts.ethBalanceWei = null;
|
||||||
|
env.routeOpts.seedTokenBalance = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ticked from ConfirmTx's gear, so Back lands on the screen that said no
|
||||||
|
// Etherscan check was made. Left on for the rest of the run, so the later
|
||||||
|
// confirmation screens are driven with the check made.
|
||||||
|
test("turning on Check recipients on Etherscan from ConfirmTx's gear has ConfirmTx ask Etherscan on Back and show its label (#668)", async (env) => {
|
||||||
|
env.routeOpts.ethBalanceWei = toHexWei(FUNDED_ETH_WEI);
|
||||||
|
env.routeOpts.etherscanLabel = "Fake_Phishing668";
|
||||||
|
try {
|
||||||
|
await settleOnMain(env, { ethWei: FUNDED_ETH_WEI });
|
||||||
|
await goToConfirm(env.page, {
|
||||||
|
token: "ETH",
|
||||||
|
balance: FUNDED_ETH_TEXT + " ETH",
|
||||||
|
amount: COMFORTABLE_AMOUNT,
|
||||||
|
});
|
||||||
|
await visible(env.page, "#confirm-etherscan-note");
|
||||||
|
|
||||||
|
await openSettings(env.page);
|
||||||
|
await env.page.check("#settings-check-recipients-on-etherscan");
|
||||||
|
await waitForPersisted(
|
||||||
|
env.page,
|
||||||
|
"checkRecipientsOnEtherscan",
|
||||||
|
true,
|
||||||
|
"once ticked",
|
||||||
|
);
|
||||||
|
const before = env.routeOpts.requests.length;
|
||||||
|
await env.page.click("#btn-settings-back");
|
||||||
|
await visible(env.page, "#view-confirm-tx");
|
||||||
|
await visible(env.page, "#confirm-etherscan-warning", 30000);
|
||||||
|
assert(
|
||||||
|
!(await env.page.isVisible("#confirm-etherscan-note")),
|
||||||
|
"ConfirmTx says no Etherscan check was made, with the check on",
|
||||||
|
);
|
||||||
|
const asked = env.routeOpts.requests
|
||||||
|
.slice(before)
|
||||||
|
.map((r) => r.url.toLowerCase())
|
||||||
|
.filter((url) => url.includes("etherscan.io"));
|
||||||
|
assert(
|
||||||
|
asked.length === 1 &&
|
||||||
|
asked[0] ===
|
||||||
|
"https://etherscan.io/address/" + STUB_COUNTERPARTY,
|
||||||
|
"ConfirmTx asked Etherscan for " + JSON.stringify(asked),
|
||||||
|
);
|
||||||
|
await backToAddress(env.page);
|
||||||
|
} finally {
|
||||||
|
env.routeOpts.ethBalanceWei = null;
|
||||||
|
env.routeOpts.etherscanLabel = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("add token screen opens from address detail (#150)", async (env) => {
|
test("add token screen opens from address detail (#150)", async (env) => {
|
||||||
await openAddressDetail(env.page);
|
await openAddressDetail(env.page);
|
||||||
await env.page.click("#btn-add-token");
|
await env.page.click("#btn-add-token");
|
||||||
@@ -5258,10 +5572,17 @@ async function main() {
|
|||||||
seedReceipt: false,
|
seedReceipt: false,
|
||||||
// Whether eth_getTransactionReceipt fails instead of answering (#315).
|
// Whether eth_getTransactionReceipt fails instead of answering (#315).
|
||||||
failReceiptLookup: false,
|
failReceiptLookup: false,
|
||||||
|
// The label Etherscan's page gives an address, when it gives one
|
||||||
|
// (#668).
|
||||||
|
etherscanLabel: null,
|
||||||
// Every raw signed transaction handed to eth_sendRawTransaction, in
|
// Every raw signed transaction handed to eth_sendRawTransaction, in
|
||||||
// order. The dApp transaction round trip asserts against these bytes
|
// order. The dApp transaction round trip asserts against these bytes
|
||||||
// rather than against anything the extension reported about them.
|
// rather than against anything the extension reported about them.
|
||||||
broadcastTransactions: [],
|
broadcastTransactions: [],
|
||||||
|
// Every request the extension makes, in order, from when launch()
|
||||||
|
// installs the route handler, before the first popup page opens
|
||||||
|
// (#668).
|
||||||
|
requests: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
let session;
|
let session;
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
// ENS lookups ask nothing but the RPC endpoint. A resolver can answer that a
|
||||||
|
// name is resolved off-chain, naming a web address to fetch the answer from.
|
||||||
|
// ethers fetches it unless the provider has `disableCcipRead` set. That request
|
||||||
|
// would carry the name to a server other than the RPC endpoint
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/668). With the flag set, such a
|
||||||
|
// name does not resolve.
|
||||||
|
//
|
||||||
|
// The provider getProvider() builds runs against a fake JSON-RPC node. Its
|
||||||
|
// answers go through ethers' own request hook, which also records the address
|
||||||
|
// of every request ethers makes, the off-chain fetch included.
|
||||||
|
|
||||||
|
const { FetchRequest, Interface } = require("ethers");
|
||||||
|
const { getProvider } = require("../src/shared/balances");
|
||||||
|
|
||||||
|
const RPC_URL = "https://rpc.example.invalid";
|
||||||
|
const RESOLVER = "0x5555555555555555555555555555555555555555";
|
||||||
|
const USER = "0x1111111111111111111111111111111111111111";
|
||||||
|
const NAME = "offchain.eth";
|
||||||
|
|
||||||
|
// The ENS registry's resolver(), and the resolver's functions ethers calls:
|
||||||
|
//
|
||||||
|
// - the registry names RESOLVER for every name
|
||||||
|
// - the resolver says it resolves names through resolve()
|
||||||
|
// - it gives NAME as the reverse record of any address
|
||||||
|
// - it answers resolve() with the OffchainLookup error, which says the name
|
||||||
|
// is resolved off-chain
|
||||||
|
const ens = new Interface([
|
||||||
|
"function resolver(bytes32) view returns (address)",
|
||||||
|
"function supportsInterface(bytes4) view returns (bool)",
|
||||||
|
"function name(bytes32) view returns (string)",
|
||||||
|
"function resolve(bytes, bytes) view returns (bytes)",
|
||||||
|
"error OffchainLookup(address sender, string[] urls, bytes callData, bytes4 callbackFunction, bytes extraData)",
|
||||||
|
]);
|
||||||
|
|
||||||
|
let asked;
|
||||||
|
|
||||||
|
// One JSON-RPC answer. Anything but a call to one of the functions above is
|
||||||
|
// refused.
|
||||||
|
function reply(req) {
|
||||||
|
const envelope = { jsonrpc: "2.0", id: req.id };
|
||||||
|
const call =
|
||||||
|
req.method === "eth_call" ? ens.parseTransaction(req.params[0]) : null;
|
||||||
|
if (call === null) {
|
||||||
|
return {
|
||||||
|
...envelope,
|
||||||
|
error: { code: -32601, message: "method not found" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (call.name === "resolve") {
|
||||||
|
const offchain = ens.encodeErrorResult("OffchainLookup", [
|
||||||
|
RESOLVER,
|
||||||
|
["https://gateway.example.invalid/{sender}/{data}.json"],
|
||||||
|
"0x1234",
|
||||||
|
"0x12345678",
|
||||||
|
"0x",
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
...envelope,
|
||||||
|
error: { code: 3, message: "execution reverted", data: offchain },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const answers = {
|
||||||
|
resolver: [RESOLVER],
|
||||||
|
supportsInterface: [true],
|
||||||
|
name: [NAME],
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...envelope,
|
||||||
|
result: ens.encodeFunctionResult(call.name, answers[call.name]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
asked = [];
|
||||||
|
FetchRequest.registerGetUrl(async (req) => {
|
||||||
|
asked.push(req.url);
|
||||||
|
// Any other address is the off-chain fetch, answered as not found.
|
||||||
|
if (req.url !== RPC_URL) {
|
||||||
|
return {
|
||||||
|
statusCode: 404,
|
||||||
|
statusMessage: "Not Found",
|
||||||
|
headers: {},
|
||||||
|
body: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const body = JSON.parse(Buffer.from(req.body).toString("utf8"));
|
||||||
|
const answer = Array.isArray(body) ? body.map(reply) : reply(body);
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
statusMessage: "OK",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: Buffer.from(JSON.stringify(answer)),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
FetchRequest.registerGetUrl(FetchRequest.createGetUrlFunc());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a name typed into Send that is resolved off-chain does not resolve, and only the RPC endpoint is asked", async () => {
|
||||||
|
const provider = getProvider(RPC_URL, "mainnet");
|
||||||
|
const [outcome] = await Promise.allSettled([provider.resolveName(NAME)]);
|
||||||
|
expect(asked.filter((url) => url !== RPC_URL)).toEqual([]);
|
||||||
|
expect(outcome).toEqual({ status: "fulfilled", value: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the reverse lookup of a user's address whose name is resolved off-chain finds no name, and only the RPC endpoint is asked", async () => {
|
||||||
|
const provider = getProvider(RPC_URL, "mainnet");
|
||||||
|
const [outcome] = await Promise.allSettled([provider.lookupAddress(USER)]);
|
||||||
|
expect(asked.filter((url) => url !== RPC_URL)).toEqual([]);
|
||||||
|
expect(outcome).toEqual({ status: "fulfilled", value: null });
|
||||||
|
});
|
||||||
@@ -114,6 +114,7 @@ async function fetchOnto(items, trackedTokens = []) {
|
|||||||
const balances = await fetchTokenBalances(
|
const balances = await fetchTokenBalances(
|
||||||
HOLDER,
|
HOLDER,
|
||||||
BLOCKSCOUT,
|
BLOCKSCOUT,
|
||||||
|
true,
|
||||||
trackedTokens,
|
trackedTokens,
|
||||||
);
|
);
|
||||||
state.trackedTokens = trackedTokens;
|
state.trackedTokens = trackedTokens;
|
||||||
|
|||||||
@@ -150,19 +150,23 @@ describe("the balance-list spam gate", () => {
|
|||||||
|
|
||||||
test("a token with plenty of reported holders is listed", async () => {
|
test("a token with plenty of reported holders is listed", async () => {
|
||||||
respondWith([item()]);
|
respondWith([item()]);
|
||||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
|
||||||
expect(balances).toHaveLength(1);
|
expect(balances).toHaveLength(1);
|
||||||
expect(balances[0].holders).toBe(50000);
|
expect(balances[0].holders).toBe(50000);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("a token reporting zero holders is still excluded", async () => {
|
test("a token reporting zero holders is still excluded", async () => {
|
||||||
respondWith([item({ token: { holders_count: "0" } })]);
|
respondWith([item({ token: { holders_count: "0" } })]);
|
||||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
|
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
|
||||||
|
[],
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("an unknown holder count does not admit an unvouched token", async () => {
|
test("an unknown holder count does not admit an unvouched token", async () => {
|
||||||
respondWith([item({ token: { holders_count: null } })]);
|
respondWith([item({ token: { holders_count: null } })]);
|
||||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
|
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
|
||||||
|
[],
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// The path that reaches the send selector and the history filter: a token
|
// The path that reaches the send selector and the history filter: a token
|
||||||
@@ -171,7 +175,7 @@ describe("the balance-list spam gate", () => {
|
|||||||
// zero that would then hide it downstream.
|
// zero that would then hide it downstream.
|
||||||
test("a tracked token with an unknown count is listed with holders null", async () => {
|
test("a tracked token with an unknown count is listed with holders null", async () => {
|
||||||
respondWith([item({ token: { holders_count: undefined } })]);
|
respondWith([item({ token: { holders_count: undefined } })]);
|
||||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, [
|
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [
|
||||||
{ address: NOVEL_TOKEN.toUpperCase() },
|
{ address: NOVEL_TOKEN.toUpperCase() },
|
||||||
]);
|
]);
|
||||||
expect(balances).toHaveLength(1);
|
expect(balances).toHaveLength(1);
|
||||||
@@ -188,7 +192,7 @@ describe("the balance-list spam gate", () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
|
||||||
expect(balances).toHaveLength(1);
|
expect(balances).toHaveLength(1);
|
||||||
expect(balances[0].holders).toBeNull();
|
expect(balances[0].holders).toBeNull();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -436,6 +436,8 @@ describe.each([
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("the transaction detail screen's type line and fee", async () => {
|
test("the transaction detail screen's type line and fee", async () => {
|
||||||
|
// The fee is Blockscout's.
|
||||||
|
state.useBlockscout = true;
|
||||||
debugFetch.mockImplementationOnce(async () => ({
|
debugFetch.mockImplementationOnce(async () => ({
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|||||||
@@ -286,8 +286,12 @@ describe("a malformed fraudContracts", () => {
|
|||||||
function stubSendDocument() {
|
function stubSendDocument() {
|
||||||
const select = { innerHTML: "", children: [] };
|
const select = { innerHTML: "", children: [] };
|
||||||
select.appendChild = (child) => select.children.push(child);
|
select.appendChild = (child) => select.children.push(child);
|
||||||
|
const elements = {
|
||||||
|
"send-token": select,
|
||||||
|
"send-tokens-note": { classList: { toggle: () => {} } },
|
||||||
|
};
|
||||||
globalThis.document = {
|
globalThis.document = {
|
||||||
getElementById: (id) => (id === "send-token" ? select : null),
|
getElementById: (id) => elements[id] || null,
|
||||||
createElement: () => ({ value: "", textContent: "" }),
|
createElement: () => ({ value: "", textContent: "" }),
|
||||||
};
|
};
|
||||||
return select;
|
return select;
|
||||||
|
|||||||
@@ -629,6 +629,14 @@ const CONTRACT = [
|
|||||||
// the falsy answer is also the DEFAULT_STATE default.
|
// the falsy answer is also the DEFAULT_STATE default.
|
||||||
falsy: [0],
|
falsy: [0],
|
||||||
})),
|
})),
|
||||||
|
// Each lets a request reach another service, so anything but true is off.
|
||||||
|
...["useBlockscout", "checkRecipientsOnEtherscan"].map((field) => ({
|
||||||
|
field,
|
||||||
|
kind: KIND.SCALAR,
|
||||||
|
hostile: [42, "notabool", { a: 1 }],
|
||||||
|
floorOnly: [0, null, "true"],
|
||||||
|
holds: (v) => v === false,
|
||||||
|
})),
|
||||||
];
|
];
|
||||||
|
|
||||||
function siteMapHolds(v) {
|
function siteMapHolds(v) {
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ test("a flash message's timer does not fire after cleanupPopup()", async () => {
|
|||||||
|
|
||||||
// A boot onto a screen that sends a request no mocked module answers: that
|
// A boot onto a screen that sends a request no mocked module answers: that
|
||||||
// request reaches the boot's fetch() stub, never the internet
|
// request reaches the boot's fetch() stub, never the internet
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/559).
|
// (https://git.eeqj.de/sneak/AutistMask/issues/559). TransactionDetail asks
|
||||||
|
// Blockscout only while "Use Blockscout" is on. ConfirmTx asks Etherscan only
|
||||||
|
// while "Check recipients on Etherscan" is on. So the profile turns both on.
|
||||||
function restoringOnto(view, viewData) {
|
function restoringOnto(view, viewData) {
|
||||||
return unversionedValidProfile({
|
return unversionedValidProfile({
|
||||||
currentView: view,
|
currentView: view,
|
||||||
@@ -52,6 +54,8 @@ function restoringOnto(view, viewData) {
|
|||||||
selectedAddress: 0,
|
selectedAddress: 0,
|
||||||
viewStack: ["main"],
|
viewStack: ["main"],
|
||||||
viewData,
|
viewData,
|
||||||
|
useBlockscout: true,
|
||||||
|
checkRecipientsOnEtherscan: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ test("the popup open on AddressDetail shows the new network and its balances", a
|
|||||||
// Its next refresh runs on Sepolia, and what it fetched is stored.
|
// Its next refresh runs on Sepolia, and what it fetched is stored.
|
||||||
await page.tick();
|
await page.tick();
|
||||||
const { refreshBalances } = require("../src/shared/balances");
|
const { refreshBalances } = require("../src/shared/balances");
|
||||||
expect(refreshBalances.mock.lastCall[4]).toBe("sepolia");
|
expect(refreshBalances.mock.lastCall[5]).toBe("sepolia");
|
||||||
const stored = page.storage.read("autistmask");
|
const stored = page.storage.read("autistmask");
|
||||||
expect(stored.networkId).toBe("sepolia");
|
expect(stored.networkId).toBe("sepolia");
|
||||||
expect(stored.lastBalanceRefresh).toBeGreaterThan(0);
|
expect(stored.lastBalanceRefresh).toBeGreaterThan(0);
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ test("the balance refresh", async () => {
|
|||||||
wallets,
|
wallets,
|
||||||
RPC_URL,
|
RPC_URL,
|
||||||
"https://explorer.example.invalid/api/v2",
|
"https://explorer.example.invalid/api/v2",
|
||||||
|
true,
|
||||||
[],
|
[],
|
||||||
"mainnet",
|
"mainnet",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ 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",
|
||||||
|
true,
|
||||||
state.trackedTokens,
|
state.trackedTokens,
|
||||||
"mainnet",
|
"mainnet",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ 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",
|
||||||
|
true,
|
||||||
state.trackedTokens,
|
state.trackedTokens,
|
||||||
"mainnet",
|
"mainnet",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,8 +27,12 @@ let select;
|
|||||||
function installStubDocument() {
|
function installStubDocument() {
|
||||||
select = { innerHTML: "", children: [] };
|
select = { innerHTML: "", children: [] };
|
||||||
select.appendChild = (child) => select.children.push(child);
|
select.appendChild = (child) => select.children.push(child);
|
||||||
|
const elements = {
|
||||||
|
"send-token": select,
|
||||||
|
"send-tokens-note": { classList: { toggle: () => {} } },
|
||||||
|
};
|
||||||
globalThis.document = {
|
globalThis.document = {
|
||||||
getElementById: (id) => (id === "send-token" ? select : null),
|
getElementById: (id) => elements[id] || null,
|
||||||
createElement: () => ({ value: "", textContent: "" }),
|
createElement: () => ({ value: "", textContent: "" }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -523,13 +523,15 @@ describe("a garbage value in a field the gate does not check", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("a malformed tokenBalances entry is dropped, and the wallet and its address survive", async () => {
|
test("a malformed tokenBalances entry is dropped, and the wallet and its address survive", async () => {
|
||||||
const env = await bootPopup(
|
// "Use Blockscout" is on, or the boot's balance refresh drops the
|
||||||
profileWithTokenBalances([
|
// well-formed entry too: the user has not tracked its token.
|
||||||
null,
|
const profile = profileWithTokenBalances([
|
||||||
42,
|
null,
|
||||||
{ address: TOKEN_ADDRESS, symbol: "AAA", balance: "2.0" },
|
42,
|
||||||
]),
|
{ address: TOKEN_ADDRESS, symbol: "AAA", balance: "2.0" },
|
||||||
);
|
]);
|
||||||
|
profile.useBlockscout = true;
|
||||||
|
const env = await bootPopup(profile);
|
||||||
|
|
||||||
const stored = env.storage.read("autistmask");
|
const stored = env.storage.read("autistmask");
|
||||||
expect(stored.wallets[0].encryptedSecret).toBe("encrypted-secret-1");
|
expect(stored.wallets[0].encryptedSecret).toBe("encrypted-secret-1");
|
||||||
|
|||||||
@@ -283,6 +283,8 @@ async function bootPopup(stored, options) {
|
|||||||
jest.doMock("../../src/shared/balances", () => ({
|
jest.doMock("../../src/shared/balances", () => ({
|
||||||
fetchTokenBalances: jest.fn(async () => []),
|
fetchTokenBalances: jest.fn(async () => []),
|
||||||
refreshBalances: jest.fn(async () => {}),
|
refreshBalances: jest.fn(async () => {}),
|
||||||
|
dropUntrackedTokens: jest.requireActual("../../src/shared/balances")
|
||||||
|
.dropUntrackedTokens,
|
||||||
lookupTokenInfo: jest.fn(async () => null),
|
lookupTokenInfo: jest.fn(async () => null),
|
||||||
getProvider: () => ({}),
|
getProvider: () => ({}),
|
||||||
scanForAddresses: jest.fn(async () => ({
|
scanForAddresses: jest.fn(async () => ({
|
||||||
|
|||||||
+28
-11
@@ -485,8 +485,12 @@ describe("surface 2: the Send token selector", () => {
|
|||||||
function render(tokenBalances) {
|
function render(tokenBalances) {
|
||||||
select = { innerHTML: "", children: [] };
|
select = { innerHTML: "", children: [] };
|
||||||
select.appendChild = (child) => select.children.push(child);
|
select.appendChild = (child) => select.children.push(child);
|
||||||
|
const elements = {
|
||||||
|
"send-token": select,
|
||||||
|
"send-tokens-note": { classList: { toggle: () => {} } },
|
||||||
|
};
|
||||||
globalThis.document = {
|
globalThis.document = {
|
||||||
getElementById: (id) => (id === "send-token" ? select : null),
|
getElementById: (id) => elements[id] || null,
|
||||||
createElement: () => ({ value: "", textContent: "" }),
|
createElement: () => ({ value: "", textContent: "" }),
|
||||||
};
|
};
|
||||||
renderSendTokenSelect({
|
renderSendTokenSelect({
|
||||||
@@ -582,12 +586,14 @@ describe("surface 3: the balance list", () => {
|
|||||||
// 1,000-holder floor and was listed as a holding named ETH.
|
// 1,000-holder floor and was listed as a holding named ETH.
|
||||||
test("a fake ETH token clearing the holder floor is filtered", async () => {
|
test("a fake ETH token clearing the holder floor is filtered", async () => {
|
||||||
respondWith([fakeEthItem()]);
|
respondWith([fakeEthItem()]);
|
||||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
|
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
|
||||||
|
[],
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("tracking the fake token manually does not admit it either", async () => {
|
test("tracking the fake token manually does not admit it either", async () => {
|
||||||
respondWith([fakeEthItem({ holders_count: "0" })]);
|
respondWith([fakeEthItem({ holders_count: "0" })]);
|
||||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, [
|
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [
|
||||||
{ address: FAKE_ETH_CONTRACT },
|
{ address: FAKE_ETH_CONTRACT },
|
||||||
]);
|
]);
|
||||||
expect(balances).toEqual([]);
|
expect(balances).toEqual([]);
|
||||||
@@ -597,12 +603,16 @@ describe("surface 3: the balance list", () => {
|
|||||||
// their belief about what they own, and it renders the symbol into HTML.
|
// their belief about what they own, and it renders the symbol into HTML.
|
||||||
test("a padded fake ETH token is filtered too", async () => {
|
test("a padded fake ETH token is filtered too", async () => {
|
||||||
respondWith([fakeEthItem({ symbol: " ETH " })]);
|
respondWith([fakeEthItem({ symbol: " ETH " })]);
|
||||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
|
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
|
||||||
|
[],
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("a fake ETH token padded with a no-break space is filtered", async () => {
|
test("a fake ETH token padded with a no-break space is filtered", async () => {
|
||||||
respondWith([fakeEthItem({ symbol: NBSP + "ETH" + NBSP })]);
|
respondWith([fakeEthItem({ symbol: NBSP + "ETH" + NBSP })]);
|
||||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
|
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
|
||||||
|
[],
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// The false-positive direction on the surface that matters most: a real
|
// The false-positive direction on the surface that matters most: a real
|
||||||
@@ -617,7 +627,7 @@ describe("surface 3: the balance list", () => {
|
|||||||
decimals: "6",
|
decimals: "6",
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
|
||||||
expect(balances).toHaveLength(1);
|
expect(balances).toHaveLength(1);
|
||||||
expect(balances[0].symbol).toBe(" USDC ");
|
expect(balances[0].symbol).toBe(" USDC ");
|
||||||
});
|
});
|
||||||
@@ -631,7 +641,7 @@ describe("surface 3: the balance list", () => {
|
|||||||
decimals: "6",
|
decimals: "6",
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
|
||||||
expect(balances).toHaveLength(1);
|
expect(balances).toHaveLength(1);
|
||||||
expect(balances[0].symbol).toBe("USDC");
|
expect(balances[0].symbol).toBe("USDC");
|
||||||
});
|
});
|
||||||
@@ -642,7 +652,9 @@ describe("surface 3: the balance list", () => {
|
|||||||
// before the symbol rule is consulted.
|
// before the symbol rule is consulted.
|
||||||
test("a non-ERC-20 row claiming ETH never reaches the symbol rule", async () => {
|
test("a non-ERC-20 row claiming ETH never reaches the symbol rule", async () => {
|
||||||
respondWith([fakeEthItem({ type: "ERC-721" })]);
|
respondWith([fakeEthItem({ type: "ERC-721" })]);
|
||||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
|
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
|
||||||
|
[],
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// The adjacent finding from the same review as issue #260: the type gate
|
// The adjacent finding from the same review as issue #260: the type gate
|
||||||
@@ -660,16 +672,20 @@ describe("surface 3: the balance list", () => {
|
|||||||
decimals: "6",
|
decimals: "6",
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
|
||||||
expect(balances).toHaveLength(1);
|
expect(balances).toHaveLength(1);
|
||||||
expect(balances[0].symbol).toBe("USDC");
|
expect(balances[0].symbol).toBe("USDC");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("case insensitivity does not admit another token type", async () => {
|
test("case insensitivity does not admit another token type", async () => {
|
||||||
respondWith([fakeEthItem({ type: "erc-721" })]);
|
respondWith([fakeEthItem({ type: "erc-721" })]);
|
||||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
|
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
|
||||||
|
[],
|
||||||
|
);
|
||||||
respondWith([fakeEthItem({ type: "ERC-20-EXTRA" })]);
|
respondWith([fakeEthItem({ type: "ERC-20-EXTRA" })]);
|
||||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
|
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
|
||||||
|
[],
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// The money test: the user holds real ETH and has been airdropped a fake
|
// The money test: the user holds real ETH and has been airdropped a fake
|
||||||
@@ -682,6 +698,7 @@ describe("surface 3: the balance list", () => {
|
|||||||
[{ addresses: [addr] }],
|
[{ addresses: [addr] }],
|
||||||
"https://rpc.example.invalid",
|
"https://rpc.example.invalid",
|
||||||
BLOCKSCOUT,
|
BLOCKSCOUT,
|
||||||
|
true,
|
||||||
[],
|
[],
|
||||||
"mainnet",
|
"mainnet",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -134,6 +134,8 @@ beforeEach(() => {
|
|||||||
state.selectedWallet = 0;
|
state.selectedWallet = 0;
|
||||||
state.selectedAddress = 0;
|
state.selectedAddress = 0;
|
||||||
state.selectedToken = "ETH";
|
state.selectedToken = "ETH";
|
||||||
|
// The history is Blockscout's.
|
||||||
|
state.useBlockscout = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.each([
|
describe.each([
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ beforeEach(() => {
|
|||||||
globalThis.document = makeDocument();
|
globalThis.document = makeDocument();
|
||||||
globalThis.window = { location: { search: "" } };
|
globalThis.window = { location: { search: "" } };
|
||||||
state.blockscoutUrl = EXPLORER;
|
state.blockscoutUrl = EXPLORER;
|
||||||
|
state.useBlockscout = true;
|
||||||
state.networkEndpoints = {};
|
state.networkEndpoints = {};
|
||||||
state.wallets = [
|
state.wallets = [
|
||||||
{
|
{
|
||||||
@@ -177,6 +178,7 @@ test("a token transfer's decoded details name the token contract and the amount
|
|||||||
const [row] = await fetchRecentTransactions(
|
const [row] = await fetchRecentTransactions(
|
||||||
HOLDER,
|
HOLDER,
|
||||||
EXPLORER,
|
EXPLORER,
|
||||||
|
true,
|
||||||
NETWORKS.mainnet.chainId,
|
NETWORKS.mainnet.chainId,
|
||||||
);
|
);
|
||||||
expect(row.to).toBe(RECIPIENT);
|
expect(row.to).toBe(RECIPIENT);
|
||||||
@@ -209,6 +211,7 @@ test("with mainnet active, a Sepolia transaction is read from Sepolia's Blocksco
|
|||||||
const [row] = await fetchRecentTransactions(
|
const [row] = await fetchRecentTransactions(
|
||||||
HOLDER,
|
HOLDER,
|
||||||
EXPLORER,
|
EXPLORER,
|
||||||
|
true,
|
||||||
NETWORKS.sepolia.chainId,
|
NETWORKS.sepolia.chainId,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -237,6 +240,7 @@ test("with mainnet active and no Blockscout remembered for Sepolia, a Sepolia tr
|
|||||||
const [row] = await fetchRecentTransactions(
|
const [row] = await fetchRecentTransactions(
|
||||||
HOLDER,
|
HOLDER,
|
||||||
EXPLORER,
|
EXPLORER,
|
||||||
|
true,
|
||||||
NETWORKS.sepolia.chainId,
|
NETWORKS.sepolia.chainId,
|
||||||
);
|
);
|
||||||
mockAsked.length = 0;
|
mockAsked.length = 0;
|
||||||
@@ -258,6 +262,7 @@ async function showSepoliaTransferWithMainnetActive() {
|
|||||||
const [row] = await fetchRecentTransactions(
|
const [row] = await fetchRecentTransactions(
|
||||||
HOLDER,
|
HOLDER,
|
||||||
EXPLORER,
|
EXPLORER,
|
||||||
|
true,
|
||||||
NETWORKS.sepolia.chainId,
|
NETWORKS.sepolia.chainId,
|
||||||
);
|
);
|
||||||
transactionDetail.show(row);
|
transactionDetail.show(row);
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ async function refresh(addresses, trackedTokens) {
|
|||||||
[{ addresses }],
|
[{ addresses }],
|
||||||
RPC_URL,
|
RPC_URL,
|
||||||
EXPLORER_URL,
|
EXPLORER_URL,
|
||||||
|
true,
|
||||||
trackedTokens,
|
trackedTokens,
|
||||||
"mainnet",
|
"mainnet",
|
||||||
new AbortController().signal,
|
new AbortController().signal,
|
||||||
|
|||||||
@@ -287,6 +287,7 @@ describe("Back after a switch from the Settings gear", () => {
|
|||||||
currentView: "confirm-tx",
|
currentView: "confirm-tx",
|
||||||
selectedWallet: 0,
|
selectedWallet: 0,
|
||||||
selectedAddress: 0,
|
selectedAddress: 0,
|
||||||
|
checkRecipientsOnEtherscan: true,
|
||||||
viewData: {
|
viewData: {
|
||||||
pendingTx: {
|
pendingTx: {
|
||||||
token: "ETH",
|
token: "ETH",
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ beforeEach(() => {
|
|||||||
state.selectedWallet = 0;
|
state.selectedWallet = 0;
|
||||||
state.selectedAddress = 0;
|
state.selectedAddress = 0;
|
||||||
state.selectedToken = "ETH";
|
state.selectedToken = "ETH";
|
||||||
|
// The history is Blockscout's.
|
||||||
|
state.useBlockscout = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -1219,7 +1219,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
|
|
||||||
test("queries only the two Blockscout endpoints for the address", async () => {
|
test("queries only the two Blockscout endpoints for the address", async () => {
|
||||||
respondWith([], []);
|
respondWith([], []);
|
||||||
await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
await fetchRecentTransactions(VICTIM, BLOCKSCOUT, true, "0x1");
|
||||||
expect(debugFetch).toHaveBeenCalledTimes(2);
|
expect(debugFetch).toHaveBeenCalledTimes(2);
|
||||||
const urls = debugFetch.mock.calls.map((c) => c[0]);
|
const urls = debugFetch.mock.calls.map((c) => c[0]);
|
||||||
expect(urls).toContain(
|
expect(urls).toContain(
|
||||||
@@ -1283,7 +1283,12 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
const txs = await fetchRecentTransactions(
|
||||||
|
VICTIM,
|
||||||
|
BLOCKSCOUT,
|
||||||
|
true,
|
||||||
|
"0x1",
|
||||||
|
);
|
||||||
expect(txs).toHaveLength(1);
|
expect(txs).toHaveLength(1);
|
||||||
const merged = txs[0];
|
const merged = txs[0];
|
||||||
// The received leg (the swap output) supplies the display amount.
|
// The received leg (the swap output) supplies the display amount.
|
||||||
@@ -1320,7 +1325,12 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
const txs = await fetchRecentTransactions(
|
||||||
|
VICTIM,
|
||||||
|
BLOCKSCOUT,
|
||||||
|
true,
|
||||||
|
"0x1",
|
||||||
|
);
|
||||||
expect(txs).toHaveLength(1);
|
expect(txs).toHaveLength(1);
|
||||||
expect(txs[0].symbol).toBe("USDC");
|
expect(txs[0].symbol).toBe("USDC");
|
||||||
expect(txs[0].value).toBe("1500.5000");
|
expect(txs[0].value).toBe("1500.5000");
|
||||||
@@ -1346,7 +1356,12 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
});
|
});
|
||||||
respondWith([], [leg("1000000"), leg("2000000")]);
|
respondWith([], [leg("1000000"), leg("2000000")]);
|
||||||
|
|
||||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
const txs = await fetchRecentTransactions(
|
||||||
|
VICTIM,
|
||||||
|
BLOCKSCOUT,
|
||||||
|
true,
|
||||||
|
"0x1",
|
||||||
|
);
|
||||||
expect(txs).toHaveLength(1);
|
expect(txs).toHaveLength(1);
|
||||||
// Keyed by hash plus contract, so the later leg wins.
|
// Keyed by hash plus contract, so the later leg wins.
|
||||||
expect(txs[0].exactValue).toBe("2.0");
|
expect(txs[0].exactValue).toBe("2.0");
|
||||||
@@ -1386,7 +1401,12 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
const txs = await fetchRecentTransactions(
|
||||||
|
VICTIM,
|
||||||
|
BLOCKSCOUT,
|
||||||
|
true,
|
||||||
|
"0x1",
|
||||||
|
);
|
||||||
expect(txs.map((t) => t.symbol).sort()).toEqual(["USDC", "WETH"]);
|
expect(txs.map((t) => t.symbol).sort()).toEqual(["USDC", "WETH"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1427,7 +1447,12 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
const txs = await fetchRecentTransactions(
|
||||||
|
VICTIM,
|
||||||
|
BLOCKSCOUT,
|
||||||
|
true,
|
||||||
|
"0x1",
|
||||||
|
);
|
||||||
expect(txs).toHaveLength(1);
|
expect(txs).toHaveLength(1);
|
||||||
expect(txs[0].symbol).toBe("USDC");
|
expect(txs[0].symbol).toBe("USDC");
|
||||||
expect(txs[0].exactValue).toBe("1.0");
|
expect(txs[0].exactValue).toBe("1.0");
|
||||||
@@ -1453,7 +1478,13 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
});
|
});
|
||||||
respondWith([item(6), item(8), item(7)], []);
|
respondWith([item(6), item(8), item(7)], []);
|
||||||
|
|
||||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1", 2);
|
const txs = await fetchRecentTransactions(
|
||||||
|
VICTIM,
|
||||||
|
BLOCKSCOUT,
|
||||||
|
true,
|
||||||
|
"0x1",
|
||||||
|
2,
|
||||||
|
);
|
||||||
expect(txs.map((t) => t.blockNumber)).toEqual([21000008, 21000007]);
|
expect(txs.map((t) => t.blockNumber)).toEqual([21000008, 21000007]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1479,6 +1510,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
const sepolia = await fetchRecentTransactions(
|
const sepolia = await fetchRecentTransactions(
|
||||||
VICTIM,
|
VICTIM,
|
||||||
BLOCKSCOUT,
|
BLOCKSCOUT,
|
||||||
|
true,
|
||||||
"0xaa36a7",
|
"0xaa36a7",
|
||||||
);
|
);
|
||||||
expect(sepolia[0].symbol).toBe("SepoliaETH");
|
expect(sepolia[0].symbol).toBe("SepoliaETH");
|
||||||
@@ -1487,6 +1519,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
const mainnet = await fetchRecentTransactions(
|
const mainnet = await fetchRecentTransactions(
|
||||||
VICTIM,
|
VICTIM,
|
||||||
BLOCKSCOUT,
|
BLOCKSCOUT,
|
||||||
|
true,
|
||||||
"0x1",
|
"0x1",
|
||||||
);
|
);
|
||||||
expect(mainnet[0].symbol).toBe("ETH");
|
expect(mainnet[0].symbol).toBe("ETH");
|
||||||
@@ -1513,7 +1546,12 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
const txs = await fetchRecentTransactions(
|
||||||
|
VICTIM,
|
||||||
|
BLOCKSCOUT,
|
||||||
|
true,
|
||||||
|
"0x1",
|
||||||
|
);
|
||||||
expect(txs).toHaveLength(1);
|
expect(txs).toHaveLength(1);
|
||||||
expect(txs[0].contractAddress).toBe(FAKE_ETH_CONTRACT);
|
expect(txs[0].contractAddress).toBe(FAKE_ETH_CONTRACT);
|
||||||
expect(txs[0].holders).toBe(0);
|
expect(txs[0].holders).toBe(0);
|
||||||
@@ -1555,6 +1593,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
const txs = await fetchRecentTransactions(
|
const txs = await fetchRecentTransactions(
|
||||||
VICTIM,
|
VICTIM,
|
||||||
BLOCKSCOUT,
|
BLOCKSCOUT,
|
||||||
|
true,
|
||||||
"0x1",
|
"0x1",
|
||||||
);
|
);
|
||||||
expect(txs[0].holders).toBeNull();
|
expect(txs[0].holders).toBeNull();
|
||||||
@@ -1565,6 +1604,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
const txs = await fetchRecentTransactions(
|
const txs = await fetchRecentTransactions(
|
||||||
VICTIM,
|
VICTIM,
|
||||||
BLOCKSCOUT,
|
BLOCKSCOUT,
|
||||||
|
true,
|
||||||
"0x1",
|
"0x1",
|
||||||
);
|
);
|
||||||
expect(txs[0].holders).toBeNull();
|
expect(txs[0].holders).toBeNull();
|
||||||
@@ -1575,6 +1615,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
const txs = await fetchRecentTransactions(
|
const txs = await fetchRecentTransactions(
|
||||||
VICTIM,
|
VICTIM,
|
||||||
BLOCKSCOUT,
|
BLOCKSCOUT,
|
||||||
|
true,
|
||||||
"0x1",
|
"0x1",
|
||||||
);
|
);
|
||||||
expect(filterTransactions(txs, filters()).transactions).toEqual(
|
expect(filterTransactions(txs, filters()).transactions).toEqual(
|
||||||
@@ -1591,6 +1632,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
const txs = await fetchRecentTransactions(
|
const txs = await fetchRecentTransactions(
|
||||||
VICTIM,
|
VICTIM,
|
||||||
BLOCKSCOUT,
|
BLOCKSCOUT,
|
||||||
|
true,
|
||||||
"0x1",
|
"0x1",
|
||||||
);
|
);
|
||||||
expect(txs[0].holders).toBe(0);
|
expect(txs[0].holders).toBe(0);
|
||||||
@@ -1608,7 +1650,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
await expect(
|
await expect(
|
||||||
fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1"),
|
fetchRecentTransactions(VICTIM, BLOCKSCOUT, true, "0x1"),
|
||||||
).resolves.toEqual([]);
|
).resolves.toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3456,6 +3456,7 @@ async function showDetail(data) {
|
|||||||
globalThis.window = { location: { search: "" } };
|
globalThis.window = { location: { search: "" } };
|
||||||
state.networkId = "mainnet";
|
state.networkId = "mainnet";
|
||||||
state.blockscoutUrl = "https://explorer.example/api/v2";
|
state.blockscoutUrl = "https://explorer.example/api/v2";
|
||||||
|
state.useBlockscout = true;
|
||||||
state.viewData = {};
|
state.viewData = {};
|
||||||
state.viewStack = [];
|
state.viewStack = [];
|
||||||
state.currentView = null;
|
state.currentView = null;
|
||||||
|
|||||||
@@ -208,6 +208,7 @@ async function showDetail(row, activeNetworkId) {
|
|||||||
globalThis.window = { location: { search: "" } };
|
globalThis.window = { location: { search: "" } };
|
||||||
state.networkId = activeNetworkId;
|
state.networkId = activeNetworkId;
|
||||||
state.blockscoutUrl = "https://explorer.example/api/v2";
|
state.blockscoutUrl = "https://explorer.example/api/v2";
|
||||||
|
state.useBlockscout = true;
|
||||||
state.viewData = {};
|
state.viewData = {};
|
||||||
state.viewStack = [];
|
state.viewStack = [];
|
||||||
state.currentView = null;
|
state.currentView = null;
|
||||||
@@ -287,6 +288,7 @@ async function historyRow(method, sentTo, chainId) {
|
|||||||
const rows = await fetchRecentTransactions(
|
const rows = await fetchRecentTransactions(
|
||||||
RECIPIENT,
|
RECIPIENT,
|
||||||
"https://explorer.example/api/v2",
|
"https://explorer.example/api/v2",
|
||||||
|
true,
|
||||||
chainId,
|
chainId,
|
||||||
);
|
);
|
||||||
return rows[0];
|
return rows[0];
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ async function fetchOnto(items) {
|
|||||||
statusText: "OK",
|
statusText: "OK",
|
||||||
json: async () => items,
|
json: async () => items,
|
||||||
}));
|
}));
|
||||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
|
||||||
state.wallets = [
|
state.wallets = [
|
||||||
{
|
{
|
||||||
name: "Wallet 1",
|
name: "Wallet 1",
|
||||||
@@ -197,14 +197,14 @@ async function fetchOntoBoth(itemsA, itemsB) {
|
|||||||
statusText: "OK",
|
statusText: "OK",
|
||||||
json: async () => itemsA,
|
json: async () => itemsA,
|
||||||
}));
|
}));
|
||||||
const a = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
const a = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
|
||||||
debugFetch.mockImplementation(async () => ({
|
debugFetch.mockImplementation(async () => ({
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
statusText: "OK",
|
statusText: "OK",
|
||||||
json: async () => itemsB,
|
json: async () => itemsB,
|
||||||
}));
|
}));
|
||||||
const b = await fetchTokenBalances(SECOND_HOLDER, BLOCKSCOUT, []);
|
const b = await fetchTokenBalances(SECOND_HOLDER, BLOCKSCOUT, true, []);
|
||||||
state.wallets = [
|
state.wallets = [
|
||||||
{
|
{
|
||||||
name: "Wallet 1",
|
name: "Wallet 1",
|
||||||
|
|||||||
Reference in New Issue
Block a user