Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1727a4af07 |
@@ -1494,6 +1494,15 @@ 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.
|
||||||
|
|
||||||
|
A network switch in Settings leaves every screen beneath Settings showing the
|
||||||
|
previous network, so after it "Back" behaves as in a reopened popup: every
|
||||||
|
screen on the stack counts as not yet rendered, and the stack is cut at the
|
||||||
|
first screen that cannot be rendered from saved state, such as Send or AddToken.
|
||||||
|
"Back" then renders the screen it lands on for the new network, and anything
|
||||||
|
typed there and not saved, which belonged to the previous network, is dropped.
|
||||||
|
Without a wallet the stack is not cut, because it starts at Welcome, which shows
|
||||||
|
nothing from a network.
|
||||||
|
|
||||||
Every screen that holds secret material in the page registers a cleanup with
|
Every screen that holds secret material in the page registers a cleanup with
|
||||||
`onViewLeave()` (`src/popup/views/helpers.js`), which `showView()` runs on every
|
`onViewLeave()` (`src/popup/views/helpers.js`), which `showView()` runs on every
|
||||||
exit from that screen rather than only on its "Back" button, so nothing secret
|
exit from that screen rather than only on its "Back" button, so nothing secret
|
||||||
@@ -1924,7 +1933,8 @@ view would leave a wallet one click from deletion.
|
|||||||
- Contract address input
|
- Contract address input
|
||||||
- Status line ("Looking up token...", cleared or replaced on failure)
|
- Status line ("Looking up token...", cleared or replaced on failure)
|
||||||
- Common token quick-pick buttons (top 25 by market cap), which fill the
|
- Common token quick-pick buttons (top 25 by market cap), which fill the
|
||||||
contract address input
|
contract address input. Shown on Ethereum Mainnet only, because the
|
||||||
|
bundled token list holds mainnet contracts
|
||||||
- "Add" button
|
- "Add" button
|
||||||
- **Transitions**:
|
- **Transitions**:
|
||||||
- "Add" (valid contract) → tracks the token, pops the stack, and re-renders
|
- "Add" (valid contract) → tracks the token, pops the stack, and re-renders
|
||||||
@@ -1941,16 +1951,19 @@ view would leave a wallet one click from deletion.
|
|||||||
- Wallets: one row per wallet with its name (tap to rename inline), a
|
- Wallets: one row per wallet with its name (tap to rename inline), a
|
||||||
`[recovery phrase]` button on HD wallets only, and an `[x]` delete button,
|
`[recovery phrase]` button on HD wallets only, and an `[x]` delete button,
|
||||||
plus a "+ Add wallet" button
|
plus a "+ Add wallet" button
|
||||||
- Tracked Tokens: one row per tracked token with an `[x]` remove button,
|
- Tracked Tokens: one row per token tracked on the active network with an
|
||||||
plus a "+ Add token" button
|
`[x]` remove button, plus a "+ Add token" button
|
||||||
- Display: "Show tracked tokens with zero balance" checkbox, "UTC
|
- Display: "Show tracked tokens with zero balance" checkbox, "UTC
|
||||||
Timestamps" checkbox, and a Theme selector (System / Light / Dark)
|
Timestamps" checkbox, and a Theme selector (System / Light / Dark)
|
||||||
- Network: network selector (Ethereum Mainnet / Sepolia Testnet). The
|
- Network: network selector (Ethereum Mainnet / Sepolia Testnet). The
|
||||||
network changes only here, or when the user approves a site's request on
|
network changes only here, or when the user approves a site's request on
|
||||||
**NetworkApproval**; either way, switching restores the RPC and Blockscout
|
**NetworkApproval**; either way, switching restores the RPC and Blockscout
|
||||||
endpoints last used on that network, or that network's defaults if it has
|
endpoints last used on that network, or that network's defaults if it has
|
||||||
none, and sends `chainChanged` with the new chain id to every open tab.
|
none, and the tokens tracked on that network, and sends `chainChanged`
|
||||||
Choosing the network already active changes nothing and sends nothing
|
with the new chain id to every open tab. A token is tracked only on the
|
||||||
|
network it was added on, because a contract address names a token on one
|
||||||
|
network only. Choosing the network 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: endpoint URL input + "Save" button (validated against
|
||||||
@@ -1995,7 +2008,8 @@ view would leave a wallet one click from deletion.
|
|||||||
- `[x]` on a denied site → forgets the refusal, in place; it connects
|
- `[x]` on a denied site → forgets the refusal, in place; it connects
|
||||||
nothing and tells the background nothing
|
nothing and tells the background nothing
|
||||||
- Ten clicks on the version → reveals the Debug well (no screen change)
|
- Ten clicks on the version → reveals the Debug well (no screen change)
|
||||||
- "Back" (or Settings gear again) → previous screen (Home)
|
- "Back" (or Settings gear again) → previous screen (Home); after a network
|
||||||
|
switch, the stack is cut first (see the top of Screen Map)
|
||||||
|
|
||||||
#### ShowRecoveryPhrase (`show-phrase`)
|
#### ShowRecoveryPhrase (`show-phrase`)
|
||||||
|
|
||||||
@@ -2036,9 +2050,7 @@ view would leave a wallet one click from deletion.
|
|||||||
- **Elements**:
|
- **Elements**:
|
||||||
- "Back" button, "Delete Wallet" heading
|
- "Back" button, "Delete Wallet" heading
|
||||||
- Warning naming the wallet and stating that deletion is permanent and any
|
- Warning naming the wallet and stating that deletion is permanent and any
|
||||||
funds are unrecoverable without the recovery phrase, the private key or
|
funds are unrecoverable without the recovery phrase
|
||||||
the extended private key, according to the wallet's kind
|
|
||||||
(`keyMaterialName()` in `src/popup/views/helpers.js`)
|
|
||||||
- Password input
|
- Password input
|
||||||
- Error line
|
- Error line
|
||||||
- "Confirm Delete" button
|
- "Confirm Delete" button
|
||||||
@@ -2079,10 +2091,9 @@ view would leave a wallet one click from deletion.
|
|||||||
cannot be unlocked again, and that no password is needed to delete it
|
cannot be unlocked again, and that no password is needed to delete it
|
||||||
- What deletion does and does not do: it erases the copy of the key stored
|
- What deletion does and does not do: it erases the copy of the key stored
|
||||||
on this device; nothing on chain changes and no money is moved
|
on this device; nothing on chain changes and no money is moved
|
||||||
- The route back — adding the wallet again, with a new password, from a kept
|
- The route back — adding the wallet again with the recovery phrase and a
|
||||||
copy of its recovery phrase, private key or extended private key (named as
|
new password — and, in bold, that without that phrase written down the
|
||||||
on DeleteWallet) — and, in bold, that without that copy the deletion loses
|
deletion loses everything the wallet holds, forever
|
||||||
everything the wallet holds, forever
|
|
||||||
- That the other wallets are not touched
|
- That the other wallets are not touched
|
||||||
- The wallet's name, and a text input asking for it to be typed back. A name
|
- The wallet's name, and a text input asking for it to be typed back. A name
|
||||||
that shows nothing at all (only spaces, or only characters that paint
|
that shows nothing at all (only spaces, or only characters that paint
|
||||||
@@ -2140,9 +2151,8 @@ view would leave a wallet one click from deletion.
|
|||||||
the address **only if it has on-chain activity**. An address that was
|
the address **only if it has on-chain activity**. An address that was
|
||||||
never used is not found by that scan. The text is written by
|
never used is not found by that scan. The text is written by
|
||||||
`recoveryPathText()` rather than sitting in `index.html`, so it can name
|
`recoveryPathText()` rather than sitting in `index.html`, so it can name
|
||||||
the wallet's own kind of key material through `keyMaterialName()`, as
|
the wallet's own kind of key material: an xprv wallet has no recovery
|
||||||
DeleteWallet and DeleteWalletLostPassword do: an xprv wallet has no
|
phrase to re-import.
|
||||||
recovery phrase to re-import.
|
|
||||||
- A warning when the address holds anything, ETH or any tracked ERC-20,
|
- A warning when the address holds anything, ETH or any tracked ERC-20,
|
||||||
followed by the holdings themselves via `balanceLinesForAddress()` and the
|
followed by the holdings themselves via `balanceLinesForAddress()` and the
|
||||||
USD total via `formatAddressTotal()` (see
|
USD total via `formatAddressTotal()` (see
|
||||||
@@ -2178,7 +2188,8 @@ view would leave a wallet one click from deletion.
|
|||||||
#### SettingsAddToken (`settings-addtoken`)
|
#### SettingsAddToken (`settings-addtoken`)
|
||||||
|
|
||||||
- **When**: User tapped "+ Add token" in Settings. Tokens added here are tracked
|
- **When**: User tapped "+ Add token" in Settings. Tokens added here are tracked
|
||||||
across every address, unlike AddToken which is reached from one address.
|
across every address on the active network, unlike AddToken which is reached
|
||||||
|
from one address.
|
||||||
- **Elements**:
|
- **Elements**:
|
||||||
- "Back" button, "Add Token" heading
|
- "Back" button, "Add Token" heading
|
||||||
- Instruction text
|
- Instruction text
|
||||||
@@ -2187,8 +2198,12 @@ view would leave a wallet one click from deletion.
|
|||||||
- "Or pick from top 100:" dropdown (already-tracked tokens are disabled) +
|
- "Or pick from top 100:" dropdown (already-tracked tokens are disabled) +
|
||||||
"Add selected" button
|
"Add selected" button
|
||||||
- "Or enter contract address:" input, a status line, and an "Add" button
|
- "Or enter contract address:" input, a status line, and an "Add" button
|
||||||
|
- The two lists from the bundled token list, "Top tokens:" and "Or pick from
|
||||||
|
top 100:", are shown on Ethereum Mainnet only, because that list holds
|
||||||
|
mainnet contracts. On any other network the contract address is the only
|
||||||
|
add path
|
||||||
- **Transitions**:
|
- **Transitions**:
|
||||||
- Any of the three add paths, on success → adds the token and shows an
|
- Any of the add paths shown, on success → adds the token and shows an
|
||||||
"Added SYMBOL" flash message (no screen change)
|
"Added SYMBOL" flash message (no screen change)
|
||||||
- A duplicate, a scam-listed address, or a failed contract lookup → flash
|
- A duplicate, a scam-listed address, or a failed contract lookup → flash
|
||||||
message, no screen change
|
message, no screen change
|
||||||
@@ -2406,8 +2421,9 @@ view would leave a wallet one click from deletion.
|
|||||||
- **Transitions**:
|
- **Transitions**:
|
||||||
- "Switch" → closes popup; the background switches the network as
|
- "Switch" → closes popup; the background switches the network as
|
||||||
**Settings** does, restoring the RPC and Blockscout endpoints last used on
|
**Settings** does, restoring the RPC and Blockscout endpoints last used on
|
||||||
that network (or that network's defaults if it has none), sends
|
that network (or that network's defaults if it has none) and the tokens
|
||||||
`chainChanged` to every open tab, and answers the site with success
|
tracked on that network, sends `chainChanged` to every open tab, and
|
||||||
|
answers the site with success
|
||||||
- "Reject" → closes popup; the site is answered with EIP-1193 code 4001 and
|
- "Reject" → closes popup; the site is answered with EIP-1193 code 4001 and
|
||||||
nothing changes
|
nothing changes
|
||||||
- Popup window closed without answering → the same as "Reject"
|
- Popup window closed without answering → the same as "Reject"
|
||||||
@@ -2884,14 +2900,10 @@ Currently supported:
|
|||||||
or recipient, and amount.
|
or recipient, and amount.
|
||||||
- **Uniswap Universal Router**: `execute()` calls — shows swap direction (e.g.
|
- **Uniswap Universal Router**: `execute()` calls — shows swap direction (e.g.
|
||||||
"Swap USDT → ETH"), token addresses, amounts, execution steps, and deadline.
|
"Swap USDT → ETH"), token addresses, amounts, execution steps, and deadline.
|
||||||
The swap summary is shown only for a transaction sent to one of the Universal
|
Decodes Permit2, V2/V3/V4 swaps, wrap/unwrap, and balance checks. A V4 swap
|
||||||
Router addresses Uniswap publishes for the network: on TxApproval the network
|
action is read only in the parameter shape deployed on mainnet and Sepolia,
|
||||||
the transaction is for, on TransactionDetail the active network. The same
|
not in the newer one on `v4-periphery` `main`, which adds a `minHopPriceX36`
|
||||||
calldata sent to any other address is shown as raw data. Decodes Permit2,
|
field. An action in any other shape is not read, and its step is listed as
|
||||||
V2/V3/V4 swaps, wrap/unwrap, and balance checks. A V4 swap action is read only
|
|
||||||
in the parameter shape deployed on mainnet and Sepolia, not in the newer one
|
|
||||||
on `v4-periphery` `main`, which adds a `minHopPriceX36` field. An action in
|
|
||||||
any other shape is not read, and its step is listed as
|
|
||||||
`V4 Swap (could not be decoded)`.
|
`V4 Swap (could not be decoded)`.
|
||||||
|
|
||||||
### Non-Goals Forever
|
### Non-Goals Forever
|
||||||
|
|||||||
+14
-12
@@ -838,7 +838,7 @@
|
|||||||
id="add-token-info"
|
id="add-token-info"
|
||||||
class="text-xs text-muted mb-2 min-h-[1.25rem] invisible"
|
class="text-xs text-muted mb-2 min-h-[1.25rem] invisible"
|
||||||
></div>
|
></div>
|
||||||
<div class="mb-2">
|
<div id="add-token-common" class="mb-2">
|
||||||
<label class="block mb-1 text-xs text-muted"
|
<label class="block mb-1 text-xs text-muted"
|
||||||
>Or pick a common token:</label
|
>Or pick a common token:</label
|
||||||
>
|
>
|
||||||
@@ -924,9 +924,9 @@
|
|||||||
<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">Network</h3>
|
<h3 class="font-bold mb-1">Network</h3>
|
||||||
<p class="text-xs text-muted mb-1">
|
<p class="text-xs text-muted mb-1">
|
||||||
Select the Ethereum network. Switching restores the RPC
|
Select the Ethereum network. Switching networks will
|
||||||
and Blockscout endpoints last used on that network, or
|
update the RPC and Blockscout endpoints to their
|
||||||
that network's defaults if it has none.
|
defaults.
|
||||||
</p>
|
</p>
|
||||||
<div class="text-xs flex items-center gap-1">
|
<div class="text-xs flex items-center gap-1">
|
||||||
<select
|
<select
|
||||||
@@ -1123,8 +1123,7 @@
|
|||||||
<p class="text-xs mb-3">
|
<p class="text-xs mb-3">
|
||||||
Deleting
|
Deleting
|
||||||
<strong id="delete-wallet-name"></strong> is permanent. Any
|
<strong id="delete-wallet-name"></strong> is permanent. Any
|
||||||
funds will be unrecoverable without your
|
funds will be unrecoverable without your recovery phrase.
|
||||||
<span id="delete-wallet-key-material"></span>.
|
|
||||||
</p>
|
</p>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="block mb-1">Password</label>
|
<label class="block mb-1">Password</label>
|
||||||
@@ -1175,13 +1174,13 @@
|
|||||||
money at its addresses is not moved or destroyed.
|
money at its addresses is not moved or destroyed.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs mb-2">
|
<p class="text-xs mb-2">
|
||||||
If you have kept a copy of this wallet's
|
If you have the recovery phrase for this wallet written
|
||||||
<span id="delete-wallet-lost-key-material"></span>, add the
|
down, add the wallet again afterwards with a new password
|
||||||
wallet again afterwards with a new password and you will
|
and you will have it back.
|
||||||
have it back.
|
|
||||||
<strong
|
<strong
|
||||||
>If you have not, deleting this wallet means losing
|
>If you do not have it written down, deleting this
|
||||||
everything it holds, forever.</strong
|
wallet means losing everything it holds,
|
||||||
|
forever.</strong
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs mb-3">Your other wallets are not touched.</p>
|
<p class="text-xs mb-3">Your other wallets are not touched.</p>
|
||||||
@@ -1322,8 +1321,10 @@
|
|||||||
<h2 class="font-bold mb-2">Add Token</h2>
|
<h2 class="font-bold mb-2">Add Token</h2>
|
||||||
<p class="text-xs text-muted mb-3">
|
<p class="text-xs text-muted mb-3">
|
||||||
Pick a common token or enter a contract address manually.
|
Pick a common token or enter a contract address manually.
|
||||||
|
Common tokens are offered on Ethereum Mainnet only.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div id="settings-addtoken-common">
|
||||||
<!-- top 10 quick-pick buttons -->
|
<!-- top 10 quick-pick buttons -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="block mb-1 text-xs text-muted"
|
<label class="block mb-1 text-xs text-muted"
|
||||||
@@ -1353,6 +1354,7 @@
|
|||||||
Add selected
|
Add selected
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- manual contract address -->
|
<!-- manual contract address -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
|||||||
@@ -37,9 +37,11 @@ function markViewRendered(view) {
|
|||||||
if (view) renderedViews.add(view);
|
if (view) renderedViews.add(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Begin a fresh page-load scope. The popup gets one by being loaded; the
|
// Begin a fresh page-load scope. The popup gets one by being loaded, and
|
||||||
|
// asks for one when the user switches the network, because every screen
|
||||||
|
// then on the page shows the previous network (views/settings.js); the
|
||||||
// unit tests, which simulate several page loads against one module
|
// unit tests, which simulate several page loads against one module
|
||||||
// instance, ask for one.
|
// instance, ask for one too.
|
||||||
function resetRenderedViews() {
|
function resetRenderedViews() {
|
||||||
renderedViews.clear();
|
renderedViews.clear();
|
||||||
}
|
}
|
||||||
@@ -213,9 +215,10 @@ function renderView(view, state, views) {
|
|||||||
// in the two cases where the view is known to be on the page already:
|
// in the two cases where the view is known to be on the page already:
|
||||||
//
|
//
|
||||||
// - It is not one the popup renders from persisted state. The restored
|
// - It is not one the popup renders from persisted state. The restored
|
||||||
// stack is filtered against RESTORABLE_VIEWS, so such a view can only
|
// stack is filtered against RESTORABLE_VIEWS, and a network switch cuts
|
||||||
// be on the stack from this page load, where forward navigation
|
// it the same way (views/settings.js), so such a view can only be on the
|
||||||
// rendered it on the way in.
|
// stack from this page load, where forward navigation rendered it on the
|
||||||
|
// way in.
|
||||||
// - This page load has rendered it. Re-rendering would re-fetch and
|
// - This page load has rendered it. Re-rendering would re-fetch and
|
||||||
// clobber what it holds; Home is rendered anyway, see above.
|
// clobber what it holds; Home is rendered anyway, see above.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ function show() {
|
|||||||
$("add-token-address").value = "";
|
$("add-token-address").value = "";
|
||||||
$("add-token-info").textContent = "";
|
$("add-token-info").textContent = "";
|
||||||
$("add-token-info").style.visibility = "hidden";
|
$("add-token-info").style.visibility = "hidden";
|
||||||
|
// The bundled list holds Ethereum Mainnet contracts, which name no token
|
||||||
|
// on any other network.
|
||||||
|
$("add-token-common").classList.toggle(
|
||||||
|
"hidden",
|
||||||
|
state.networkId !== "mainnet",
|
||||||
|
);
|
||||||
const list = $("common-token-list");
|
const list = $("common-token-list");
|
||||||
list.innerHTML = getTopTokens(25)
|
list.innerHTML = getTopTokens(25)
|
||||||
.map(
|
.map(
|
||||||
|
|||||||
@@ -79,10 +79,9 @@ function tokenAmountText(rawAmount, decimals, symbol) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to decode calldata sent to `toAddress` on the network with chain id
|
// Try to decode calldata using known ABIs.
|
||||||
// `chainId`, using known ABIs.
|
|
||||||
// Returns { name, description, details } or null.
|
// Returns { name, description, details } or null.
|
||||||
function decodeCalldata(data, toAddress, chainId) {
|
function decodeCalldata(data, toAddress) {
|
||||||
if (!data || data === "0x" || data.length < 10) return null;
|
if (!data || data === "0x" || data.length < 10) return null;
|
||||||
|
|
||||||
// Where a token's scale is looked for, for every decoder below: the ERC-20
|
// Where a token's scale is looked for, for every decoder below: the ERC-20
|
||||||
@@ -186,12 +185,7 @@ function decodeCalldata(data, toAddress, chainId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try Uniswap Universal Router
|
// Try Uniswap Universal Router
|
||||||
const routerResult = uniswap.decode(
|
const routerResult = uniswap.decode(data, toAddress, decimalsSources);
|
||||||
data,
|
|
||||||
toAddress,
|
|
||||||
chainId,
|
|
||||||
decimalsSources,
|
|
||||||
);
|
|
||||||
if (routerResult) return routerResult;
|
if (routerResult) return routerResult;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -275,14 +269,8 @@ function showTxApproval(details) {
|
|||||||
chainId: approvedTx.chainId,
|
chainId: approvedTx.chainId,
|
||||||
};
|
};
|
||||||
|
|
||||||
// If this is an ERC-20 call, try to extract the real recipient and amount.
|
// If this is an ERC-20 call, try to extract the real recipient and amount
|
||||||
// Decoded for the network the transaction is for, which the Network line
|
const decoded = decodeCalldata(approvedTx.data, toAddr || "");
|
||||||
// names.
|
|
||||||
const decoded = decodeCalldata(
|
|
||||||
approvedTx.data,
|
|
||||||
toAddr || "",
|
|
||||||
approvedTx.chainId,
|
|
||||||
);
|
|
||||||
if (decoded && decoded.details) {
|
if (decoded && decoded.details) {
|
||||||
// The asset the status summary is counted in: an ERC-20 call's Token
|
// The asset the status summary is counted in: an ERC-20 call's Token
|
||||||
// contract, or a swap's input token. Its symbol is resolved from the
|
// contract, or a swap's input token. Its symbol is resolved from the
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ const {
|
|||||||
attachCopyHandlers,
|
attachCopyHandlers,
|
||||||
addressHoldsFunds,
|
addressHoldsFunds,
|
||||||
balanceLinesForAddress,
|
balanceLinesForAddress,
|
||||||
keyMaterialName,
|
|
||||||
} = require("./helpers");
|
} = require("./helpers");
|
||||||
const { formatAddressTotal, getAddressValue } = require("../../shared/prices");
|
const { formatAddressTotal, getAddressValue } = require("../../shared/prices");
|
||||||
|
const { walletHasRecoveryPhrase } = require("../../shared/wallet");
|
||||||
const { state, saveState } = require("../../shared/state");
|
const { state, saveState } = require("../../shared/state");
|
||||||
const {
|
const {
|
||||||
canRemoveAddress,
|
canRemoveAddress,
|
||||||
@@ -54,7 +54,9 @@ function setFlash(msg) {
|
|||||||
// The noun follows the wallet: an xprv wallet holds no recovery phrase, and
|
// The noun follows the wallet: an xprv wallet holds no recovery phrase, and
|
||||||
// this screen is offered on xprv wallets too.
|
// this screen is offered on xprv wallets too.
|
||||||
function recoveryPathText(wallet) {
|
function recoveryPathText(wallet) {
|
||||||
const secret = keyMaterialName(wallet);
|
const secret = walletHasRecoveryPhrase(wallet)
|
||||||
|
? "recovery phrase"
|
||||||
|
: "extended private key";
|
||||||
return (
|
return (
|
||||||
"Getting the address back into this list is not easy, so be sure. " +
|
"Getting the address back into this list is not easy, so be sure. " +
|
||||||
"Adding an address derives the next unused one, not this one, and " +
|
"Adding an address derives the next unused one, not this one, and " +
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ const {
|
|||||||
goBack,
|
goBack,
|
||||||
clearViewStack,
|
clearViewStack,
|
||||||
onViewLeave,
|
onViewLeave,
|
||||||
keyMaterialName,
|
|
||||||
} = require("./helpers");
|
} = require("./helpers");
|
||||||
const { state, saveState } = require("../../shared/state");
|
const { state, saveState } = require("../../shared/state");
|
||||||
const { decryptWithPassword } = require("../../shared/vault");
|
const { decryptWithPassword } = require("../../shared/vault");
|
||||||
@@ -76,9 +75,6 @@ function show(walletIdx) {
|
|||||||
clear();
|
clear();
|
||||||
deleteWalletIndex = walletIdx;
|
deleteWalletIndex = walletIdx;
|
||||||
$("delete-wallet-name").textContent = displayName(walletIdx);
|
$("delete-wallet-name").textContent = displayName(walletIdx);
|
||||||
$("delete-wallet-key-material").textContent = keyMaterialName(
|
|
||||||
state.wallets[walletIdx],
|
|
||||||
);
|
|
||||||
showView("delete-wallet-confirm");
|
showView("delete-wallet-confirm");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,9 +94,6 @@ function showLostPassword() {
|
|||||||
clearLostPassword();
|
clearLostPassword();
|
||||||
$("delete-wallet-lost-name").textContent = name;
|
$("delete-wallet-lost-name").textContent = name;
|
||||||
$("delete-wallet-lost-name-echo").textContent = name;
|
$("delete-wallet-lost-name-echo").textContent = name;
|
||||||
$("delete-wallet-lost-key-material").textContent = keyMaterialName(
|
|
||||||
state.wallets[walletIdx],
|
|
||||||
);
|
|
||||||
// showView() runs the leave hook of delete-wallet-confirm, which nulls
|
// showView() runs the leave hook of delete-wallet-confirm, which nulls
|
||||||
// deleteWalletIndex, so this screen's own selection is recorded after
|
// deleteWalletIndex, so this screen's own selection is recorded after
|
||||||
// it and not before.
|
// it and not before.
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ const { isDebug } = require("../../shared/log");
|
|||||||
const { formatUsd, getPrice } = require("../../shared/prices");
|
const { formatUsd, getPrice } = require("../../shared/prices");
|
||||||
const { state, saveState, currentNetwork } = require("../../shared/state");
|
const { state, saveState, currentNetwork } = require("../../shared/state");
|
||||||
const { displaySymbol } = require("../../shared/symbolDisplay");
|
const { displaySymbol } = require("../../shared/symbolDisplay");
|
||||||
const { walletHasRecoveryPhrase } = require("../../shared/wallet");
|
|
||||||
const { markViewRendered } = require("../viewRouter");
|
const { markViewRendered } = require("../viewRouter");
|
||||||
|
|
||||||
// When views are added, removed, or transitions between them change,
|
// When views are added, removed, or transitions between them change,
|
||||||
@@ -419,16 +418,6 @@ function addressHoldsFunds(addr) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The name of a wallet's key material, which is also its only backup: the
|
|
||||||
// recovery phrase of an HD wallet, the private key of a key wallet, the
|
|
||||||
// extended private key of an xprv wallet. DeleteWallet,
|
|
||||||
// DeleteWalletLostPassword and DeleteAddress name the backup with it.
|
|
||||||
function keyMaterialName(wallet) {
|
|
||||||
if (walletHasRecoveryPhrase(wallet)) return "recovery phrase";
|
|
||||||
if (wallet.type === "key") return "private key";
|
|
||||||
return "extended private key";
|
|
||||||
}
|
|
||||||
|
|
||||||
// The fewest characters of an address any caller may ask to display. The
|
// The fewest characters of an address any caller may ask to display. The
|
||||||
// 10-character cap inside truncateMiddle() is the other half of the same
|
// 10-character cap inside truncateMiddle() is the other half of the same
|
||||||
// guarantee; this is the half that used to be spelled out at each call
|
// guarantee; this is the half that used to be spelled out at each call
|
||||||
@@ -736,7 +725,6 @@ module.exports = {
|
|||||||
balanceLine,
|
balanceLine,
|
||||||
balanceLinesForAddress,
|
balanceLinesForAddress,
|
||||||
addressHoldsFunds,
|
addressHoldsFunds,
|
||||||
keyMaterialName,
|
|
||||||
unknownableAmount,
|
unknownableAmount,
|
||||||
nativeCurrency,
|
nativeCurrency,
|
||||||
tokenLabel,
|
tokenLabel,
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ const {
|
|||||||
} = require("../dustThreshold");
|
} = require("../dustThreshold");
|
||||||
const { state, saveState, currentNetwork } = require("../../shared/state");
|
const { state, saveState, currentNetwork } = require("../../shared/state");
|
||||||
const { onChainSwitch } = require("../../shared/chainSwitch");
|
const { onChainSwitch } = require("../../shared/chainSwitch");
|
||||||
|
const { restorableStack } = require("../../shared/persistedState");
|
||||||
|
const { resetRenderedViews, markViewRendered } = require("../viewRouter");
|
||||||
const {
|
const {
|
||||||
log,
|
log,
|
||||||
debugFetch,
|
debugFetch,
|
||||||
@@ -323,8 +325,21 @@ function init(ctx) {
|
|||||||
// Open pages are told by the background, as after a site's approved
|
// Open pages are told by the background, as after a site's approved
|
||||||
// switch request.
|
// switch request.
|
||||||
notify({ type: "AUTISTMASK_NETWORK_CHANGED", chainId: net.chainId });
|
notify({ type: "AUTISTMASK_NETWORK_CHANGED", chainId: net.chainId });
|
||||||
|
// The screens beneath Settings show the previous network, so Back
|
||||||
|
// finds them as it does in a reopened popup: each is rendered again
|
||||||
|
// when Back lands on it, and the stack is cut at the first one that
|
||||||
|
// cannot be rendered from saved state, such as Send or AddToken.
|
||||||
|
// Without a wallet the stack starts at Welcome, which that cut would
|
||||||
|
// drop, and Welcome shows nothing from a network.
|
||||||
|
if (state.hasWallet) {
|
||||||
|
state.viewStack = restorableStack(state.viewStack, "settings");
|
||||||
|
}
|
||||||
|
resetRenderedViews();
|
||||||
|
// Settings itself is brought up to date for the new network here.
|
||||||
|
markViewRendered("settings");
|
||||||
$("settings-rpc").value = state.rpcUrl;
|
$("settings-rpc").value = state.rpcUrl;
|
||||||
$("settings-blockscout").value = state.blockscoutUrl;
|
$("settings-blockscout").value = state.blockscoutUrl;
|
||||||
|
renderTrackedTokens();
|
||||||
showFlash("Switched to " + net.name + ".");
|
showFlash("Switched to " + net.name + ".");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,12 @@ function show() {
|
|||||||
$("settings-addtoken-address").value = "";
|
$("settings-addtoken-address").value = "";
|
||||||
$("settings-addtoken-info").textContent = "";
|
$("settings-addtoken-info").textContent = "";
|
||||||
$("settings-addtoken-info").style.visibility = "hidden";
|
$("settings-addtoken-info").style.visibility = "hidden";
|
||||||
|
// The bundled list holds Ethereum Mainnet contracts, which name no token
|
||||||
|
// on any other network.
|
||||||
|
$("settings-addtoken-common").classList.toggle(
|
||||||
|
"hidden",
|
||||||
|
state.networkId !== "mainnet",
|
||||||
|
);
|
||||||
renderTop10();
|
renderTop10();
|
||||||
renderDropdown();
|
renderDropdown();
|
||||||
showView("settings-addtoken");
|
showView("settings-addtoken");
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const {
|
|||||||
displaySymbol,
|
displaySymbol,
|
||||||
goBack,
|
goBack,
|
||||||
} = require("./helpers");
|
} = require("./helpers");
|
||||||
const { state, currentNetwork } = require("../../shared/state");
|
const { state } = require("../../shared/state");
|
||||||
const { nativeCurrencyByChainId } = require("../../shared/networks");
|
const { nativeCurrencyByChainId } = require("../../shared/networks");
|
||||||
const { formatEther, formatUnits } = require("ethers");
|
const { formatEther, formatUnits } = require("ethers");
|
||||||
const { log, debugFetch } = require("../../shared/log");
|
const { log, debugFetch } = require("../../shared/log");
|
||||||
@@ -316,12 +316,7 @@ async function loadFullTxDetails(txHash, chainId) {
|
|||||||
// Decoded against the address the transaction was sent to, as the
|
// Decoded against the address the transaction was sent to, as the
|
||||||
// explorer reports it, not the history entry's `to`: for a token
|
// explorer reports it, not the history entry's `to`: for a token
|
||||||
// transfer that is the transfer's recipient, not the token contract.
|
// transfer that is the transfer's recipient, not the token contract.
|
||||||
// On the active network, the one whose explorer was just asked.
|
const decoded = decodeCalldata(inputData, txData.to?.hash || "");
|
||||||
const decoded = decodeCalldata(
|
|
||||||
inputData,
|
|
||||||
txData.to?.hash || "",
|
|
||||||
currentNetwork().chainId,
|
|
||||||
);
|
|
||||||
if (decoded) {
|
if (decoded) {
|
||||||
// Render decoded calldata matching approval view style
|
// Render decoded calldata matching approval view style
|
||||||
actionEl.textContent = decoded.name;
|
actionEl.textContent = decoded.name;
|
||||||
|
|||||||
@@ -38,6 +38,16 @@ function applyChainSwitchFields(s, newNetworkId) {
|
|||||||
blockscoutUrl: s.blockscoutUrl,
|
blockscoutUrl: s.blockscoutUrl,
|
||||||
};
|
};
|
||||||
const remembered = s.networkEndpoints[net.id] || {};
|
const remembered = s.networkEndpoints[net.id] || {};
|
||||||
|
|
||||||
|
// Tracked tokens are kept per network the same way, because a contract
|
||||||
|
// address names a token on one network only. s.trackedTokens stays the
|
||||||
|
// active network's list, so nothing that reads it changes: the outgoing
|
||||||
|
// list is put away under its network, and the incoming network's list, or
|
||||||
|
// an empty one, takes its place.
|
||||||
|
if (!s.networkTrackedTokens) s.networkTrackedTokens = {};
|
||||||
|
s.networkTrackedTokens[s.networkId] = s.trackedTokens;
|
||||||
|
s.trackedTokens = s.networkTrackedTokens[net.id] || [];
|
||||||
|
|
||||||
s.networkId = net.id;
|
s.networkId = net.id;
|
||||||
s.rpcUrl = remembered.rpcUrl || net.defaultRpcUrl;
|
s.rpcUrl = remembered.rpcUrl || net.defaultRpcUrl;
|
||||||
s.blockscoutUrl = remembered.blockscoutUrl || net.defaultBlockscoutUrl;
|
s.blockscoutUrl = remembered.blockscoutUrl || net.defaultBlockscoutUrl;
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ const DEFAULT_STATE = {
|
|||||||
hasWallet: false,
|
hasWallet: false,
|
||||||
wallets: [],
|
wallets: [],
|
||||||
trackedTokens: [],
|
trackedTokens: [],
|
||||||
|
// Tracked tokens remembered per network: { [networkId]: [token, ...] }.
|
||||||
|
// trackedTokens above is the active network's list; this is what the
|
||||||
|
// others are restored from when the active network changes. See
|
||||||
|
// applyChainSwitchFields().
|
||||||
|
networkTrackedTokens: {},
|
||||||
networkId: "mainnet",
|
networkId: "mainnet",
|
||||||
rpcUrl: DEFAULT_RPC_URL,
|
rpcUrl: DEFAULT_RPC_URL,
|
||||||
blockscoutUrl: DEFAULT_BLOCKSCOUT_URL,
|
blockscoutUrl: DEFAULT_BLOCKSCOUT_URL,
|
||||||
@@ -289,6 +294,20 @@ function normalizePersisted(saved) {
|
|||||||
// profile whose wallets are perfectly fine. An empty list is a legitimate
|
// profile whose wallets are perfectly fine. An empty list is a legitimate
|
||||||
// value and survives.
|
// value and survives.
|
||||||
out.trackedTokens = structuredClone(tokenRefs(saved.trackedTokens));
|
out.trackedTokens = structuredClone(tokenRefs(saved.trackedTokens));
|
||||||
|
// Each remembered list is floored like trackedTokens, because
|
||||||
|
// applyChainSwitchFields() puts it straight back onto trackedTokens when
|
||||||
|
// its network becomes the active one. A key that is not a network in
|
||||||
|
// networks.js is dropped, which also keeps an own "__proto__" key from
|
||||||
|
// reaching the plain assignment below.
|
||||||
|
out.networkTrackedTokens = {};
|
||||||
|
if (isRecord(saved.networkTrackedTokens)) {
|
||||||
|
for (const netId of Object.keys(saved.networkTrackedTokens)) {
|
||||||
|
if (!isKnownNetworkId(netId)) continue;
|
||||||
|
out.networkTrackedTokens[netId] = structuredClone(
|
||||||
|
tokenRefs(saved.networkTrackedTokens[netId]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
// The loud refusal for an unknown id is assertStateUsable(); this is the
|
// The loud refusal for an unknown id is assertStateUsable(); this is the
|
||||||
// floor under it. networkId is an object KEY into networkEndpoints below,
|
// floor under it. networkId is an object KEY into networkEndpoints below,
|
||||||
// so a value that is not a network in networks.js must never get that far
|
// so a value that is not a network in networks.js must never get that far
|
||||||
|
|||||||
+12
-8
@@ -42,9 +42,11 @@ const { log } = require("./log");
|
|||||||
// and writes THIS object, never the proxy: the guard is for callers.
|
// and writes THIS object, never the proxy: the guard is for callers.
|
||||||
const rawState = {
|
const rawState = {
|
||||||
...DEFAULT_STATE,
|
...DEFAULT_STATE,
|
||||||
// Its own object, not the one DEFAULT_STATE holds: applyChainSwitchFields()
|
// Their own objects, not the ones DEFAULT_STATE holds:
|
||||||
// mutates this map in place, and a spread copies the reference.
|
// applyChainSwitchFields() mutates these maps in place, and a spread copies
|
||||||
|
// the reference.
|
||||||
networkEndpoints: {},
|
networkEndpoints: {},
|
||||||
|
networkTrackedTokens: {},
|
||||||
currentView: null,
|
currentView: null,
|
||||||
selectedWallet: null,
|
selectedWallet: null,
|
||||||
selectedAddress: null,
|
selectedAddress: null,
|
||||||
@@ -438,12 +440,14 @@ function mergeNetworkEndpoints(base, ours, theirs) {
|
|||||||
//
|
//
|
||||||
// Every other persisted field stays a whole-field diff:
|
// Every other persisted field stays a whole-field diff:
|
||||||
// `trackedTokens`/`fraudContracts`/`viewStack` are arrays of scalars with no
|
// `trackedTokens`/`fraudContracts`/`viewStack` are arrays of scalars with no
|
||||||
// per-element identity to merge by; `tokenHolderCache` is a map shaped like
|
// per-element identity to merge by; `networkTrackedTokens` is diffed whole like
|
||||||
// the ones above, but nothing in src/ ever writes an entry into it — it is
|
// `trackedTokens` and `networkId`, the two fields a network switch moves
|
||||||
// only ever reset wholesale to `{}` (applyChainSwitchFields()) — so there is
|
// together with it; `tokenHolderCache` is a map shaped like the ones above,
|
||||||
// no in-place mutation for a whole-field diff to collide with; `viewData` is
|
// but nothing in src/ ever writes an entry into it — it is only ever reset
|
||||||
// this page's own UI scratch space, not data another page has any reason to
|
// wholesale to `{}` (applyChainSwitchFields()) — so there is no in-place
|
||||||
// share membership of.
|
// mutation for a whole-field diff to collide with; `viewData` is this page's
|
||||||
|
// own UI scratch space, not data another page has any reason to share
|
||||||
|
// membership of.
|
||||||
//
|
//
|
||||||
// This does not make two pages that both change the SAME leaf concurrently
|
// This does not make two pages that both change the SAME leaf concurrently
|
||||||
// safe: last write wins on that one leaf, same as before. What it removes
|
// safe: last write wins on that one leaf, same as before. What it removes
|
||||||
|
|||||||
+4
-45
@@ -8,7 +8,6 @@ const {
|
|||||||
resolveTokenSymbol,
|
resolveTokenSymbol,
|
||||||
unknownDecimalsAmount,
|
unknownDecimalsAmount,
|
||||||
} = require("./approvalAmount");
|
} = require("./approvalAmount");
|
||||||
const { NETWORKS } = require("./networks");
|
|
||||||
|
|
||||||
const coder = AbiCoder.defaultAbiCoder();
|
const coder = AbiCoder.defaultAbiCoder();
|
||||||
|
|
||||||
@@ -16,41 +15,6 @@ const ROUTER_IFACE = new Interface([
|
|||||||
"function execute(bytes commands, bytes[] inputs, uint256 deadline)",
|
"function execute(bytes commands, bytes[] inputs, uint256 deadline)",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// The Universal Router deployments on each network, by chain id, copied from
|
|
||||||
// the deployment files Uniswap publishes, each address named by its key there.
|
|
||||||
// The files also list `UnsupportedProtocol`, which is not a router.
|
|
||||||
const ROUTER_ADDRESSES = new Map([
|
|
||||||
[
|
|
||||||
NETWORKS.mainnet.chainId,
|
|
||||||
// https://github.com/Uniswap/universal-router/blob/543e1a19d6e21e31ced2512eec5792b50f13a0ba/deploy-addresses/mainnet.json
|
|
||||||
[
|
|
||||||
"0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B", // UniversalRouterV1
|
|
||||||
"0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", // UniversalRouterV1_2_V2Support
|
|
||||||
"0x66a9893cc07d91d95644aedd05d03f95e1dba8af", // UniversalRouterV2
|
|
||||||
"0x4C82D1fBFe28C977cBB58D8C7FF8FCF9F70a2cCA", // UniversalRouterV2_1_1
|
|
||||||
"0x23617e59A5925b2A4Bf75d73ff6711cD0b29De85", // UniversalRouterV2_1_2
|
|
||||||
],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
NETWORKS.sepolia.chainId,
|
|
||||||
// https://github.com/Uniswap/universal-router/blob/543e1a19d6e21e31ced2512eec5792b50f13a0ba/deploy-addresses/sepolia.json
|
|
||||||
[
|
|
||||||
"0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", // UniversalRouterV1_2_V2Support
|
|
||||||
"0x3a9d48ab9751398bbfa63ad67599bb04e4bdf98b", // UniversalRouterV2
|
|
||||||
"0x7DfD4F31be6814D2906BDE155c3e1B146EAc1468", // UniversalRouterV2_1_1
|
|
||||||
"0x7E4f6c5e954Da5c61B3423D81E2277431Ac043f3", // UniversalRouterV2_1_2
|
|
||||||
],
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Whether `address` is a Universal Router deployment on the network with this
|
|
||||||
// chain id. Address case is ignored.
|
|
||||||
function isRouterAddress(address, chainId) {
|
|
||||||
const routers = ROUTER_ADDRESSES.get(chainId) || [];
|
|
||||||
const wanted = (address || "").toLowerCase();
|
|
||||||
return routers.some((router) => router.toLowerCase() === wanted);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Universal Router command IDs (lower 5 bits of each command byte)
|
// Universal Router command IDs (lower 5 bits of each command byte)
|
||||||
const COMMAND_NAMES = {
|
const COMMAND_NAMES = {
|
||||||
0x00: "V3 Swap (Exact In)",
|
0x00: "V3 Swap (Exact In)",
|
||||||
@@ -156,7 +120,7 @@ const NO_MINIMUM = "None (no minimum guaranteed)";
|
|||||||
const MAX_UINT160 = BigInt("0xffffffffffffffffffffffffffffffffffffffff");
|
const MAX_UINT160 = BigInt("0xffffffffffffffffffffffffffffffffffffffff");
|
||||||
|
|
||||||
// WETH, the token UNWRAP_WETH turns into ETH: on mainnet, then on Sepolia.
|
// WETH, the token UNWRAP_WETH turns into ETH: on mainnet, then on Sepolia.
|
||||||
// Either one counts on either network.
|
// decode() is not told the network, so it takes either.
|
||||||
const WETH_ADDRESSES = [
|
const WETH_ADDRESSES = [
|
||||||
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
|
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
|
||||||
"0xfff9976782d46cc05630d1f6ebab18b2324d6b14",
|
"0xfff9976782d46cc05630d1f6ebab18b2324d6b14",
|
||||||
@@ -595,15 +559,10 @@ function decodeV4Swap(input) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to decode a Universal Router execute() call sent to `toAddress` on the
|
// Try to decode a Universal Router execute() call.
|
||||||
// network with chain id `chainId`.
|
|
||||||
// Returns { name, description, details } matching the format used by
|
// Returns { name, description, details } matching the format used by
|
||||||
// the approval UI, or null if the calldata is not a recognised execute() or
|
// the approval UI, or null if the calldata is not a recognised execute().
|
||||||
// `toAddress` is not a Universal Router on that network: the same calldata
|
function decode(data, toAddress, sources) {
|
||||||
// sent to any other contract is not a swap through the router, whatever that
|
|
||||||
// contract does with it.
|
|
||||||
function decode(data, toAddress, chainId, sources) {
|
|
||||||
if (!isRouterAddress(toAddress, chainId)) return null;
|
|
||||||
try {
|
try {
|
||||||
const parsed = ROUTER_IFACE.parseTransaction({ data });
|
const parsed = ROUTER_IFACE.parseTransaction({ data });
|
||||||
if (!parsed) return null;
|
if (!parsed) return null;
|
||||||
|
|||||||
@@ -68,11 +68,7 @@ function swapData(amountIn, amountOutMin) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function swapDetail(amountIn, amountOutMin, label) {
|
function swapDetail(amountIn, amountOutMin, label) {
|
||||||
const decoded = uniswap.decode(
|
const decoded = uniswap.decode(swapData(amountIn, amountOutMin), ROUTER);
|
||||||
swapData(amountIn, amountOutMin),
|
|
||||||
ROUTER,
|
|
||||||
"0x1",
|
|
||||||
);
|
|
||||||
return decoded.details.find((d) => d.label === label);
|
return decoded.details.find((d) => d.label === label);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -204,17 +204,21 @@ describe("a chain switch on a worker that never loaded state", () => {
|
|||||||
expect(after.rpcUrl).toBe(SEPOLIA.defaultRpcUrl);
|
expect(after.rpcUrl).toBe(SEPOLIA.defaultRpcUrl);
|
||||||
|
|
||||||
// And it took nothing else with it. Without the load these come back
|
// And it took nothing else with it. Without the load these come back
|
||||||
// as [], {}, [] and "system" from DEFAULT_STATE — every wallet in the
|
// as [], {}, {} and "system" from DEFAULT_STATE — every wallet in the
|
||||||
// extension gone, encrypted secrets included.
|
// extension gone, encrypted secrets included.
|
||||||
expect(after.wallets).toEqual(walletFixture());
|
expect(after.wallets).toEqual(walletFixture());
|
||||||
expect(after.hasWallet).toBe(true);
|
expect(after.hasWallet).toBe(true);
|
||||||
expect(after.activeAddress).toBe(ADDRESS);
|
expect(after.activeAddress).toBe(ADDRESS);
|
||||||
expect(after.allowedSites).toEqual({ [ADDRESS]: [CONNECTED_ORIGIN] });
|
expect(after.allowedSites).toEqual({ [ADDRESS]: [CONNECTED_ORIGIN] });
|
||||||
expect(after.trackedTokens).toEqual([
|
expect(after.networkTrackedTokens).toEqual({
|
||||||
{ address: TOKEN, symbol: "DAI", decimals: 18 },
|
mainnet: [{ address: TOKEN, symbol: "DAI", decimals: 18 }],
|
||||||
]);
|
});
|
||||||
expect(after.theme).toBe("dark");
|
expect(after.theme).toBe("dark");
|
||||||
|
|
||||||
|
// The mainnet token is tracked on mainnet only: Sepolia starts with
|
||||||
|
// none of its own.
|
||||||
|
expect(after.trackedTokens).toEqual([]);
|
||||||
|
|
||||||
// The user's mainnet endpoint is remembered rather than replaced by
|
// The user's mainnet endpoint is remembered rather than replaced by
|
||||||
// the public default, so switching back returns it.
|
// the public default, so switching back returns it.
|
||||||
expect(after.networkEndpoints.mainnet).toEqual({
|
expect(after.networkEndpoints.mainnet).toEqual({
|
||||||
@@ -226,6 +230,9 @@ describe("a chain switch on a worker that never loaded state", () => {
|
|||||||
expect(bg.persisted().rpcUrl).toBe(CUSTOM_RPC);
|
expect(bg.persisted().rpcUrl).toBe(CUSTOM_RPC);
|
||||||
expect(bg.persisted().blockscoutUrl).toBe(CUSTOM_BLOCKSCOUT);
|
expect(bg.persisted().blockscoutUrl).toBe(CUSTOM_BLOCKSCOUT);
|
||||||
expect(bg.persisted().wallets).toEqual(walletFixture());
|
expect(bg.persisted().wallets).toEqual(walletFixture());
|
||||||
|
expect(bg.persisted().trackedTokens).toEqual([
|
||||||
|
{ address: TOKEN, symbol: "DAI", decimals: 18 },
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("compares the requested chain against the stored one, not the default", async () => {
|
test("compares the requested chain against the stored one, not the default", async () => {
|
||||||
|
|||||||
@@ -259,36 +259,6 @@ describe("reaching the screen", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Both screens tell the user what they need to get the wallet back, and a
|
|
||||||
// key or xprv wallet has no recovery phrase: telling its owner to rely on
|
|
||||||
// one sends them looking for words that do not exist
|
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/529).
|
|
||||||
describe("what the screens say the wallet's backup is", () => {
|
|
||||||
test.each([
|
|
||||||
["hd", "recovery phrase"],
|
|
||||||
["key", "private key"],
|
|
||||||
["xprv", "extended private key"],
|
|
||||||
])(
|
|
||||||
"a wallet of type %s is told about its %s",
|
|
||||||
async (type, keyMaterial) => {
|
|
||||||
const { deleteWallet, state } = load();
|
|
||||||
state.wallets[1].type = type;
|
|
||||||
|
|
||||||
deleteWallet.show(1);
|
|
||||||
expect(state.currentView).toBe("delete-wallet-confirm");
|
|
||||||
expect(node("delete-wallet-key-material").textContent).toBe(
|
|
||||||
keyMaterial,
|
|
||||||
);
|
|
||||||
|
|
||||||
await click("btn-delete-wallet-lost-password");
|
|
||||||
expect(state.currentView).toBe(VIEW);
|
|
||||||
expect(node("delete-wallet-lost-key-material").textContent).toBe(
|
|
||||||
keyMaterial,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("the typed confirmation", () => {
|
describe("the typed confirmation", () => {
|
||||||
test("a name that is not the wallet's deletes nothing", async () => {
|
test("a name that is not the wallet's deletes nothing", async () => {
|
||||||
const { deleteWallet, state, storage } = load();
|
const { deleteWallet, state, storage } = load();
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ function erc20AmountLine(data, tokenAddress) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function swapAmountLine(data) {
|
function swapAmountLine(data) {
|
||||||
return decodeCalldata(data, ROUTER, "0x1").details.find(
|
return decodeCalldata(data, ROUTER).details.find(
|
||||||
(d) => d.label === "Amount",
|
(d) => d.label === "Amount",
|
||||||
).value;
|
).value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,12 +61,16 @@ async function importOn(page, { tab, field, secret }) {
|
|||||||
await page.click("btn-add-wallet-confirm");
|
await page.click("btn-add-wallet-confirm");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fake timers hold the timer of the flash message an import ends with, which
|
||||||
|
// would otherwise fire after cleanupPopup() has removed the page and throw.
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
chainBalance = "0.0";
|
chainBalance = "0.0";
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanupPopup();
|
cleanupPopup();
|
||||||
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("a popup that opened on Welcome", () => {
|
describe("a popup that opened on Welcome", () => {
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ const {
|
|||||||
normalizePersisted,
|
normalizePersisted,
|
||||||
} = require("../src/shared/persistedState");
|
} = require("../src/shared/persistedState");
|
||||||
const { stateProblem } = require("../src/shared/stateSchema");
|
const { stateProblem } = require("../src/shared/stateSchema");
|
||||||
|
const { isKnownNetworkId } = require("../src/shared/networks");
|
||||||
const { RESTORABLE_VIEWS } = require("../src/shared/restorableViews");
|
const { RESTORABLE_VIEWS } = require("../src/shared/restorableViews");
|
||||||
const {
|
const {
|
||||||
bootPopup,
|
bootPopup,
|
||||||
@@ -193,6 +194,29 @@ const CONTRACT = [
|
|||||||
floorOnly: [[1, 2], [null], [{}], [[TOKEN_ADDRESS]]],
|
floorOnly: [[1, 2], [null], [{}], [[TOKEN_ADDRESS]]],
|
||||||
holds: (v) => everyEntry(v, (t) => isRecord(t) && isText(t.address)),
|
holds: (v) => everyEntry(v, (t) => isRecord(t) && isText(t.address)),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: "networkTrackedTokens",
|
||||||
|
kind: KIND.ENTRIES,
|
||||||
|
hostile: [42, "notarecord", { sepolia: "notalist" }],
|
||||||
|
floorOnly: [
|
||||||
|
[1, 2],
|
||||||
|
{ sepolia: [1, 2] },
|
||||||
|
{ mainnet: [null, {}, [TOKEN_ADDRESS]] },
|
||||||
|
{ notanetwork: [{ address: TOKEN_ADDRESS }] },
|
||||||
|
JSON.parse('{"__proto__":[{"address":"0xabc"}]}'),
|
||||||
|
],
|
||||||
|
// Each list is put straight back onto trackedTokens by
|
||||||
|
// applyChainSwitchFields() when its network becomes the active one, so
|
||||||
|
// it is floored the same way; a key that names no network is dropped.
|
||||||
|
holds: (v) =>
|
||||||
|
isRecord(v) &&
|
||||||
|
Object.getPrototypeOf(v) === Object.prototype &&
|
||||||
|
Object.keys(v).every(
|
||||||
|
(id) =>
|
||||||
|
isKnownNetworkId(id) &&
|
||||||
|
everyEntry(v[id], (t) => isRecord(t) && isText(t.address)),
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: "allowedSites",
|
field: "allowedSites",
|
||||||
kind: KIND.ENTRIES,
|
kind: KIND.ENTRIES,
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
// A timer the popup started and that fires after cleanupPopup() has removed
|
|
||||||
// the page throws "document is not defined", and the throw fails whatever test
|
|
||||||
// the jest worker is running by then, in this file or another
|
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/578). The flash message's
|
|
||||||
// timer is the one that did it. A throw from a timer during a test fails that
|
|
||||||
// test, so each test here passes only if nothing throws.
|
|
||||||
|
|
||||||
const {
|
|
||||||
bootPopup,
|
|
||||||
cleanupPopup,
|
|
||||||
unversionedValidProfile,
|
|
||||||
} = require("./support/popupBoot");
|
|
||||||
|
|
||||||
const FLASH_MS = 10;
|
|
||||||
|
|
||||||
function wait(ms) {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
cleanupPopup();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a flash message clears while the page is up", async () => {
|
|
||||||
const page = await bootPopup(unversionedValidProfile());
|
|
||||||
const { showFlash } = require("../src/popup/views/helpers");
|
|
||||||
showFlash("Copied.", FLASH_MS);
|
|
||||||
expect(page.text("flash-msg")).toBe("Copied.");
|
|
||||||
|
|
||||||
await wait(FLASH_MS * 5);
|
|
||||||
|
|
||||||
expect(page.text("flash-msg")).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a flash message's timer does not fire after cleanupPopup()", async () => {
|
|
||||||
await bootPopup(unversionedValidProfile());
|
|
||||||
const { showFlash } = require("../src/popup/views/helpers");
|
|
||||||
showFlash("Copied.", FLASH_MS);
|
|
||||||
cleanupPopup();
|
|
||||||
|
|
||||||
await wait(FLASH_MS * 5);
|
|
||||||
});
|
|
||||||
@@ -43,12 +43,6 @@ jest.doMock("ethereum-blockies-base64", () => makeBlockie);
|
|||||||
// Taken before any boot replaces them; see bootPopup().
|
// Taken before any boot replaces them; see bootPopup().
|
||||||
const realSetInterval = globalThis.setInterval;
|
const realSetInterval = globalThis.setInterval;
|
||||||
const realClearInterval = globalThis.clearInterval;
|
const realClearInterval = globalThis.clearInterval;
|
||||||
const realSetTimeout = globalThis.setTimeout;
|
|
||||||
const realClearTimeout = globalThis.clearTimeout;
|
|
||||||
|
|
||||||
// Every timeout set since the last cleanupPopup() that has neither fired nor
|
|
||||||
// been cleared. cleanupPopup() clears them all.
|
|
||||||
const pendingTimeouts = new Set();
|
|
||||||
|
|
||||||
const POPUP_HTML = fs.readFileSync(
|
const POPUP_HTML = fs.readFileSync(
|
||||||
path.join(__dirname, "..", "..", "src", "popup", "index.html"),
|
path.join(__dirname, "..", "..", "src", "popup", "index.html"),
|
||||||
@@ -348,24 +342,6 @@ async function bootPopup(stored, options) {
|
|||||||
globalThis.clearInterval = (id) => {
|
globalThis.clearInterval = (id) => {
|
||||||
intervals.delete(id);
|
intervals.delete(id);
|
||||||
};
|
};
|
||||||
// Timeouts, such as the one that clears a flash message, run for real, but
|
|
||||||
// each is recorded until it fires or is cleared, so cleanupPopup() can
|
|
||||||
// clear the ones still pending. One that fired after the page was removed
|
|
||||||
// would throw "document is not defined" into whatever test the worker runs
|
|
||||||
// by then. jest.useFakeTimers() in a test does not reach them. Put back by
|
|
||||||
// cleanupPopup().
|
|
||||||
globalThis.setTimeout = (fn, ms, ...args) => {
|
|
||||||
const timer = realSetTimeout(() => {
|
|
||||||
pendingTimeouts.delete(timer);
|
|
||||||
fn(...args);
|
|
||||||
}, ms);
|
|
||||||
pendingTimeouts.add(timer);
|
|
||||||
return timer;
|
|
||||||
};
|
|
||||||
globalThis.clearTimeout = (timer) => {
|
|
||||||
pendingTimeouts.delete(timer);
|
|
||||||
realClearTimeout(timer);
|
|
||||||
};
|
|
||||||
|
|
||||||
require("../../src/popup/index");
|
require("../../src/popup/index");
|
||||||
|
|
||||||
@@ -431,10 +407,6 @@ function cleanupPopup() {
|
|||||||
delete globalThis.window;
|
delete globalThis.window;
|
||||||
globalThis.setInterval = realSetInterval;
|
globalThis.setInterval = realSetInterval;
|
||||||
globalThis.clearInterval = realClearInterval;
|
globalThis.clearInterval = realClearInterval;
|
||||||
for (const timer of pendingTimeouts) realClearTimeout(timer);
|
|
||||||
pendingTimeouts.clear();
|
|
||||||
globalThis.setTimeout = realSetTimeout;
|
|
||||||
globalThis.clearTimeout = realClearTimeout;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
// Tracked tokens are kept per network, and the two add-token screens offer the
|
||||||
|
// bundled token list, which holds Ethereum Mainnet contracts, on Ethereum
|
||||||
|
// Mainnet only (https://git.eeqj.de/sneak/AutistMask/issues/523), including on
|
||||||
|
// the screen "Back" lands on after a switch made from the Settings gear. Driven
|
||||||
|
// through the real popup over a stored profile; the same switch made by the
|
||||||
|
// background is in tests/coldWorkerChainSwitch.test.js.
|
||||||
|
|
||||||
|
const {
|
||||||
|
bootPopup,
|
||||||
|
cleanupPopup,
|
||||||
|
unversionedValidProfile,
|
||||||
|
TOKEN_ADDRESS,
|
||||||
|
} = require("./support/popupBoot");
|
||||||
|
|
||||||
|
const SEPOLIA_TOKEN = {
|
||||||
|
address: TOKEN_ADDRESS,
|
||||||
|
symbol: "TST",
|
||||||
|
decimals: 18,
|
||||||
|
name: "Test Token",
|
||||||
|
};
|
||||||
|
const MAINNET_TOKEN = {
|
||||||
|
address: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||||
|
symbol: "DAI",
|
||||||
|
decimals: 18,
|
||||||
|
name: "Dai Stablecoin",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fake timers hold the timer of the flash message a switch ends with, which
|
||||||
|
// would otherwise fire after cleanupPopup() has removed the page and throw.
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupPopup();
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
// The popup reopened onto SettingsAddToken, on `networkId`.
|
||||||
|
function bootOnAddToken(networkId, extra) {
|
||||||
|
return bootPopup(
|
||||||
|
unversionedValidProfile({
|
||||||
|
networkId,
|
||||||
|
currentView: "settings-addtoken",
|
||||||
|
...extra,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The popup reopened onto AddressDetail on Ethereum Mainnet, with DAI tracked
|
||||||
|
// there and TST tracked on Sepolia.
|
||||||
|
function bootOnAddress() {
|
||||||
|
return bootPopup(
|
||||||
|
unversionedValidProfile({
|
||||||
|
currentView: "address",
|
||||||
|
selectedWallet: 0,
|
||||||
|
selectedAddress: 0,
|
||||||
|
trackedTokens: [MAINNET_TOKEN],
|
||||||
|
networkTrackedTokens: { sepolia: [SEPOLIA_TOKEN] },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Picks `networkId` in the Settings network selector, as the user does.
|
||||||
|
async function chooseNetwork(boot, networkId) {
|
||||||
|
const select = boot.node("settings-network");
|
||||||
|
select.value = networkId;
|
||||||
|
for (const fn of select.listeners.change) await fn();
|
||||||
|
await boot.settle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Settings gear, a switch to Sepolia, and Back.
|
||||||
|
async function gearSwitchBack(boot) {
|
||||||
|
await boot.click("btn-settings");
|
||||||
|
expect(boot.visibleViews()).toEqual(["settings"]);
|
||||||
|
await chooseNetwork(boot, "sepolia");
|
||||||
|
await boot.click("btn-settings-back");
|
||||||
|
}
|
||||||
|
|
||||||
|
describe.each([
|
||||||
|
["mainnet", false],
|
||||||
|
["sepolia", true],
|
||||||
|
])("on %s", (networkId, hidden) => {
|
||||||
|
test(`the common tokens are hidden: ${hidden}`, async () => {
|
||||||
|
const boot = await bootOnAddToken(networkId);
|
||||||
|
expect(boot.pageErrors).toEqual([]);
|
||||||
|
expect(boot.visibleViews()).toEqual(["settings-addtoken"]);
|
||||||
|
expect(boot.hidden("settings-addtoken-common")).toBe(hidden);
|
||||||
|
|
||||||
|
require("../src/popup/views/addToken").show();
|
||||||
|
expect(boot.visibleViews()).toEqual(["add-token"]);
|
||||||
|
expect(boot.hidden("add-token-common")).toBe(hidden);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a token added by contract address on Sepolia is tracked on Sepolia only", async () => {
|
||||||
|
const boot = await bootOnAddToken("sepolia", {
|
||||||
|
networkTrackedTokens: { mainnet: [MAINNET_TOKEN] },
|
||||||
|
});
|
||||||
|
const stored = () => boot.storage.read("autistmask");
|
||||||
|
const { lookupTokenInfo } = require("../src/shared/balances");
|
||||||
|
lookupTokenInfo.mockResolvedValueOnce({
|
||||||
|
symbol: SEPOLIA_TOKEN.symbol,
|
||||||
|
decimals: SEPOLIA_TOKEN.decimals,
|
||||||
|
name: SEPOLIA_TOKEN.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
boot.node("settings-addtoken-address").value = TOKEN_ADDRESS;
|
||||||
|
await boot.click("btn-settings-addtoken-manual");
|
||||||
|
|
||||||
|
expect(lookupTokenInfo).toHaveBeenCalledWith(
|
||||||
|
TOKEN_ADDRESS,
|
||||||
|
expect.any(String),
|
||||||
|
"sepolia",
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
expect(stored().trackedTokens).toEqual([SEPOLIA_TOKEN]);
|
||||||
|
|
||||||
|
await chooseNetwork(boot, "mainnet");
|
||||||
|
expect(stored().networkId).toBe("mainnet");
|
||||||
|
expect(stored().trackedTokens).toEqual([MAINNET_TOKEN]);
|
||||||
|
const onMainnet = boot.node("settings-tracked-tokens").innerHTML;
|
||||||
|
expect(onMainnet).toContain("DAI");
|
||||||
|
expect(onMainnet).not.toContain("TST");
|
||||||
|
|
||||||
|
await chooseNetwork(boot, "sepolia");
|
||||||
|
expect(stored().networkId).toBe("sepolia");
|
||||||
|
expect(stored().trackedTokens).toEqual([SEPOLIA_TOKEN]);
|
||||||
|
const onSepolia = boot.node("settings-tracked-tokens").innerHTML;
|
||||||
|
expect(onSepolia).toContain("TST");
|
||||||
|
expect(onSepolia).not.toContain("DAI");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Back after a switch from the Settings gear", () => {
|
||||||
|
test("SettingsAddToken no longer offers the common tokens", async () => {
|
||||||
|
const boot = await bootOnAddToken("mainnet");
|
||||||
|
expect(boot.hidden("settings-addtoken-common")).toBe(false);
|
||||||
|
|
||||||
|
await gearSwitchBack(boot);
|
||||||
|
expect(boot.visibleViews()).toEqual(["settings-addtoken"]);
|
||||||
|
expect(boot.hidden("settings-addtoken-common")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("AddressDetail lists the new network's tracked tokens", async () => {
|
||||||
|
const boot = await bootOnAddress();
|
||||||
|
expect(boot.node("address-balances").innerHTML).toContain("DAI");
|
||||||
|
|
||||||
|
await gearSwitchBack(boot);
|
||||||
|
expect(boot.visibleViews()).toEqual(["address"]);
|
||||||
|
const balances = boot.node("address-balances").innerHTML;
|
||||||
|
expect(balances).toContain("TST");
|
||||||
|
expect(balances).not.toContain("DAI");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Neither can be rendered from saved state, so neither is landed on.
|
||||||
|
test.each([
|
||||||
|
["btn-add-token", "add-token"],
|
||||||
|
["btn-send", "send"],
|
||||||
|
])("from over %s, it lands on AddressDetail", async (button, view) => {
|
||||||
|
const boot = await bootOnAddress();
|
||||||
|
await boot.click(button);
|
||||||
|
expect(boot.visibleViews()).toEqual([view]);
|
||||||
|
|
||||||
|
await gearSwitchBack(boot);
|
||||||
|
expect(boot.visibleViews()).toEqual(["address"]);
|
||||||
|
expect(boot.node("address-balances").innerHTML).not.toContain("DAI");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("without a wallet, it lands on Welcome", async () => {
|
||||||
|
const boot = await bootPopup(undefined);
|
||||||
|
expect(boot.visibleViews()).toEqual(["welcome"]);
|
||||||
|
|
||||||
|
await gearSwitchBack(boot);
|
||||||
|
expect(boot.visibleViews()).toEqual(["welcome"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a later Back onto Settings keeps what was typed there", async () => {
|
||||||
|
const boot = await bootOnAddress();
|
||||||
|
await boot.click("btn-settings");
|
||||||
|
await chooseNetwork(boot, "sepolia");
|
||||||
|
boot.node("settings-rpc").value = "https://typed.example";
|
||||||
|
|
||||||
|
await boot.click("btn-settings-add-token");
|
||||||
|
expect(boot.visibleViews()).toEqual(["settings-addtoken"]);
|
||||||
|
await boot.click("btn-settings-addtoken-back");
|
||||||
|
expect(boot.visibleViews()).toEqual(["settings"]);
|
||||||
|
expect(boot.value("settings-rpc")).toBe("https://typed.example");
|
||||||
|
});
|
||||||
|
});
|
||||||
+44
-47
@@ -1,7 +1,6 @@
|
|||||||
const { AbiCoder, Interface, solidityPacked } = require("ethers");
|
const { AbiCoder, Interface, solidityPacked } = require("ethers");
|
||||||
const uniswap = require("../src/shared/uniswap");
|
const uniswap = require("../src/shared/uniswap");
|
||||||
|
|
||||||
// A Universal Router on Ethereum Mainnet, chain id 0x1.
|
|
||||||
const ROUTER_ADDR = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af";
|
const ROUTER_ADDR = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af";
|
||||||
const USDT_ADDR = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
|
const USDT_ADDR = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
|
||||||
const WETH_ADDR = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
|
const WETH_ADDR = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
|
||||||
@@ -316,13 +315,13 @@ function detail(result, label) {
|
|||||||
|
|
||||||
describe("uniswap decoder", () => {
|
describe("uniswap decoder", () => {
|
||||||
test("returns null for non-execute calldata", () => {
|
test("returns null for non-execute calldata", () => {
|
||||||
expect(uniswap.decode("0x", ROUTER_ADDR, "0x1")).toBeNull();
|
expect(uniswap.decode("0x", ROUTER_ADDR)).toBeNull();
|
||||||
expect(uniswap.decode("0xdeadbeef", ROUTER_ADDR, "0x1")).toBeNull();
|
expect(uniswap.decode("0xdeadbeef", ROUTER_ADDR)).toBeNull();
|
||||||
expect(uniswap.decode(null, ROUTER_ADDR, "0x1")).toBeNull();
|
expect(uniswap.decode(null, ROUTER_ADDR)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("decodes first-ever AutistMask swap (PERMIT2_PERMIT + V4_SWAP)", () => {
|
test("decodes first-ever AutistMask swap (PERMIT2_PERMIT + V4_SWAP)", () => {
|
||||||
const result = uniswap.decode(FIRST_SWAP_CALLDATA, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(FIRST_SWAP_CALLDATA, ROUTER_ADDR);
|
||||||
|
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
expect(result.name).toBe("Swap USDT \u2192 ETH");
|
expect(result.name).toBe("Swap USDT \u2192 ETH");
|
||||||
@@ -362,7 +361,6 @@ describe("uniswap decoder", () => {
|
|||||||
const result = uniswap.decode(
|
const result = uniswap.decode(
|
||||||
v4SwapCalldata(id, deployed),
|
v4SwapCalldata(id, deployed),
|
||||||
ROUTER_ADDR,
|
ROUTER_ADDR,
|
||||||
"0x1",
|
|
||||||
);
|
);
|
||||||
expect(detail(result, "Steps").value).toBe("V4 Swap");
|
expect(detail(result, "Steps").value).toBe("V4 Swap");
|
||||||
},
|
},
|
||||||
@@ -378,7 +376,6 @@ describe("uniswap decoder", () => {
|
|||||||
const result = uniswap.decode(
|
const result = uniswap.decode(
|
||||||
v4SwapCalldata(id, newer),
|
v4SwapCalldata(id, newer),
|
||||||
ROUTER_ADDR,
|
ROUTER_ADDR,
|
||||||
"0x1",
|
|
||||||
);
|
);
|
||||||
expect(detail(result, "Amount")).toBeUndefined();
|
expect(detail(result, "Amount")).toBeUndefined();
|
||||||
expect(detail(result, "Min. received")).toBeUndefined();
|
expect(detail(result, "Min. received")).toBeUndefined();
|
||||||
@@ -409,7 +406,7 @@ describe("uniswap decoder", () => {
|
|||||||
],
|
],
|
||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(detail(result, "Steps").value).toBe(
|
expect(detail(result, "Steps").value).toBe(
|
||||||
"V4 Swap (could not be decoded)",
|
"V4 Swap (could not be decoded)",
|
||||||
);
|
);
|
||||||
@@ -430,7 +427,7 @@ describe("uniswap decoder", () => {
|
|||||||
[encodeV4Swap(new Uint8Array([id]), [newer])],
|
[encodeV4Swap(new Uint8Array([id]), [newer])],
|
||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(detail(result, "Steps").value).toBe(
|
expect(detail(result, "Steps").value).toBe(
|
||||||
"V4 Swap (could not be decoded)",
|
"V4 Swap (could not be decoded)",
|
||||||
);
|
);
|
||||||
@@ -460,7 +457,7 @@ describe("uniswap decoder", () => {
|
|||||||
],
|
],
|
||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result.name).toBe("Uniswap Swap");
|
expect(result.name).toBe("Uniswap Swap");
|
||||||
expect(detail(result, "Amount").value).toBe("2.0000 USDT");
|
expect(detail(result, "Amount").value).toBe("2.0000 USDT");
|
||||||
expect(detail(result, "Token Out").value).toBe(
|
expect(detail(result, "Token Out").value).toBe(
|
||||||
@@ -488,7 +485,7 @@ describe("uniswap decoder", () => {
|
|||||||
],
|
],
|
||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result.name).toBe("Uniswap Swap");
|
expect(result.name).toBe("Uniswap Swap");
|
||||||
expect(detail(result, "Token In").value).toBe(
|
expect(detail(result, "Token In").value).toBe(
|
||||||
"Unknown (could not be decoded)",
|
"Unknown (could not be decoded)",
|
||||||
@@ -510,7 +507,7 @@ describe("uniswap decoder", () => {
|
|||||||
],
|
],
|
||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(detail(result, "Token Out").value).toBe(
|
expect(detail(result, "Token Out").value).toBe(
|
||||||
"Unknown (could not be decoded)",
|
"Unknown (could not be decoded)",
|
||||||
);
|
);
|
||||||
@@ -530,7 +527,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
expect(result.name).toBe("Swap USDT \u2192 WETH");
|
expect(result.name).toBe("Swap USDT \u2192 WETH");
|
||||||
|
|
||||||
@@ -557,7 +554,7 @@ describe("uniswap decoder", () => {
|
|||||||
1767225600n,
|
1767225600n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result.name).toBe("Swap USDC → WETH");
|
expect(result.name).toBe("Swap USDC → WETH");
|
||||||
expect(detail(result, "Token In").address).toBe(USDC_ADDR);
|
expect(detail(result, "Token In").address).toBe(USDC_ADDR);
|
||||||
expect(detail(result, "Token Out").address).toBe(WETH_ADDR);
|
expect(detail(result, "Token Out").address).toBe(WETH_ADDR);
|
||||||
@@ -589,7 +586,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result.name).toBe("Swap ETH → USDC");
|
expect(result.name).toBe("Swap ETH → USDC");
|
||||||
|
|
||||||
const amount = detail(result, "Amount");
|
const amount = detail(result, "Amount");
|
||||||
@@ -618,7 +615,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result.name).toBe("Swap USDC → ETH");
|
expect(result.name).toBe("Swap USDC → ETH");
|
||||||
expect(detail(result, "Amount").value).toBe("Up to 1500.0000 USDC");
|
expect(detail(result, "Amount").value).toBe("Up to 1500.0000 USDC");
|
||||||
expect(detail(result, "Token Out").value).toBe("ETH");
|
expect(detail(result, "Token Out").value).toBe("ETH");
|
||||||
@@ -661,7 +658,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(detail(result, "Token Out").address).toBe(tokenOut);
|
expect(detail(result, "Token Out").address).toBe(tokenOut);
|
||||||
expect(detail(result, "Min. received").value).toBe(minReceived);
|
expect(detail(result, "Min. received").value).toBe(minReceived);
|
||||||
},
|
},
|
||||||
@@ -682,7 +679,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result.name).toBe("Swap USDT → USDC");
|
expect(result.name).toBe("Swap USDT → USDC");
|
||||||
expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
|
expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
|
||||||
expect(detail(result, "Min. received").value).toBe("1.9000 USDC");
|
expect(detail(result, "Min. received").value).toBe("1.9000 USDC");
|
||||||
@@ -715,7 +712,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(detail(result, "Token Out").value).toBe("ETH");
|
expect(detail(result, "Token Out").value).toBe("ETH");
|
||||||
expect(detail(result, "Min. received").value).toBe("0.4000 ETH");
|
expect(detail(result, "Min. received").value).toBe("0.4000 ETH");
|
||||||
});
|
});
|
||||||
@@ -734,7 +731,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(detail(result, "Token Out").value).toBe("ETH");
|
expect(detail(result, "Token Out").value).toBe("ETH");
|
||||||
expect(detail(result, "Min. received").value).toBe("0.0005 ETH");
|
expect(detail(result, "Min. received").value).toBe("0.0005 ETH");
|
||||||
});
|
});
|
||||||
@@ -759,7 +756,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(detail(result, "Token Out").value).toBe(
|
expect(detail(result, "Token Out").value).toBe(
|
||||||
"Unknown (not named in the calldata)",
|
"Unknown (not named in the calldata)",
|
||||||
);
|
);
|
||||||
@@ -782,7 +779,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
expect(result.name).toBe("Swap USDT \u2192 WETH");
|
expect(result.name).toBe("Swap USDT \u2192 WETH");
|
||||||
});
|
});
|
||||||
@@ -794,7 +791,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
|
|
||||||
const tokenIn = result.details.find((d) => d.label === "Token In");
|
const tokenIn = result.details.find((d) => d.label === "Token In");
|
||||||
@@ -817,7 +814,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
// UNWRAP_WETH means output is native ETH
|
// UNWRAP_WETH means output is native ETH
|
||||||
expect(result.name).toBe("Swap USDT \u2192 ETH");
|
expect(result.name).toBe("Swap USDT \u2192 ETH");
|
||||||
@@ -833,7 +830,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
|
|
||||||
const minOut = result.details.find((d) => d.label === "Min. received");
|
const minOut = result.details.find((d) => d.label === "Min. received");
|
||||||
@@ -853,7 +850,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
|
|
||||||
// The V4 swap action's parameters are empty, so that step is said to
|
// The V4 swap action's parameters are empty, so that step is said to
|
||||||
@@ -865,7 +862,7 @@ describe("uniswap decoder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("shows the deadline as a UTC date and time", () => {
|
test("shows the deadline as a UTC date and time", () => {
|
||||||
const result = uniswap.decode(FIRST_SWAP_CALLDATA, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(FIRST_SWAP_CALLDATA, ROUTER_ADDR);
|
||||||
expect(detail(result, "Deadline").value).toBe("2026-02-27 08:25:51");
|
expect(detail(result, "Deadline").value).toBe("2026-02-27 08:25:51");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -883,7 +880,7 @@ describe("uniswap decoder", () => {
|
|||||||
2n ** 256n - 1n,
|
2n ** 256n - 1n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
expect(result.name).toBe("Swap USDT \u2192 WETH");
|
expect(result.name).toBe("Swap USDT \u2192 WETH");
|
||||||
expect(detail(result, "Deadline").value).toBe(
|
expect(detail(result, "Deadline").value).toBe(
|
||||||
@@ -898,7 +895,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
|
|
||||||
const amount = result.details.find((d) => d.label === "Amount");
|
const amount = result.details.find((d) => d.label === "Amount");
|
||||||
@@ -973,7 +970,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
// Before decodeV4Swap fix: name would be "Swap USDT → ETH" or "Uniswap Swap"
|
// Before decodeV4Swap fix: name would be "Swap USDT → ETH" or "Uniswap Swap"
|
||||||
// After fix: correctly identifies both tokens from V4 sub-actions
|
// After fix: correctly identifies both tokens from V4 sub-actions
|
||||||
@@ -1007,7 +1004,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
|
|
||||||
// The amount and the token it is counted in come from the same hop.
|
// The amount and the token it is counted in come from the same hop.
|
||||||
@@ -1047,7 +1044,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
|
|
||||||
expect(detail(result, "Token Out").value).toBe(
|
expect(detail(result, "Token Out").value).toBe(
|
||||||
@@ -1092,7 +1089,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
|
|
||||||
expect(detail(result, "Token Out").value).toContain("USDC");
|
expect(detail(result, "Token Out").value).toContain("USDC");
|
||||||
@@ -1121,7 +1118,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
|
|
||||||
expect(detail(result, "Token Out").value).toContain("USDC");
|
expect(detail(result, "Token Out").value).toContain("USDC");
|
||||||
@@ -1158,7 +1155,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
|
|
||||||
expect(detail(result, "Token In").value).toContain("WETH");
|
expect(detail(result, "Token In").value).toContain("WETH");
|
||||||
@@ -1351,7 +1348,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result.name).toBe("Swap USDT → USDC");
|
expect(result.name).toBe("Swap USDT → USDC");
|
||||||
expect(detail(result, "Token In").address).toBe(USDT_ADDR);
|
expect(detail(result, "Token In").address).toBe(USDT_ADDR);
|
||||||
expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
|
expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
|
||||||
@@ -1403,7 +1400,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(detail(result, "Steps").value).toBe(
|
expect(detail(result, "Steps").value).toBe(
|
||||||
"V4 Swap (could not be decoded)",
|
"V4 Swap (could not be decoded)",
|
||||||
);
|
);
|
||||||
@@ -1462,7 +1459,7 @@ describe("uniswap decoder", () => {
|
|||||||
],
|
],
|
||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result.name).toBe("Uniswap Swap");
|
expect(result.name).toBe("Uniswap Swap");
|
||||||
expect(detail(result, "Token In").address).toBe(
|
expect(detail(result, "Token In").address).toBe(
|
||||||
USDT_ADDR.toLowerCase(),
|
USDT_ADDR.toLowerCase(),
|
||||||
@@ -1518,7 +1515,7 @@ describe("uniswap decoder", () => {
|
|||||||
],
|
],
|
||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result.name).toBe("Uniswap Swap");
|
expect(result.name).toBe("Uniswap Swap");
|
||||||
expect(detail(result, "Token In").value).toBe(
|
expect(detail(result, "Token In").value).toBe(
|
||||||
"Unknown (not named in the calldata)",
|
"Unknown (not named in the calldata)",
|
||||||
@@ -1541,7 +1538,7 @@ describe("uniswap decoder", () => {
|
|||||||
],
|
],
|
||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(detail(result, "Token Out").value).toBe(
|
expect(detail(result, "Token Out").value).toBe(
|
||||||
"Unknown (not named in the calldata)",
|
"Unknown (not named in the calldata)",
|
||||||
);
|
);
|
||||||
@@ -1575,7 +1572,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result.name).toBe("Swap USDT → USDC");
|
expect(result.name).toBe("Swap USDT → USDC");
|
||||||
expect(detail(result, "Token In").address).toBe(USDT_ADDR);
|
expect(detail(result, "Token In").address).toBe(USDT_ADDR);
|
||||||
expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
|
expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
|
||||||
@@ -1599,7 +1596,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
|
|
||||||
expect(detail(result, "Token In").value).toContain("USDT");
|
expect(detail(result, "Token In").value).toContain("USDT");
|
||||||
@@ -1630,7 +1627,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
|
|
||||||
expect(detail(result, "Token Out").value).toContain("WETH");
|
expect(detail(result, "Token Out").value).toContain("WETH");
|
||||||
@@ -1651,7 +1648,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
|
|
||||||
expect(detail(result, "Token Out").value).toContain("USDC");
|
expect(detail(result, "Token Out").value).toContain("USDC");
|
||||||
@@ -1670,7 +1667,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
|
|
||||||
expect(detail(result, "Token Out").value).toContain("USDT");
|
expect(detail(result, "Token Out").value).toContain("USDT");
|
||||||
@@ -1690,7 +1687,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
expect(result.name).toBe("Uniswap Swap");
|
expect(result.name).toBe("Uniswap Swap");
|
||||||
|
|
||||||
@@ -1710,7 +1707,7 @@ describe("uniswap decoder", () => {
|
|||||||
9999999999n,
|
9999999999n,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
expect(result.name).toBe("Uniswap Swap");
|
expect(result.name).toBe("Uniswap Swap");
|
||||||
|
|
||||||
|
|||||||
@@ -1,298 +0,0 @@
|
|||||||
// Which transactions are shown as a swap through the Uniswap Universal Router
|
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/520).
|
|
||||||
//
|
|
||||||
// The decoder used to recognise the router's execute() call by its selector
|
|
||||||
// alone, so the same calldata sent to an ordinary address was shown as a swap
|
|
||||||
// through the router, with that address as its Protocol. It is now decoded
|
|
||||||
// only when the transaction is sent to a Universal Router deployment on the
|
|
||||||
// network it is for; anywhere else it is left to the raw data.
|
|
||||||
//
|
|
||||||
// The transaction detail screen and the approval screen are driven against a
|
|
||||||
// minimal DOM stub in the shape tests/tokenTransferDetail.test.js uses, the
|
|
||||||
// approval screen opened the way tests/swapStatusAmount.test.js opens it.
|
|
||||||
|
|
||||||
const { AbiCoder, Interface, getAddress, solidityPacked } = require("ethers");
|
|
||||||
|
|
||||||
jest.mock("../src/shared/log", () => ({
|
|
||||||
log: {
|
|
||||||
debugf: () => {},
|
|
||||||
infof: () => {},
|
|
||||||
warnf: () => {},
|
|
||||||
errorf: () => {},
|
|
||||||
},
|
|
||||||
debugFetch: async (url) => ({
|
|
||||||
ok: true,
|
|
||||||
json: async () => mockExplorerAnswer(url),
|
|
||||||
}),
|
|
||||||
setRuntimeDebug: () => {},
|
|
||||||
isDebug: () => false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
globalThis.chrome = {
|
|
||||||
storage: { local: { get: async () => ({}), set: async () => {} } },
|
|
||||||
};
|
|
||||||
|
|
||||||
const { state } = require("../src/shared/state");
|
|
||||||
const approval = require("../src/popup/views/approval");
|
|
||||||
const { decodeCalldata } = approval;
|
|
||||||
const transactionDetail = require("../src/popup/views/transactionDetail");
|
|
||||||
|
|
||||||
const MAINNET = "0x1";
|
|
||||||
const SEPOLIA = "0xaa36a7";
|
|
||||||
|
|
||||||
// From Uniswap's deployment files: one router on Ethereum Mainnet only, one on
|
|
||||||
// Sepolia only, and one at the same address on both.
|
|
||||||
const MAINNET_ROUTER = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af";
|
|
||||||
const SEPOLIA_ROUTER = "0x3a9d48ab9751398bbfa63ad67599bb04e4bdf98b";
|
|
||||||
const ROUTER_ON_BOTH = "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD";
|
|
||||||
// The address with no code from the issue.
|
|
||||||
const ORDINARY = "0x0b0050Da68435b3EcBC236c40F16dB2070bCD79A";
|
|
||||||
|
|
||||||
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
|
|
||||||
const SEPOLIA_WETH = "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14";
|
|
||||||
const TOKEN = "0xE2E0000000000000000000000000000000000E2e";
|
|
||||||
const HUNDREDTH_ETH = 10000000000000000n;
|
|
||||||
|
|
||||||
const coder = AbiCoder.defaultAbiCoder();
|
|
||||||
const routerIface = new Interface([
|
|
||||||
"function execute(bytes commands, bytes[] inputs, uint256 deadline)",
|
|
||||||
]);
|
|
||||||
|
|
||||||
// The calldata from the issue: wrap 0.01 ETH, then a V3 swap of it into a
|
|
||||||
// token.
|
|
||||||
const SWAP_DATA = routerIface.encodeFunctionData("execute", [
|
|
||||||
solidityPacked(["uint8", "uint8"], [0x0b, 0x00]),
|
|
||||||
[
|
|
||||||
coder.encode(["address", "uint256"], [RECIPIENT, HUNDREDTH_ETH]),
|
|
||||||
coder.encode(
|
|
||||||
["address", "uint256", "uint256", "bytes", "bool"],
|
|
||||||
[
|
|
||||||
RECIPIENT,
|
|
||||||
HUNDREDTH_ETH,
|
|
||||||
1n,
|
|
||||||
solidityPacked(
|
|
||||||
["address", "uint24", "address"],
|
|
||||||
[SEPOLIA_WETH, 3000, TOKEN],
|
|
||||||
),
|
|
||||||
false,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
9999999999n,
|
|
||||||
]);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
state.trackedTokens = [];
|
|
||||||
state.wallets = [];
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("execute() calldata on the approval screen's decoder", () => {
|
|
||||||
test("sent to a router on the transaction's network, it is a swap", () => {
|
|
||||||
const onMainnet = decodeCalldata(SWAP_DATA, MAINNET_ROUTER, MAINNET);
|
|
||||||
expect(onMainnet.description).toBe("Swap via Uniswap Universal Router");
|
|
||||||
expect(onMainnet.details[0]).toMatchObject({
|
|
||||||
label: "Protocol",
|
|
||||||
address: MAINNET_ROUTER,
|
|
||||||
});
|
|
||||||
expect(
|
|
||||||
decodeCalldata(SWAP_DATA, SEPOLIA_ROUTER, SEPOLIA).description,
|
|
||||||
).toBe("Swap via Uniswap Universal Router");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("sent to an ordinary address, it is not decoded", () => {
|
|
||||||
expect(decodeCalldata(SWAP_DATA, ORDINARY, MAINNET)).toBeNull();
|
|
||||||
expect(decodeCalldata(SWAP_DATA, ORDINARY, SEPOLIA)).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("sent to a router of the other network only, it is not decoded", () => {
|
|
||||||
expect(decodeCalldata(SWAP_DATA, SEPOLIA_ROUTER, MAINNET)).toBeNull();
|
|
||||||
expect(decodeCalldata(SWAP_DATA, MAINNET_ROUTER, SEPOLIA)).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a router deployed at the same address on both networks is one on each", () => {
|
|
||||||
expect(
|
|
||||||
decodeCalldata(SWAP_DATA, ROUTER_ON_BOTH, MAINNET),
|
|
||||||
).not.toBeNull();
|
|
||||||
expect(
|
|
||||||
decodeCalldata(SWAP_DATA, ROUTER_ON_BOTH, SEPOLIA),
|
|
||||||
).not.toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("the router address matches in any letter case", () => {
|
|
||||||
const upper = "0x" + MAINNET_ROUTER.slice(2).toUpperCase();
|
|
||||||
for (const address of [getAddress(MAINNET_ROUTER), upper]) {
|
|
||||||
expect(decodeCalldata(SWAP_DATA, address, MAINNET)).not.toBeNull();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// The transaction the explorer reports: SWAP_DATA, sent to `mockSentTo`.
|
|
||||||
const TX_HASH = "0x" + "5".repeat(64);
|
|
||||||
let mockSentTo = null;
|
|
||||||
|
|
||||||
function mockExplorerAnswer(url) {
|
|
||||||
if (url.endsWith("/transactions/" + TX_HASH)) {
|
|
||||||
return {
|
|
||||||
to: { hash: mockSentTo },
|
|
||||||
raw_input: SWAP_DATA,
|
|
||||||
method: "execute",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeElement(id) {
|
|
||||||
const classes = new Set();
|
|
||||||
const el = {
|
|
||||||
id,
|
|
||||||
textContent: "",
|
|
||||||
value: "",
|
|
||||||
innerHTML: "",
|
|
||||||
style: {},
|
|
||||||
dataset: {},
|
|
||||||
classList: {
|
|
||||||
add: (...names) => names.forEach((n) => classes.add(n)),
|
|
||||||
remove: (...names) => names.forEach((n) => classes.delete(n)),
|
|
||||||
contains: (n) => classes.has(n),
|
|
||||||
toggle: () => false,
|
|
||||||
},
|
|
||||||
addEventListener: () => {},
|
|
||||||
querySelectorAll: () => [],
|
|
||||||
appendChild: () => {},
|
|
||||||
};
|
|
||||||
// Views reach for .parentElement to hide whole sections.
|
|
||||||
Object.defineProperty(el, "parentElement", {
|
|
||||||
get: () => node(id + "-parent"),
|
|
||||||
});
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeDocument() {
|
|
||||||
const els = new Map();
|
|
||||||
return {
|
|
||||||
getElementById(id) {
|
|
||||||
// The debug banner is created on demand by helpers.js; absent
|
|
||||||
// is the state a non-debug popup is in.
|
|
||||||
if (id === "debug-banner") return null;
|
|
||||||
if (!els.has(id)) els.set(id, makeElement(id));
|
|
||||||
return els.get(id);
|
|
||||||
},
|
|
||||||
createElement: () => makeElement("created"),
|
|
||||||
body: { prepend: () => {} },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function node(id) {
|
|
||||||
return globalThis.document.getElementById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open the transaction detail screen with Sepolia active, for the
|
|
||||||
// transaction the explorer reports sent to `sentTo`, and wait for its
|
|
||||||
// decoded details.
|
|
||||||
async function showDetailSentTo(sentTo) {
|
|
||||||
mockSentTo = sentTo;
|
|
||||||
globalThis.document = makeDocument();
|
|
||||||
globalThis.window = { location: { search: "" } };
|
|
||||||
state.networkId = "sepolia";
|
|
||||||
state.blockscoutUrl = "https://explorer.example/api/v2";
|
|
||||||
state.viewData = {};
|
|
||||||
state.viewStack = [];
|
|
||||||
state.currentView = null;
|
|
||||||
transactionDetail.show({
|
|
||||||
hash: TX_HASH,
|
|
||||||
from: RECIPIENT,
|
|
||||||
to: sentTo,
|
|
||||||
value: "",
|
|
||||||
symbol: "SepoliaETH",
|
|
||||||
timestamp: 1790000000,
|
|
||||||
isError: false,
|
|
||||||
direction: "contract",
|
|
||||||
directionLabel: "Swap",
|
|
||||||
method: "execute",
|
|
||||||
chainId: SEPOLIA,
|
|
||||||
});
|
|
||||||
for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("execute() calldata on the transaction detail screen", () => {
|
|
||||||
test("sent to a router on the active network, it is a swap", async () => {
|
|
||||||
await showDetailSentTo(SEPOLIA_ROUTER);
|
|
||||||
expect(node("tx-detail-calldata-details").innerHTML).toContain(
|
|
||||||
"Swap via Uniswap Universal Router",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("sent to an ordinary address, it is not decoded", async () => {
|
|
||||||
await showDetailSentTo(ORDINARY);
|
|
||||||
expect(node("tx-detail-calldata-action").textContent).toBe("execute");
|
|
||||||
expect(node("tx-detail-calldata-details").innerHTML).toBe("");
|
|
||||||
expect(node("tx-detail-rawdata").innerHTML).toContain(SWAP_DATA);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("sent to a router of the other network only, it is not decoded", async () => {
|
|
||||||
await showDetailSentTo(MAINNET_ROUTER);
|
|
||||||
expect(node("tx-detail-calldata-action").textContent).toBe("execute");
|
|
||||||
expect(node("tx-detail-calldata-details").innerHTML).toBe("");
|
|
||||||
expect(node("tx-detail-rawdata").innerHTML).toContain(SWAP_DATA);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Open the approval screen with Ethereum Mainnet active, for a site's Sepolia
|
|
||||||
// transaction that sends SWAP_DATA to `sentTo`.
|
|
||||||
async function showApprovalSentTo(sentTo) {
|
|
||||||
globalThis.document = makeDocument();
|
|
||||||
globalThis.window = { location: { search: "" }, close: () => {} };
|
|
||||||
globalThis.chrome.runtime = {
|
|
||||||
connect: () => ({ postMessage: () => {} }),
|
|
||||||
sendMessage: (_msg, reply) =>
|
|
||||||
reply({
|
|
||||||
type: "tx",
|
|
||||||
origin: "https://dapp.example",
|
|
||||||
isPhishingDomain: false,
|
|
||||||
approvedFrom: RECIPIENT,
|
|
||||||
approvedTx: {
|
|
||||||
type: 2,
|
|
||||||
from: RECIPIENT,
|
|
||||||
chainId: SEPOLIA,
|
|
||||||
nonce: "0x7",
|
|
||||||
gasLimit: "0x30d40",
|
|
||||||
maxPriorityFeePerGas: "0x3b9aca00",
|
|
||||||
maxFeePerGas: "0x77359400",
|
|
||||||
to: sentTo,
|
|
||||||
value: "0x" + HUNDREDTH_ETH.toString(16),
|
|
||||||
data: SWAP_DATA,
|
|
||||||
accessList: [],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
state.networkId = "mainnet";
|
|
||||||
state.currentView = null;
|
|
||||||
await approval.show(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The router is on Sepolia's list only, so the swap is shown only if the
|
|
||||||
// screen decodes for the transaction's network: on Ethereum Mainnet, the
|
|
||||||
// active network, that address is not a router.
|
|
||||||
describe("execute() calldata on the approval screen, for a Sepolia transaction while Ethereum Mainnet is active", () => {
|
|
||||||
test("sent to a router on Sepolia, it is a swap", async () => {
|
|
||||||
await showApprovalSentTo(SEPOLIA_ROUTER);
|
|
||||||
expect(node("approve-tx-decoded").classList.contains("hidden")).toBe(
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
expect(node("approve-tx-decoded-details").innerHTML).toContain(
|
|
||||||
"Swap via Uniswap Universal Router",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("sent to an ordinary address, it is shown as raw data", async () => {
|
|
||||||
await showApprovalSentTo(ORDINARY);
|
|
||||||
expect(node("approve-tx-decoded").classList.contains("hidden")).toBe(
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
expect(node("approve-tx-decoded-details").innerHTML).toBe("");
|
|
||||||
expect(node("approve-tx-data").textContent).toBe(SWAP_DATA);
|
|
||||||
expect(
|
|
||||||
node("approve-tx-data-section").classList.contains("hidden"),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -53,7 +53,7 @@ function unwrapData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function detail(data, label, sources) {
|
function detail(data, label, sources) {
|
||||||
const decoded = uniswap.decode(data, ROUTER, "0x1", sources || {});
|
const decoded = uniswap.decode(data, ROUTER, sources || {});
|
||||||
return decoded.details.find((d) => d.label === label);
|
return decoded.details.find((d) => d.label === label);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ function balanceCheck(token, minBalance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function detail(data, label) {
|
function detail(data, label) {
|
||||||
const decoded = uniswap.decode(data, ROUTER, "0x1", {});
|
const decoded = uniswap.decode(data, ROUTER, {});
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
return decoded.details.find((d) => d.label === label);
|
return decoded.details.find((d) => d.label === label);
|
||||||
}
|
}
|
||||||
@@ -113,9 +113,7 @@ describe("an execute() whose input currency never decoded", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("does not name ETH in the swap title either", () => {
|
test("does not name ETH in the swap title either", () => {
|
||||||
expect(uniswap.decode(data(), ROUTER, "0x1", {}).name).toBe(
|
expect(uniswap.decode(data(), ROUTER, {}).name).toBe("Uniswap Swap");
|
||||||
"Uniswap Swap",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -125,9 +123,7 @@ describe("an execute() that names only an output token", () => {
|
|||||||
|
|
||||||
test("refuses the input rather than defaulting it to ETH", () => {
|
test("refuses the input rather than defaulting it to ETH", () => {
|
||||||
expect(detail(data(), "Token In").value).toBe(REFUSAL);
|
expect(detail(data(), "Token In").value).toBe(REFUSAL);
|
||||||
expect(uniswap.decode(data(), ROUTER, "0x1", {}).name).toBe(
|
expect(uniswap.decode(data(), ROUTER, {}).name).toBe("Uniswap Swap");
|
||||||
"Uniswap Swap",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("still states the output it did establish", () => {
|
test("still states the output it did establish", () => {
|
||||||
@@ -174,9 +170,7 @@ describe("native ETH in, which V4 spells as the zero address", () => {
|
|||||||
|
|
||||||
expect(detail(data, "Token In").value).toBe("ETH (native)");
|
expect(detail(data, "Token In").value).toBe("ETH (native)");
|
||||||
expect(detail(data, "Amount").value).toBe("1.0000 ETH");
|
expect(detail(data, "Amount").value).toBe("1.0000 ETH");
|
||||||
expect(uniswap.decode(data, ROUTER, "0x1", {}).name).toBe(
|
expect(uniswap.decode(data, ROUTER, {}).name).toBe("Swap ETH → USDT");
|
||||||
"Swap ETH → USDT",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("WRAP_ETH is still ETH at 18 decimals", () => {
|
test("WRAP_ETH is still ETH at 18 decimals", () => {
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ function v3ExactIn(tokenIn, tokenOut, amountIn, amountOutMin) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function detail(data, label) {
|
function detail(data, label) {
|
||||||
const decoded = uniswap.decode(data, ROUTER, "0x1", {});
|
const decoded = uniswap.decode(data, ROUTER, {});
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
return decoded.details.find((d) => d.label === label);
|
return decoded.details.find((d) => d.label === label);
|
||||||
}
|
}
|
||||||
@@ -140,7 +140,7 @@ describe("a V4 step that states a Min. received but no output currency", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("does not name ETH in the swap title either", () => {
|
test("does not name ETH in the swap title either", () => {
|
||||||
const decoded = uniswap.decode(data(), ROUTER, "0x1", {});
|
const decoded = uniswap.decode(data(), ROUTER, {});
|
||||||
expect(decoded.name).toBe("Uniswap Swap");
|
expect(decoded.name).toBe("Uniswap Swap");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -206,8 +206,6 @@ describe("native ETH out, which V4 spells as the zero address", () => {
|
|||||||
test("is still named ETH and still scaled at 18 decimals", () => {
|
test("is still named ETH and still scaled at 18 decimals", () => {
|
||||||
expect(detail(data(), "Token Out").value).toBe("ETH");
|
expect(detail(data(), "Token Out").value).toBe("ETH");
|
||||||
expect(detail(data(), "Min. received").value).toBe("0.5000 ETH");
|
expect(detail(data(), "Min. received").value).toBe("0.5000 ETH");
|
||||||
expect(uniswap.decode(data(), ROUTER, "0x1", {}).name).toBe(
|
expect(uniswap.decode(data(), ROUTER, {}).name).toBe("Swap USDT → ETH");
|
||||||
"Swap USDT → ETH",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ function walletsHolding(token, decimals) {
|
|||||||
// decodeCalldata() rather than uniswap.decode() directly, because the scale
|
// decodeCalldata() rather than uniswap.decode() directly, because the scale
|
||||||
// sources the screen supplies are part of what is under test.
|
// sources the screen supplies are part of what is under test.
|
||||||
function swapDetail(data, label) {
|
function swapDetail(data, label) {
|
||||||
const decoded = decodeCalldata(data, ROUTER, "0x1");
|
const decoded = decodeCalldata(data, ROUTER);
|
||||||
return decoded.details.find((d) => d.label === label);
|
return decoded.details.find((d) => d.label === label);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user