Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7f0e241ff |
@@ -14,14 +14,18 @@ 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
|
don't implement any crypto, and don't send user-specific data anywhere but a
|
||||||
(user-configurable) Ethereum RPC endpoint (which defaults to a public node). The
|
(user-configurable) Ethereum RPC endpoint (which defaults to a public node)
|
||||||
extension contacts three user-configurable services: the configured RPC node for
|
unless the user turns on one of the two lookups below. Out of the box the
|
||||||
blockchain interactions, a public CoinDesk API (no API key) for realtime price
|
extension contacts the configured RPC node for blockchain interactions and a
|
||||||
information, and a Blockscout block-explorer API for transaction history and
|
public CoinDesk API (no API key) for realtime price information, which is sent
|
||||||
token balances. It also performs best-effort Etherscan address label lookups
|
token symbols and nothing else. Two more services are sent none of the user's
|
||||||
during transaction confirmation. A community-maintained phishing domain
|
data until the user turns them on, on the Welcome screen or in Settings: a
|
||||||
blocklist is built into the extension at build time and checked locally; nothing
|
Blockscout block-explorer API for transaction history, the tokens the user has
|
||||||
is fetched for it at runtime.
|
not tracked and transaction details ("Use Blockscout"), and best-effort
|
||||||
|
Etherscan address label lookups during transaction confirmation ("Check
|
||||||
|
recipients on Etherscan"). 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 +426,24 @@ 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 route handler
|
||||||
|
records every request the extension makes, from its pages and its background
|
||||||
|
worker, from when the harness installs it, before the first popup page opens; a
|
||||||
|
request the background worker makes while it starts up, before then, is not
|
||||||
|
recorded. Welcome must offer "Use Blockscout" and "Check recipients on
|
||||||
|
Etherscan" unticked. With neither changed, Home, AddressDetail, AddressToken and
|
||||||
|
ConfirmTx must show the lines saying what is off; the popup is then closed until
|
||||||
|
the background has run a balance refresh of its own, and not one recorded
|
||||||
|
request may have gone to Blockscout or Etherscan. "Use Blockscout" is then
|
||||||
|
ticked from AddressDetail's gear: on "Back", AddressDetail must ask Blockscout
|
||||||
|
for its history and no longer say it is off, and Blockscout must then be asked
|
||||||
|
for the token balances. "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 stay on for the rest of the run.
|
||||||
|
TransactionDetail with "Use Blockscout" off is not driven here, since 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 +867,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 +1411,28 @@ 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. The refresh asks Blockscout only while the user
|
||||||
nonzero, and any other ERC-20 when its balance is nonzero and it is in the
|
has "Use Blockscout" on. While it is off, as it is until the user turns it on, a
|
||||||
bundled known-token list or has 1,000 or more holders. A token claiming a symbol
|
tracked token's entry has no holder count and no `decimals` of Blockscout's, so
|
||||||
from the bundled list from any other contract address is always dropped, tracked
|
its balance is stated at the scale the bundled list or the tracked token
|
||||||
or not, and so is any token claiming a symbol that belongs to the native asset
|
records, and no other token is listed, those from earlier refreshes included. A
|
||||||
and therefore has no legitimate contract at all (`"ETH"`, and every network's
|
tracked token is shown when its balance is nonzero, and any other ERC-20 when
|
||||||
`nativeCurrency`, such as `"SepoliaETH"`, on every network). That filter is
|
its balance is nonzero and it is in the bundled known-token list or has 1,000 or
|
||||||
unconditional — the "Hide tokens with fewer than 1,000 holders" setting governs
|
more holders. A token claiming a symbol from the bundled list from any other
|
||||||
the transaction history and the send-screen token selector, not this list. A
|
contract address is always dropped, tracked or not, and so is any token claiming
|
||||||
token's holder count is unknown when the explorer reports none, or reports
|
a symbol that belongs to the native asset and therefore has no legitimate
|
||||||
anything other than a whole number written in digits alone, such as `1,000` or
|
contract at all (`"ETH"`, and every network's `nativeCurrency`, such as
|
||||||
`1e3` (`parseHoldersCount()` in `src/shared/holders.js`). This list does not
|
`"SepoliaETH"`, on every network). That filter is unconditional — the "Hide
|
||||||
take an unknown count as 1,000 or more, so such a token is shown only when it is
|
tokens with fewer than 1,000 holders" setting governs the transaction history
|
||||||
on the bundled list or tracked. Every nonzero holding of a token shown is stored
|
and the send-screen token selector, not this list. A token's holder count is
|
||||||
exactly, however small, but a holding below 0.000001 is left out of the balance
|
unknown when the explorer reports none, or reports anything other than a whole
|
||||||
lists, the send-screen token selector, the address total and the remove-address
|
number written in digits alone, such as `1,000` or `1e3` (`parseHoldersCount()`
|
||||||
warning (`isBelowOneMillionth()` in `src/shared/amountDisplay.js`). The Send and
|
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.
|
||||||
@@ -1576,7 +1604,10 @@ renders as it goes, and `viewRouter.js` records every screen that reaches
|
|||||||
more — rendering it a second time would re-fetch and overwrite what it holds,
|
more — rendering it a second time would re-fetch and overwrite what it holds,
|
||||||
such as an edit typed into Settings and not yet saved. Home is the one screen
|
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, so "Back" renders the screen it lands on under the new setting; the
|
||||||
|
stack is left as it is.
|
||||||
|
|
||||||
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
|
||||||
@@ -1619,8 +1650,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 saying that the wallet sends the user's data only to the Ethereum
|
||||||
|
RPC endpoint unless one of the two settings in it is turned on, here or
|
||||||
|
later in Settings: the "Use Blockscout" and "Check recipients on
|
||||||
|
Etherscan" checkboxes, the same two as in Settings, each with its sentence
|
||||||
|
on what it sends and to whom. 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 +1679,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 +1688,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, and this list and those two show the one 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
|
||||||
@@ -1716,10 +1760,13 @@ view would leave a wallet one click from deletion.
|
|||||||
- Full address (color dot, etherscan link, tap to copy)
|
- Full address (color dot, etherscan link, tap to copy)
|
||||||
- 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, the line Home has under its wallet list
|
||||||
|
follows it
|
||||||
- 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), or the line
|
||||||
|
Home's list shows while "Use Blockscout" is off
|
||||||
- **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)
|
||||||
@@ -1788,8 +1835,10 @@ view would leave a wallet one click from deletion.
|
|||||||
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: the explorer did not
|
||||||
report a readable one, or the token is not in the balance list
|
report a readable one, "Use Blockscout" is off, or the token is not in the
|
||||||
- Token-filtered transaction list (only this token's transfers)
|
balance list
|
||||||
|
- Token-filtered transaction list (only this token's transfers), or the line
|
||||||
|
Home's list shows while "Use Blockscout" is off
|
||||||
- **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)
|
||||||
@@ -1875,7 +1924,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 and a line in its place says "This address was not checked on
|
||||||
|
Etherscan, because "Check recipients on Etherscan" is off in Settings."
|
||||||
|
Which of the two is there is known 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
|
||||||
@@ -1921,8 +1975,8 @@ view would leave a wallet one click from deletion.
|
|||||||
are that network's, so a testnet transaction shows none, its From, To and
|
are that network's, so a testnet transaction shows none, its From, To and
|
||||||
token contract links open that network's explorer, the balance read, the
|
token contract links open that network's explorer, the balance read, the
|
||||||
fee estimate and the contract and never-sent recipient checks ask that
|
fee estimate and the contract and never-sent recipient checks ask that
|
||||||
network's RPC endpoint, and the Etherscan label check asks that network's
|
network's RPC endpoint, and the Etherscan label check, when it is made,
|
||||||
explorer
|
asks that network's explorer
|
||||||
- "Sign & Send" (correct password) when the gas limit, a fee per gas or the
|
- "Sign & Send" (correct password) when the gas limit, a fee per gas or the
|
||||||
total fee the RPC endpoint fills in at signing is over its bound in
|
total fee the RPC endpoint fills in at signing is over its bound in
|
||||||
`assertWithinCeilings()` (`src/shared/approvalVerify.js`) → nothing is
|
`assertWithinCeilings()` (`src/shared/approvalVerify.js`) → nothing is
|
||||||
@@ -2041,7 +2095,12 @@ 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. They are read only
|
||||||
|
while "Use Blockscout" is on. The screen can still be shown with it off, when
|
||||||
|
the user opens Settings from it, turns the setting off and presses Back; then
|
||||||
|
Blockscout is not asked, and 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 +2183,18 @@ 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: a "Use Blockscout" checkbox, unticked until the user ticks
|
||||||
`/stats` before being saved)
|
it, with a sentence saying it sends the user's addresses, and the hash of
|
||||||
|
each transaction opened, to the Blockscout API below, for transaction
|
||||||
|
history, tokens not tracked, and transaction details; then the endpoint
|
||||||
|
URL input + "Save" button (validated against `/stats` before being saved,
|
||||||
|
a request the user presses for that carries no user data, made whether or
|
||||||
|
not the checkbox is ticked)
|
||||||
|
- Etherscan: a "Check recipients on Etherscan" checkbox, unticked until the
|
||||||
|
user ticks it, with a sentence saying it sends the address about to be
|
||||||
|
sent to, to Etherscan, to look for a phishing or scam label on it. Both
|
||||||
|
checkboxes are one setting each for every network, and Welcome offers the
|
||||||
|
same two
|
||||||
- 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 +2218,9 @@ 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
|
||||||
- "+ 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**
|
||||||
@@ -2665,17 +2737,27 @@ 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 to one service only, the Ethereum RPC endpoint, and asks one more,
|
||||||
|
the CoinDesk price API, for prices with nothing but token symbols. Two more
|
||||||
|
services are sent none of the user's data until the user turns on the setting
|
||||||
|
for each, on the Welcome screen or in Settings: Blockscout ("Use Blockscout")
|
||||||
|
and Etherscan ("Check recipients on Etherscan"). Both settings are off in a new
|
||||||
|
profile, and each is one setting for 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. ENS lookups, of a name typed into Send and of the names shown for
|
||||||
any endpoint they prefer, including a local node). By default the extension
|
the user's addresses, are made through it alone, so a name whose resolver says
|
||||||
talks to `https://ethereum-rpc.publicnode.com`.
|
it is resolved off-chain does not resolve. The default endpoint 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 +2769,36 @@ 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"): Used to fetch
|
||||||
`https://eth.blockscout.com/api/v2` (configurable by the user in Settings).
|
transaction history (normal transactions and ERC-20 token transfers) for Home,
|
||||||
- **Data sent**: Ethereum addresses. Blockscout receives the user's
|
AddressDetail and AddressToken, the balances of the ERC-20 tokens the user has
|
||||||
addresses to query their transaction history and token balances. No
|
not tracked, token holder counts (for spam filtering), and a transaction's
|
||||||
private keys, passwords, or signing operations are sent.
|
block, network details and calldata for TransactionDetail. The default
|
||||||
|
endpoint is `https://eth.blockscout.com/api/v2` (configurable by the user in
|
||||||
|
Settings). While the setting is off, those screens say in a plain line what is
|
||||||
|
not shown and that this setting turns it on, and only tracked tokens are
|
||||||
|
listed, their balances read through the RPC endpoint. The endpoint check
|
||||||
|
Settings makes when the user presses "Save" beside the URL asks only for
|
||||||
|
`/stats` and 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 address. The user's own addresses are not
|
||||||
|
sent.
|
||||||
|
|
||||||
|
The etherscan links on the screens open a page in the browser when the user
|
||||||
|
clicks one. They are not requests the wallet makes, and nothing is fetched from
|
||||||
|
them until then.
|
||||||
|
|
||||||
What the extension does NOT do:
|
What the extension does NOT do:
|
||||||
|
|
||||||
@@ -2702,16 +2807,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, and,
|
||||||
CoinDesk price API, and Blockscout API), AutistMask also contacts:
|
when they turn Blockscout on, the Blockscout URL at their own instance (price
|
||||||
|
fetching can be disabled in a future version).
|
||||||
- **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 +2991,10 @@ 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 (tokens the user adds by contract address, and,
|
||||||
with 1,000 or more holders, and 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)
|
||||||
|
|||||||
+47
-27
@@ -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 you have "Use Blockscout" on, the others
|
||||||
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,11 @@ 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, and asks the CoinDesk price API for prices with nothing
|
||||||
|
but token symbols. Blockscout and Etherscan are sent none of your data until you
|
||||||
|
turn on "Use Blockscout" or "Check recipients on Etherscan", on the Welcome
|
||||||
|
screen or later in Settings. Both are off in a new install.
|
||||||
|
|
||||||
### External Services
|
### External Services
|
||||||
|
|
||||||
@@ -96,16 +100,23 @@ 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
|
Off until you turn on "Use Blockscout". Used to fetch transaction history, the
|
||||||
open-source blockchain explorer. AutistMask queries it for your ERC-20 token
|
balances of the tokens you have not tracked, and the details of a transaction
|
||||||
balances (including the holder counts used for spam filtering) and your recent
|
you open. Blockscout is an open-source blockchain explorer. AutistMask queries
|
||||||
transactions and token transfers. You can change this in Settings to a
|
it for your ERC-20 token balances (including the holder counts used for spam
|
||||||
self-hosted Blockscout instance.
|
filtering), your recent transactions and token transfers, and the block, fees
|
||||||
|
and calldata of a transaction you open. You can change this in Settings to a
|
||||||
|
self-hosted Blockscout instance. While it is off, the screens that would show
|
||||||
|
history say so, and only the tokens you track are listed.
|
||||||
|
|
||||||
When it is contacted: on every balance refresh, and whenever a screen showing
|
When it is contacted: while "Use Blockscout" is on, on every balance refresh,
|
||||||
transaction history is opened.
|
whenever a screen showing transaction history is opened, and when you open a
|
||||||
|
transaction. The one exception, whether or not the setting is on, is pressing
|
||||||
|
"Save" beside the Blockscout URL in Settings, which checks that URL by asking it
|
||||||
|
for `/stats` and sends none of your data.
|
||||||
|
|
||||||
What gets sent: your Ethereum addresses (to look up balances and transactions).
|
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`)
|
||||||
|
|
||||||
@@ -122,13 +133,16 @@ 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
|
Off until you turn on "Check recipients on Etherscan". When you review a send,
|
||||||
address page and looks for a "Fake_Phishing"/"Phish/Hack" label or a scam
|
AutistMask fetches the recipient's public Etherscan address page and looks for a
|
||||||
warning, and shows a red warning if it finds one. This is a plain page fetch
|
"Fake_Phishing"/"Phish/Hack" label or a scam warning, and shows a red warning if
|
||||||
with no API key, made by your browser. It is best-effort: if it fails, it is
|
it finds one. This is a plain page fetch with no API key, made by your browser.
|
||||||
silently ignored. This endpoint is not user-configurable.
|
It is best-effort: if it fails, it is silently ignored. This endpoint is not
|
||||||
|
user-configurable. While it is off, the confirmation screen says that no
|
||||||
|
Etherscan check was made.
|
||||||
|
|
||||||
When it is contacted: each time you reach the send confirmation screen.
|
When it is contacted: only while "Check recipients on Etherscan" is on, each
|
||||||
|
time you reach the send confirmation screen.
|
||||||
|
|
||||||
What gets sent: the recipient address you are about to send to, and your IP
|
What gets sent: the recipient address you are about to send to, and your IP
|
||||||
address. Your own addresses are not sent.
|
address. Your own addresses are not sent.
|
||||||
@@ -185,6 +199,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 same screen 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 +237,11 @@ 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, and
|
||||||
bundled list of well-known tokens or have at least 1,000 holders; everything
|
then only if they are in the extension's bundled list of well-known tokens or
|
||||||
else is treated as spam and hidden. To track a token explicitly (which also
|
have at least 1,000 holders; everything else is treated as spam and hidden. To
|
||||||
shows it at zero balance), add it by contract address:
|
track a token explicitly (which also shows it at zero balance, and shows it
|
||||||
|
whether or not "Use Blockscout" is on), 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 +407,10 @@ 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**: "Use Blockscout", off until you tick it, and the
|
||||||
transaction history. You can use a self-hosted instance.
|
Blockscout instance used for transaction history, the tokens you have not
|
||||||
|
tracked and transaction details. 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.
|
||||||
|
|||||||
@@ -1397,6 +1397,7 @@ async function backgroundRefresh() {
|
|||||||
wallets,
|
wallets,
|
||||||
s.rpcUrl,
|
s.rpcUrl,
|
||||||
s.blockscoutUrl,
|
s.blockscoutUrl,
|
||||||
|
s.useBlockscout,
|
||||||
s.trackedTokens,
|
s.trackedTokens,
|
||||||
s.networkId,
|
s.networkId,
|
||||||
);
|
);
|
||||||
|
|||||||
+89
-1
@@ -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 either of these, here or
|
||||||
|
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">
|
||||||
|
Sends your addresses, and the hash of each transaction
|
||||||
|
you open, to the Blockscout API, for transaction
|
||||||
|
history, tokens you have not tracked, and transaction
|
||||||
|
details.
|
||||||
|
</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 the address you are about to send to, to
|
||||||
|
Etherscan, to look for a phishing or scam label on it.
|
||||||
|
</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 -->
|
||||||
@@ -660,6 +703,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 +1052,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">
|
||||||
|
Sends your addresses, and the hash of each transaction
|
||||||
|
you open, to the Blockscout API below, for transaction
|
||||||
|
history, tokens you have not tracked, and transaction
|
||||||
|
details.
|
||||||
|
</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 +1080,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 the address you are about to send to, to
|
||||||
|
Etherscan, to look for a phishing or scam label on it.
|
||||||
|
</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">
|
||||||
@@ -1504,6 +1582,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">
|
||||||
|
|||||||
+2
-1
@@ -94,6 +94,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,
|
||||||
@@ -397,7 +398,7 @@ async function init() {
|
|||||||
followNetworkSwitch();
|
followNetworkSwitch();
|
||||||
|
|
||||||
if (!state.hasWallet) {
|
if (!state.hasWallet) {
|
||||||
showView("welcome");
|
welcome.show();
|
||||||
} else {
|
} else {
|
||||||
renderWalletList();
|
renderWalletList();
|
||||||
restoreView();
|
restoreView();
|
||||||
|
|||||||
@@ -40,9 +40,11 @@ function markViewRendered(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, and
|
||||||
// asks for one after a network switch, because every screen then on the page
|
// asks for one after a network switch, because every screen then on the page
|
||||||
// shows the previous network (showNewNetwork() in views/settings.js); the
|
// shows the previous network (showNewNetwork() in views/settings.js), and
|
||||||
// unit tests, which simulate several page loads against one module
|
// after "Use Blockscout" or "Check recipients on Etherscan" changes in
|
||||||
// instance, ask for one too.
|
// Settings, because every screen then on the page was drawn under the old
|
||||||
|
// setting; the unit tests, which simulate several page loads against one
|
||||||
|
// module 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>';
|
||||||
|
|||||||
@@ -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>';
|
||||||
|
|||||||
@@ -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, and 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)) {
|
||||||
@@ -612,6 +617,7 @@ async function checkRecipientHistory(txInfo) {
|
|||||||
const asyncWarnings = await getFullWarnings(txInfo.to, provider, {
|
const asyncWarnings = await getFullWarnings(txInfo.to, provider, {
|
||||||
fromAddress: txInfo.from,
|
fromAddress: txInfo.from,
|
||||||
chainId: txInfo.chainId,
|
chainId: txInfo.chainId,
|
||||||
|
checkRecipientsOnEtherscan: state.checkRecipientsOnEtherscan,
|
||||||
});
|
});
|
||||||
// The user may have left this transaction while the checks were in
|
// The user may have left this transaction while the checks were in
|
||||||
// flight; its warnings must not reach another recipient's screen.
|
// flight; its warnings must not reach another recipient's screen.
|
||||||
|
|||||||
@@ -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: the 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>';
|
||||||
|
|||||||
@@ -201,6 +201,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();
|
||||||
@@ -363,6 +367,33 @@ function init(_ctx) {
|
|||||||
showFlash("Saved.");
|
showFlash("Saved.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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, and Back draws the one it lands on again. Welcome
|
||||||
|
// offers the same two settings and is only unhidden on Back, so its boxes
|
||||||
|
// are set here too.
|
||||||
|
$("settings-use-blockscout").addEventListener("change", async () => {
|
||||||
|
state.useBlockscout = $("settings-use-blockscout").checked;
|
||||||
|
$("welcome-use-blockscout").checked = state.useBlockscout;
|
||||||
|
resetRenderedViews();
|
||||||
|
markViewRendered("settings");
|
||||||
|
await saveState();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("settings-check-recipients-on-etherscan").addEventListener(
|
||||||
|
"change",
|
||||||
|
async () => {
|
||||||
|
state.checkRecipientsOnEtherscan = $(
|
||||||
|
"settings-check-recipients-on-etherscan",
|
||||||
|
).checked;
|
||||||
|
$("welcome-check-recipients-on-etherscan").checked =
|
||||||
|
state.checkRecipientsOnEtherscan;
|
||||||
|
resetRenderedViews();
|
||||||
|
markViewRendered("settings");
|
||||||
|
await saveState();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -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) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The block, network details, calldata and its decoding, from Blockscout. While
|
||||||
|
// the user has "Use Blockscout" 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,34 @@
|
|||||||
const { $ } = require("./helpers");
|
// The first screen, shown while there is no wallet. It offers the two lookups
|
||||||
|
// at services other than the RPC endpoint, both off until the user ticks them,
|
||||||
|
// which Settings offers as well.
|
||||||
|
|
||||||
|
const { $, showView } = require("./helpers");
|
||||||
|
const { state, saveState } = require("../../shared/state");
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
$("welcome-use-blockscout").checked = state.useBlockscout;
|
||||||
|
$("welcome-check-recipients-on-etherscan").checked =
|
||||||
|
state.checkRecipientsOnEtherscan;
|
||||||
|
showView("welcome");
|
||||||
|
}
|
||||||
|
|
||||||
function init(ctx) {
|
function init(ctx) {
|
||||||
$("btn-welcome-add").addEventListener("click", ctx.showAddWalletView);
|
$("btn-welcome-add").addEventListener("click", ctx.showAddWalletView);
|
||||||
|
|
||||||
|
$("welcome-use-blockscout").addEventListener("change", async () => {
|
||||||
|
state.useBlockscout = $("welcome-use-blockscout").checked;
|
||||||
|
await saveState();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("welcome-check-recipients-on-etherscan").addEventListener(
|
||||||
|
"change",
|
||||||
|
async () => {
|
||||||
|
state.checkRecipientsOnEtherscan = $(
|
||||||
|
"welcome-check-recipients-on-etherscan",
|
||||||
|
).checked;
|
||||||
|
await saveState();
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { init };
|
module.exports = { init, show };
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ function getLocalWarnings(address, options = {}) {
|
|||||||
* @param {string} [options.fromAddress] - Sender address (for self-send check).
|
* @param {string} [options.fromAddress] - Sender address (for self-send check).
|
||||||
* @param {string} [options.chainId] - Chain id of the network whose explorer
|
* @param {string} [options.chainId] - Chain id of the network whose explorer
|
||||||
* the Etherscan label check asks; the active network's when omitted.
|
* the Etherscan label check asks; the active network's when omitted.
|
||||||
|
* @param {boolean} [options.checkRecipientsOnEtherscan] - The user's setting
|
||||||
|
* of that name. The Etherscan label check is made only while it is on.
|
||||||
* @returns {Promise<Array<{type: string, message: string, severity: string}>>}
|
* @returns {Promise<Array<{type: string, message: string, severity: string}>>}
|
||||||
*/
|
*/
|
||||||
async function getFullWarnings(address, provider, options = {}) {
|
async function getFullWarnings(address, provider, options = {}) {
|
||||||
@@ -114,6 +116,7 @@ async function getFullWarnings(address, provider, options = {}) {
|
|||||||
try {
|
try {
|
||||||
const etherscanWarning = await checkEtherscanLabel(
|
const etherscanWarning = await checkEtherscanLabel(
|
||||||
address,
|
address,
|
||||||
|
options.checkRecipientsOnEtherscan,
|
||||||
options.chainId,
|
options.chainId,
|
||||||
);
|
);
|
||||||
if (etherscanWarning) {
|
if (etherscanWarning) {
|
||||||
|
|||||||
+27
-7
@@ -1,6 +1,7 @@
|
|||||||
// Balance fetching: ETH balances and the balances of the tokens the user
|
// Balance fetching: ETH balances and the balances of the tokens the user
|
||||||
// tracks via RPC, the other ERC-20 token balances and the holder counts via
|
// tracks via RPC, the other ERC-20 token balances and the holder counts via
|
||||||
// Blockscout, ENS reverse lookup via RPC.
|
// Blockscout while the user has "Use Blockscout" on, ENS reverse lookup via
|
||||||
|
// RPC.
|
||||||
|
|
||||||
const {
|
const {
|
||||||
JsonRpcProvider,
|
JsonRpcProvider,
|
||||||
@@ -37,9 +38,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. Left on, as
|
||||||
|
// ethers has it, a resolver that says a name is resolved off-chain makes ethers
|
||||||
|
// fetch the web address the resolver gives, straight from the browser, with 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). 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 +91,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: 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 +111,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 +313,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 +380,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 none.
|
||||||
// handed to fetchTrackedTokenBalances() too, which waits for it
|
// The Blockscout request is started first and handed to
|
||||||
// after sending its balanceOf reads, so the two run side by side.
|
// fetchTrackedTokenBalances() too, which waits for it 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,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -78,11 +78,20 @@ 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.
|
||||||
*
|
*
|
||||||
|
* While the user's "Check recipients on Etherscan" setting is off, Etherscan
|
||||||
|
* is not asked and the answer is null.
|
||||||
|
*
|
||||||
* @param {string} address - Ethereum address to check.
|
* @param {string} address - Ethereum address to check.
|
||||||
|
* @param {boolean} checkRecipientsOnEtherscan - The user's setting.
|
||||||
* @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,
|
||||||
|
checkRecipientsOnEtherscan,
|
||||||
|
chainId,
|
||||||
|
) {
|
||||||
|
if (!checkRecipientsOnEtherscan) return null;
|
||||||
try {
|
try {
|
||||||
// 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).
|
||||||
|
|||||||
@@ -48,6 +48,12 @@ const DEFAULT_STATE = {
|
|||||||
hideDustTransactions: true,
|
hideDustTransactions: true,
|
||||||
dustThresholdGwei: 100000,
|
dustThresholdGwei: 100000,
|
||||||
utcTimestamps: false,
|
utcTimestamps: false,
|
||||||
|
// "Use Blockscout" and "Check recipients on Etherscan" in Settings, one
|
||||||
|
// setting each for every network. Both off until the user turns them on:
|
||||||
|
// the requests they allow send the user's addresses to a service other
|
||||||
|
// than the RPC endpoint.
|
||||||
|
useBlockscout: false,
|
||||||
|
checkRecipientsOnEtherscan: false,
|
||||||
fraudContracts: [],
|
fraudContracts: [],
|
||||||
tokenHolderCache: {},
|
tokenHolderCache: {},
|
||||||
theme: "system",
|
theme: "system",
|
||||||
@@ -406,6 +412,12 @@ function normalizePersisted(saved) {
|
|||||||
: 100000;
|
: 100000;
|
||||||
out.utcTimestamps =
|
out.utcTimestamps =
|
||||||
saved.utcTimestamps !== undefined ? saved.utcTimestamps : false;
|
saved.utcTimestamps !== undefined ? saved.utcTimestamps : false;
|
||||||
|
out.useBlockscout =
|
||||||
|
saved.useBlockscout !== undefined ? saved.useBlockscout : false;
|
||||||
|
out.checkRecipientsOnEtherscan =
|
||||||
|
saved.checkRecipientsOnEtherscan !== undefined
|
||||||
|
? saved.checkRecipientsOnEtherscan
|
||||||
|
: false;
|
||||||
// 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(),
|
||||||
|
|||||||
@@ -235,12 +235,18 @@ 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 there is no history: 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);
|
||||||
|
|
||||||
|
|||||||
@@ -410,7 +410,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;
|
||||||
|
|||||||
@@ -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,470 @@
|
|||||||
|
// 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.
|
||||||
|
// Each request to either service is skipped where it is made while its setting
|
||||||
|
// is off, and made while it is on; the popup and the background hand the
|
||||||
|
// stored setting to the code that makes the request. The one exception is the
|
||||||
|
// endpoint check Settings makes when the user presses "Save" beside the
|
||||||
|
// Blockscout URL. It asks only for `/stats` and carries no user data; it is
|
||||||
|
// made whether or not the setting is on.
|
||||||
|
//
|
||||||
|
// 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), whose fetch()
|
||||||
|
// stub records the same way.
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const { FetchRequest } = require("ethers");
|
||||||
|
const {
|
||||||
|
bootPopup,
|
||||||
|
cleanupPopup,
|
||||||
|
unversionedValidProfile,
|
||||||
|
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 Check recipients on Etherscan is on", async () => {
|
||||||
|
const provider = {
|
||||||
|
getCode: async () => "0x",
|
||||||
|
getTransactionCount: async () => 1,
|
||||||
|
getBalance: async () => 0n,
|
||||||
|
};
|
||||||
|
await getFullWarnings(RECIPIENT, provider, { chainId: "0x1" });
|
||||||
|
await getFullWarnings(RECIPIENT, provider, {
|
||||||
|
chainId: "0x1",
|
||||||
|
checkRecipientsOnEtherscan: false,
|
||||||
|
});
|
||||||
|
expect(asked).toEqual([]);
|
||||||
|
|
||||||
|
await getFullWarnings(RECIPIENT, provider, {
|
||||||
|
chainId: "0x1",
|
||||||
|
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 a checkbox on the page, as the user does.
|
||||||
|
async function tick(page, id) {
|
||||||
|
const box = page.node(id);
|
||||||
|
box.checked = true;
|
||||||
|
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 tick(page, "welcome-use-blockscout");
|
||||||
|
expect(page.storage.read("autistmask").useBlockscout).toBe(true);
|
||||||
|
await tick(page, "welcome-check-recipients-on-etherscan");
|
||||||
|
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 tick(page, "settings-use-blockscout");
|
||||||
|
expect(page.storage.read("autistmask").useBlockscout).toBe(true);
|
||||||
|
await tick(page, "settings-check-recipients-on-etherscan");
|
||||||
|
expect(page.storage.read("autistmask").checkRecipientsOnEtherscan).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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]),
|
||||||
|
}));
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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,62 @@ 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". The
|
||||||
|
// endpoint check Settings makes when the user presses "Save" beside the
|
||||||
|
// Blockscout URL asks only for `/stats` and carries no user data; it is made
|
||||||
|
// whether or not the setting is on. The route handler in tests/e2e/network.js
|
||||||
|
// records every request the extension makes, from its pages and its background
|
||||||
|
// worker, in env.routeOpts.requests. 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, before
|
||||||
|
// then, is not recorded. These tests drive Home, AddressDetail, AddressToken
|
||||||
|
// and ConfirmTx, and the background's own balance refresh, before either
|
||||||
|
// setting is changed, then turn each on and watch its requests start.
|
||||||
|
// 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` says what Use Blockscout being off leaves out.
|
||||||
|
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 +352,202 @@ 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, and skips a
|
||||||
|
// refresh when the stored record says one ran in the last 30 seconds, which
|
||||||
|
// the open popup's own refresh does every 10. So the popup is closed, and that
|
||||||
|
// record is written back to say no refresh has run, from the background
|
||||||
|
// worker, 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",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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, as the check was
|
||||||
|
// always made before #668.
|
||||||
|
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 +5510,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,111 @@
|
|||||||
|
// 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, and that
|
||||||
|
// request would carry the name to a server other than the RPC endpoint
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/668). With it set, such a name
|
||||||
|
// does not resolve.
|
||||||
|
//
|
||||||
|
// The provider getProvider() builds runs against a fake JSON-RPC node,
|
||||||
|
// answered 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(), gives NAME as the reverse record of any address, and
|
||||||
|
// answers resolve() with the OffchainLookup error that 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,
|
||||||
|
|||||||
@@ -618,6 +618,8 @@ const CONTRACT = [
|
|||||||
"hideFraudContracts",
|
"hideFraudContracts",
|
||||||
"hideDustTransactions",
|
"hideDustTransactions",
|
||||||
"utcTimestamps",
|
"utcTimestamps",
|
||||||
|
"useBlockscout",
|
||||||
|
"checkRecipientsOnEtherscan",
|
||||||
"debugMode",
|
"debugMode",
|
||||||
].map((field) => ({
|
].map((field) => ({
|
||||||
field,
|
field,
|
||||||
@@ -625,8 +627,9 @@ const CONTRACT = [
|
|||||||
// A flag: only ever tested for truthiness, and written back verbatim.
|
// A flag: only ever tested for truthiness, and written back verbatim.
|
||||||
hostile: [42, "notabool", { a: 1 }],
|
hostile: [42, "notabool", { a: 1 }],
|
||||||
// Both answers to that truthiness test have to be driven, and 0 is a
|
// Both answers to that truthiness test have to be driven, and 0 is a
|
||||||
// value src/ never writes for a flag. For utcTimestamps and debugMode
|
// value src/ never writes for a flag. For utcTimestamps,
|
||||||
// the falsy answer is also the DEFAULT_STATE default.
|
// useBlockscout, checkRecipientsOnEtherscan and debugMode the falsy
|
||||||
|
// answer is also the DEFAULT_STATE default.
|
||||||
falsy: [0],
|
falsy: [0],
|
||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ 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). Both requests are made
|
||||||
|
// only while the user has the setting for them on.
|
||||||
function restoringOnto(view, viewData) {
|
function restoringOnto(view, viewData) {
|
||||||
return unversionedValidProfile({
|
return unversionedValidProfile({
|
||||||
currentView: view,
|
currentView: view,
|
||||||
@@ -52,6 +53,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",
|
||||||
);
|
);
|
||||||
|
|||||||
+23
-10
@@ -582,12 +582,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 +599,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 +623,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 +637,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 +648,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 +668,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 +694,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