Compare commits
4
Commits
5dc24d2d57
..
next
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c150b83322 | ||
|
|
ee370b5ef7 | ||
|
|
e438272125 | ||
|
|
39c58b8775 |
@@ -471,7 +471,9 @@ When "Back" returns to a screen that this page load has not drawn yet,
|
||||
Home is shown instead. "Back" onto a screen already drawn only unhides it, so
|
||||
unsaved input in Settings survives. Home is drawn again on every "Back". When a
|
||||
balance refresh returns while AddressDetail or AddressToken is showing, that
|
||||
screen draws its balances again.
|
||||
screen draws its balances again. When a refresh returns while Send is showing,
|
||||
Send draws its From address and current balance again. Its inputs and dropdown
|
||||
stay as the user left them.
|
||||
|
||||
A network switch, or a change to "Use Blockscout", cuts the stack at the first
|
||||
screen that cannot be drawn from saved state. The cut also drops AddressToken
|
||||
@@ -579,8 +581,9 @@ Transitions:
|
||||
- "+": derives the next address in place.
|
||||
- `[info]`: AddressDetail.
|
||||
- `[x]`: DeleteAddress.
|
||||
- "Send": Send. Refused with a flash message when the active address has a zero
|
||||
ETH balance or belongs to a wallet carrying the `walletDefects.js` notice.
|
||||
- "Send": Send, with its dropdown on ETH. Refused with a flash message when the
|
||||
active address has a zero ETH balance or belongs to a wallet carrying the
|
||||
`walletDefects.js` notice.
|
||||
- "Receive": Receive.
|
||||
- Tap a transaction row: TransactionDetail.
|
||||
- "Add additional wallet...": AddWallet.
|
||||
@@ -1412,17 +1415,19 @@ when both tokens are known. The lines are:
|
||||
the sender that the `Amount` line does not include. The same three lines show
|
||||
what a `PERMIT2_TRANSFER_FROM` moves and where. A `WRAP_ETH` can pay its WETH
|
||||
to a recipient other than the router. When its amount gets these lines, its
|
||||
`Moved to` line names that recipient, or reads `The sender`. A `WRAP_ETH`
|
||||
after the step that sets `Token In` takes the amount it states from the
|
||||
sender. When it pays anyone but the router, that amount gets these lines. When
|
||||
it pays the router, that amount joins the `Amount` line if `Token In` is ETH,
|
||||
and gets these lines if not. A later `WRAP_ETH` of `CONTRACT_BALANCE` to
|
||||
anyone but the router gets these lines, unless an earlier step paid ETH to the
|
||||
router. Its `Moved from the sender` line reads
|
||||
`All the ETH the transaction carries`. ETH that the sender pays a V4 step or a
|
||||
`WRAP_ETH` of a stated amount can get these three lines when the router may
|
||||
hold ETH from an earlier step. When the decoder knows the amount, its
|
||||
`Moved from the sender` line reads `Up to` and the amount.
|
||||
`Moved to` line names that recipient, or reads `The sender`. For a `WRAP_ETH`
|
||||
into the router, `Moved to` reads `The router`. A `WRAP_ETH` after the step
|
||||
that sets `Token In` takes the amount it states from the sender. When it pays
|
||||
anyone but the router, that amount gets these lines. When it pays the router,
|
||||
that amount joins the `Amount` line if `Token In` is ETH, and gets these lines
|
||||
if not. A later `WRAP_ETH` of `CONTRACT_BALANCE` to anyone but the router gets
|
||||
these lines. It gets none after an `UNWRAP_WETH` into the router, or while the
|
||||
router holds ETH from a V4 step. When it gets them, its
|
||||
`Moved from the sender` line reads `All the ETH the transaction carries`. ETH
|
||||
that the sender pays a V4 step or a `WRAP_ETH` of a stated amount can get
|
||||
these three lines when the router may hold ETH from an earlier step. When the
|
||||
decoder knows the amount, its `Moved from the sender` line reads `Up to` and
|
||||
the amount.
|
||||
- `Permit2 allowance`, `Allowance token` and `Allowance spender`: each allowance
|
||||
that a `PERMIT2_PERMIT` or `PERMIT2_PERMIT_BATCH` grants, unless the permit's
|
||||
amount already stands as the `Amount` figure.
|
||||
|
||||
+5
-2
@@ -107,8 +107,10 @@ let refreshTimer = null;
|
||||
// tracked token list stays as it is.
|
||||
//
|
||||
// Once the save is done, the wallet list is drawn again. So are the balances
|
||||
// on AddressDetail or AddressToken when that screen is up. No other screen is
|
||||
// drawn again, DeleteAddress and its balance lines included.
|
||||
// on AddressDetail or AddressToken when that screen is up. When Send is up, its
|
||||
// From address and current balance are drawn again, and its inputs and dropdown
|
||||
// stay as the user left them. No other screen is drawn again, DeleteAddress and
|
||||
// its balance lines included.
|
||||
async function doRefreshAndRender() {
|
||||
if (refreshInFlight) {
|
||||
refreshAgain = true;
|
||||
@@ -164,6 +166,7 @@ async function doRefreshAndRender() {
|
||||
renderWalletList();
|
||||
if (state.currentView === "address") addressDetail.renderBalances();
|
||||
if (state.currentView === "address-token") addressToken.renderBalance();
|
||||
if (state.currentView === "send") send.updateSendBalance();
|
||||
} catch (e) {
|
||||
// No caller waits for this function, so it must never reject. An
|
||||
// unhandled rejection reports nothing. The save inside it reports its
|
||||
|
||||
@@ -360,6 +360,11 @@ function init(_ctx) {
|
||||
});
|
||||
|
||||
$("btn-address-token-back").addEventListener("click", () => {
|
||||
// AddressDetail's show() clears the token, but Back onto a drawn
|
||||
// screen only unhides it. "Send" below may have left the dropdown on
|
||||
// the token as well.
|
||||
state.selectedToken = null;
|
||||
$("send-token").value = "ETH";
|
||||
goBack();
|
||||
});
|
||||
|
||||
|
||||
@@ -510,11 +510,15 @@ function init(ctx) {
|
||||
showFlash("Cannot send \u2014 zero balance.");
|
||||
return;
|
||||
}
|
||||
// Send uses a selected token ahead of its dropdown, and AddressToken
|
||||
// can leave one selected on the way here.
|
||||
state.selectedToken = null;
|
||||
$("send-to").value = "";
|
||||
$("send-amount").value = "";
|
||||
$("send-token-dropdown").classList.remove("hidden");
|
||||
$("send-token-static").classList.add("hidden");
|
||||
renderSendTokenSelect(addr);
|
||||
$("send-token").value = "ETH";
|
||||
updateSendBalance();
|
||||
resetSendValidation();
|
||||
pushCurrentView();
|
||||
@@ -526,6 +530,9 @@ function init(ctx) {
|
||||
showFlash("No active address selected.");
|
||||
return;
|
||||
}
|
||||
// Receive warns about a selected token, and AddressToken can leave one
|
||||
// selected on the way here.
|
||||
state.selectedToken = null;
|
||||
ctx.showReceive();
|
||||
});
|
||||
}
|
||||
|
||||
+36
-60
@@ -1273,12 +1273,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// ERC-20 token from the router takes none of that token from the
|
||||
// sender.
|
||||
let inputPaidByUser = false;
|
||||
// The recipient of a WRAP_ETH that set the input side, as written,
|
||||
// when it is not the router; otherwise null. That WRAP_ETH pays its
|
||||
// WETH there, not to a swap step (see the Moved to lines).
|
||||
// The recipient of a WRAP_ETH that set the input side, as written;
|
||||
// otherwise null. That WRAP_ETH pays its WETH there (see the Moved to
|
||||
// lines).
|
||||
let wrapEthRecipient = null;
|
||||
// The index of the WRAP_ETH that set the input side, or null.
|
||||
let wrapEthStep = null;
|
||||
// The last PERMIT2_PERMIT this decoder read before a step set the
|
||||
// input side, or null (see permitStands after the loop).
|
||||
let permit = null;
|
||||
@@ -1319,10 +1317,6 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// other than WETH, and not of ETH to a WETH contract, which pays the
|
||||
// router WETH (see keepsRouterWeth in the loop). None of those can.
|
||||
let lastWethStep = -1;
|
||||
// The index of the last swap step paid from WETH the router holds, a
|
||||
// V4 SETTLE included, or -1 (see spendFromRouter()). Only such a step
|
||||
// spends the WETH of an earlier WRAP_ETH into the router.
|
||||
let lastRouterWethSwapStep = -1;
|
||||
// Where the step that set the output side paid its output: the
|
||||
// recipient of a swap step, as written, or of each TAKE, TAKE_ALL and
|
||||
// TAKE_PORTION of it in a V4 step, a TAKE_ALL as paid to the sender,
|
||||
@@ -1383,11 +1377,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// After the loop each joins the Amount line or gets lines of its own
|
||||
// (see permitStands). A take with `atMost` set may take less than
|
||||
// `amount`: it never joins the Amount line, and its line reads
|
||||
// "Up to" and the amount. A take with a `recipient`, the ETH of a
|
||||
// later WRAP_ETH that pays anyone but the router, never joins the
|
||||
// Amount line either: its Moved to line names that recipient. The
|
||||
// take of a later WRAP_ETH of a stated amount has that step's index as
|
||||
// `wrapStep`.
|
||||
// "Up to" and the amount. A take with a `recipient` is the ETH of a
|
||||
// later WRAP_ETH. If that recipient is not the router, the take never
|
||||
// joins the Amount line either: its Moved to line names that recipient.
|
||||
const userPaid = [];
|
||||
// A swap step the user pays for (`payerIsUser` true) takes `amount`
|
||||
// of `token` from the sender, or at most that when `atMost` is set. A
|
||||
@@ -1462,20 +1454,17 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
const r = recipient.toLowerCase();
|
||||
return r === MSG_SENDER || r === sender;
|
||||
};
|
||||
// A swap step or V4 SETTLE at `step` pays `amount` of `token` from the
|
||||
// router's own balance. CONTRACT_BALANCE pays all of it. Any other
|
||||
// quantity counts against the amount a WRAP_ETH stated, and the steps
|
||||
// pay all of it once their amounts reach that one: a swap paid in ETH
|
||||
// wraps what its swap steps then spend. An amount that is not a
|
||||
// quantity, such as the null decodeV4Swap() gives a SETTLE whose
|
||||
// payment it cannot tell, or one of a token whose amount is not known,
|
||||
// pays out nothing this decoder can see.
|
||||
const spendFromRouter = (step, token, amount) => {
|
||||
// A swap step or V4 SETTLE pays `amount` of `token` from the router's
|
||||
// own balance. CONTRACT_BALANCE pays all of it. Any other quantity
|
||||
// counts against the amount a WRAP_ETH stated, and the steps pay all
|
||||
// of it once their amounts reach that one: a swap paid in ETH wraps
|
||||
// what its swap steps then spend. An amount that is not a quantity,
|
||||
// such as the null decodeV4Swap() gives a SETTLE whose payment it
|
||||
// cannot tell, or one of a token whose amount is not known, pays out
|
||||
// nothing this decoder can see.
|
||||
const spendFromRouter = (token, amount) => {
|
||||
const t = token.toLowerCase();
|
||||
const held = heldByRouter.get(t);
|
||||
if (t === WETH.get(chainId) && heldByRouter.has(t)) {
|
||||
lastRouterWethSwapStep = step;
|
||||
}
|
||||
if (amount === CONTRACT_BALANCE) {
|
||||
heldByRouter.delete(t);
|
||||
} else if (typeof held === "bigint" && typeof amount === "bigint") {
|
||||
@@ -1761,7 +1750,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
[s.recipient],
|
||||
);
|
||||
if (!s.payerIsUser) {
|
||||
spendFromRouter(i, s.tokenIn, s.amountIn);
|
||||
spendFromRouter(s.tokenIn, s.amountIn);
|
||||
}
|
||||
if (!isHop(i, s.tokenOut, s.recipient)) {
|
||||
addPayee(s.recipient, s.tokenOut);
|
||||
@@ -1790,7 +1779,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
[s.recipient],
|
||||
);
|
||||
if (!s.payerIsUser) {
|
||||
spendFromRouter(i, s.tokenIn, s.amountIn);
|
||||
spendFromRouter(s.tokenIn, s.amountIn);
|
||||
}
|
||||
if (!isHop(i, s.tokenOut, s.recipient)) {
|
||||
addPayee(s.recipient, s.tokenOut);
|
||||
@@ -1806,7 +1795,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
) {
|
||||
// Buys exactly amountOut and spends at most amountInMax.
|
||||
// Paid from the router, it spends an amount the calldata
|
||||
// does not state, so spendFromRouter() has none to count.
|
||||
// does not state, so it spends nothing spendFromRouter()
|
||||
// could count.
|
||||
hasV2OrV3ExactOut = true;
|
||||
const s =
|
||||
command === "V2_SWAP_EXACT_OUT"
|
||||
@@ -1828,9 +1818,6 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
]),
|
||||
[s.recipient],
|
||||
);
|
||||
if (!s.payerIsUser) {
|
||||
spendFromRouter(i, s.tokenIn, null);
|
||||
}
|
||||
if (!isHop(i, s.tokenOut, s.recipient)) {
|
||||
addPayee(s.recipient, s.tokenOut);
|
||||
}
|
||||
@@ -1928,10 +1915,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
w.amount,
|
||||
);
|
||||
inputPaidByUser = true;
|
||||
wrapEthStep = i;
|
||||
if (!isRouter(w.recipient)) {
|
||||
wrapEthRecipient = w.recipient;
|
||||
}
|
||||
} else if (
|
||||
w.amount > 0n &&
|
||||
w.amount !== CONTRACT_BALANCE
|
||||
@@ -1946,10 +1930,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
atMost: heldByRouter.has(
|
||||
"0x0000000000000000000000000000000000000000",
|
||||
),
|
||||
recipient: isRouter(w.recipient)
|
||||
? null
|
||||
: w.recipient,
|
||||
wrapStep: i,
|
||||
recipient: w.recipient,
|
||||
});
|
||||
} else if (
|
||||
w.amount === CONTRACT_BALANCE &&
|
||||
@@ -1960,10 +1941,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
)
|
||||
) {
|
||||
// It pays the recipient all the ETH the transaction
|
||||
// carries. After an earlier step paid ETH to the
|
||||
// router there is no line: Uniswap's routes pay back
|
||||
// what their swap did not spend this way, and carry
|
||||
// no ETH.
|
||||
// carries. It gets no lines after an UNWRAP_WETH
|
||||
// into the router, or while the router holds ETH
|
||||
// from a V4 step: Uniswap's routes pay back what
|
||||
// their swap did not spend this way, and carry no
|
||||
// ETH.
|
||||
userPaid.push({
|
||||
token: "0x0000000000000000000000000000000000000000",
|
||||
amount: TRANSACTION_ETH,
|
||||
@@ -2037,7 +2019,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// Each SETTLE paid from the router spends the amount
|
||||
// decodeV4Swap() says it pays.
|
||||
for (const s of v4.settledFromRouter) {
|
||||
spendFromRouter(i, s.currency, s.amount);
|
||||
spendFromRouter(s.currency, s.amount);
|
||||
}
|
||||
for (const t of v4.takes) {
|
||||
if (!isHop(i, t.currency, t.recipient)) {
|
||||
@@ -2308,11 +2290,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
permit.beforeEveryTake &&
|
||||
!stepsUnread.some((step) => step > permit.step);
|
||||
// What the steps take that the Amount line does not give, as
|
||||
// { token, amount, atMost, recipient, wrapStep }. A null token is a
|
||||
// step this decoder did not read (see spentUnread()), or a token it
|
||||
// does not know. `atMost` is as in `userPaid`. Only the take of a
|
||||
// WRAP_ETH that pays elsewhere has a recipient (see wrapEthRecipient
|
||||
// and userPaid). Only the take of a WRAP_ETH has a `wrapStep`.
|
||||
// { token, amount, atMost, recipient }. A null token is a step this
|
||||
// decoder did not read (see spentUnread()), or a token it does not
|
||||
// know. `atMost` is as in `userPaid`. Only the take of a WRAP_ETH has
|
||||
// a recipient (see wrapEthRecipient and userPaid).
|
||||
const spentFromSender = [];
|
||||
let total = null;
|
||||
// The takes from the sender that `total` adds up, each a quantity, in
|
||||
@@ -2337,7 +2318,6 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
token: inputToken,
|
||||
amount: inputAmount,
|
||||
recipient: wrapEthRecipient,
|
||||
wrapStep: wrapEthStep,
|
||||
});
|
||||
}
|
||||
if (hasFirstTake && !present(inputAmount)) {
|
||||
@@ -2350,7 +2330,6 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
token: inputToken,
|
||||
amount: inputAmount,
|
||||
recipient: wrapEthRecipient,
|
||||
wrapStep: wrapEthStep,
|
||||
});
|
||||
}
|
||||
if (permitStands) inputToken = permit.token;
|
||||
@@ -2362,7 +2341,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
typeof total === "bigint" &&
|
||||
typeof t.amount === "bigint" &&
|
||||
!t.atMost &&
|
||||
!present(t.recipient)
|
||||
(!present(t.recipient) || isRouter(t.recipient))
|
||||
) {
|
||||
total += t.amount;
|
||||
inTotal.push(t);
|
||||
@@ -2543,9 +2522,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// of the lines below for a PERMIT2_TRANSFER_FROM. An amount this
|
||||
// decoder does not know is said so, and a step it did not read gets
|
||||
// that one line alone. A take with no recipient goes to the swap step
|
||||
// that spends it. The take of a WRAP_ETH into the router goes to the
|
||||
// router, unless a later swap step spends its WETH. A take with a
|
||||
// recipient goes to that recipient; `MSG_SENDER` means the sender.
|
||||
// that spends it. A take with a recipient goes to that recipient:
|
||||
// the router, the sender as `MSG_SENDER`, or an address.
|
||||
for (const s of spentFromSender) {
|
||||
details.push({
|
||||
label: "Moved from the sender",
|
||||
@@ -2567,12 +2545,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
if (!present(s.recipient)) {
|
||||
details.push({
|
||||
label: "Moved to",
|
||||
value:
|
||||
present(s.wrapStep) &&
|
||||
s.wrapStep > lastRouterWethSwapStep
|
||||
? "The router"
|
||||
: "The swap step that spends it",
|
||||
value: "The swap step that spends it",
|
||||
});
|
||||
} else if (isRouter(s.recipient)) {
|
||||
details.push({ label: "Moved to", value: "The router" });
|
||||
} else if (s.recipient.toLowerCase() === MSG_SENDER) {
|
||||
details.push({ label: "Moved to", value: "The sender" });
|
||||
} else {
|
||||
|
||||
@@ -19,12 +19,17 @@
|
||||
// its ETH line reads the same, and Send states it as unknown
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/736).
|
||||
//
|
||||
// A refresh that returns while Send is up draws Send's balance line again, and
|
||||
// leaves what the user entered as it is
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/748).
|
||||
//
|
||||
// Driven through the real popup over a stored profile on Ethereum Mainnet,
|
||||
// with "Show tracked tokens with zero balance" on, as it is by default.
|
||||
|
||||
const { HDNodeWallet, Mnemonic } = require("ethers");
|
||||
|
||||
const {
|
||||
ADDRESS,
|
||||
bootPopup,
|
||||
cleanupPopup,
|
||||
unversionedValidProfile,
|
||||
@@ -431,6 +436,62 @@ test("Send for the new token reads its balance as unknown before any refresh has
|
||||
await finish([MILLION]);
|
||||
});
|
||||
|
||||
test("Send for the new token reads the fetched balance once the refresh returns, without being reopened", async () => {
|
||||
const page = await bootOnAddress();
|
||||
const finish = holdNextRefresh(page);
|
||||
await addNewToken(page);
|
||||
await openToken(page, NEW_TOKEN.toLowerCase());
|
||||
await page.click("btn-address-token-send");
|
||||
expect(page.text("send-balance")).toBe("Current balance: unknown (NEW)");
|
||||
|
||||
await finish([MILLION]);
|
||||
|
||||
expect(page.visibleViews()).toEqual(["send"]);
|
||||
expect(page.text("send-balance")).toBe("Current balance: 1000000.0000 NEW");
|
||||
});
|
||||
|
||||
test("a refresh that returns while Send is up draws an unread ETH balance again and leaves the inputs alone", async () => {
|
||||
const page = await bootPopup(profileNotRead());
|
||||
// The dropdown's first option, which a browser selects by itself.
|
||||
page.node("send-token").value = "ETH";
|
||||
await page.click("btn-main-send");
|
||||
expect(page.text("send-balance")).toBe("Current balance: unknown (ETH)");
|
||||
const recipient = "0x" + "dd".repeat(20);
|
||||
page.node("send-to").value = recipient;
|
||||
page.node("send-amount").value = "0.5";
|
||||
// Stands for the options on the screen, which a refresh leaves as they are.
|
||||
page.node("send-token").innerHTML = "options on the screen";
|
||||
// Stands for the From address from before the refresh.
|
||||
page.node("send-from").innerHTML = "From address before the refresh";
|
||||
|
||||
await refreshOnce(page, []);
|
||||
|
||||
expect(page.visibleViews()).toEqual(["send"]);
|
||||
expect(page.text("send-balance")).toBe("Current balance: 2.0000 ETH");
|
||||
expect(page.node("send-from").innerHTML).toContain(ADDRESS);
|
||||
expect(page.value("send-to")).toBe(recipient);
|
||||
expect(page.value("send-amount")).toBe("0.5");
|
||||
expect(page.value("send-token")).toBe("ETH");
|
||||
expect(page.node("send-token").innerHTML).toBe("options on the screen");
|
||||
});
|
||||
|
||||
test("a refresh that returns while Send is up keeps the token picked in the dropdown and draws its new balance", async () => {
|
||||
const page = await bootOnDerivingWallet();
|
||||
const dropdown = page.node("send-token");
|
||||
dropdown.value = "ETH";
|
||||
await page.click("btn-main-send");
|
||||
dropdown.value = NEW_TOKEN;
|
||||
for (const fn of dropdown.listeners.change) await fn();
|
||||
expect(page.text("send-balance")).toBe("Current balance: 1000000.0000 NEW");
|
||||
page.node("send-amount").value = "0.5";
|
||||
|
||||
await refreshOnce(page, [{ ...MILLION, balance: "3000000.0" }]);
|
||||
|
||||
expect(page.text("send-balance")).toBe("Current balance: 3000000.0000 NEW");
|
||||
expect(page.value("send-token")).toBe(NEW_TOKEN);
|
||||
expect(page.value("send-amount")).toBe("0.5");
|
||||
});
|
||||
|
||||
test("a token added by contract address on SettingsAddToken reads quantity unknown until the refresh returns", async () => {
|
||||
const page = await bootPopup(unversionedValidProfile());
|
||||
expect(page.visibleViews()).toEqual(["main"]);
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
// "Back" from AddressToken leaves no token selected. AddressDetail's show()
|
||||
// would clear it, but Back onto a drawn screen only unhides that screen, and
|
||||
// Send and Receive both read the field
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/751). Driven through the real
|
||||
// popup over a stored profile.
|
||||
|
||||
const {
|
||||
bootPopup,
|
||||
cleanupPopup,
|
||||
unversionedValidProfile,
|
||||
TOKEN_ADDRESS,
|
||||
} = require("./support/popupBoot");
|
||||
|
||||
const RECIPIENT = "0x711e691962E5c054b78071C789A0C8CE657377aD";
|
||||
const TOKEN = {
|
||||
address: TOKEN_ADDRESS,
|
||||
symbol: "TST",
|
||||
decimals: 18,
|
||||
name: "Test Token",
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
cleanupPopup();
|
||||
});
|
||||
|
||||
// The popup reopened onto AddressDetail, at an address holding 1.5 ETH and
|
||||
// 2.5 TST.
|
||||
function bootOnAddress() {
|
||||
const profile = unversionedValidProfile({
|
||||
currentView: "address",
|
||||
viewStack: ["main"],
|
||||
selectedWallet: 0,
|
||||
selectedAddress: 0,
|
||||
trackedTokens: [TOKEN],
|
||||
});
|
||||
profile.wallets[0].addresses[0].tokenBalances = [
|
||||
{ ...TOKEN, balance: "2.5" },
|
||||
];
|
||||
return bootPopup(profile);
|
||||
}
|
||||
|
||||
// AddressToken opened from AddressDetail, as tapping the token's row does.
|
||||
async function openToken(page) {
|
||||
const { state } = require("../src/shared/state");
|
||||
state.selectedToken = TOKEN_ADDRESS;
|
||||
require("../src/popup/views/helpers").pushCurrentView();
|
||||
require("../src/popup/views/addressToken").show();
|
||||
await page.settle();
|
||||
expect(page.visibleViews()).toEqual(["address-token"]);
|
||||
}
|
||||
|
||||
async function backFromToken(page) {
|
||||
await page.click("btn-address-token-back");
|
||||
expect(page.visibleViews()).toEqual(["address"]);
|
||||
}
|
||||
|
||||
// ETH chosen in Send's dropdown, as the user does.
|
||||
async function chooseEth(page) {
|
||||
const dropdown = page.node("send-token");
|
||||
dropdown.value = "ETH";
|
||||
for (const fn of dropdown.listeners.change) await fn();
|
||||
}
|
||||
|
||||
// Review for 1 of the token Send has, to RECIPIENT.
|
||||
async function review(page) {
|
||||
page.node("send-to").value = RECIPIENT;
|
||||
page.node("send-amount").value = "1";
|
||||
await page.click("btn-send-review");
|
||||
expect(page.visibleViews()).toEqual(["confirm-tx"]);
|
||||
}
|
||||
|
||||
describe("after Back from AddressToken", () => {
|
||||
test("no token is selected, on the page or in the stored record", async () => {
|
||||
const page = await bootOnAddress();
|
||||
await openToken(page);
|
||||
await backFromToken(page);
|
||||
|
||||
const { state } = require("../src/shared/state");
|
||||
expect(state.selectedToken).toBeNull();
|
||||
expect(page.storage.read("autistmask").selectedToken).toBeNull();
|
||||
});
|
||||
|
||||
test("Send with ETH chosen shows the ETH balance", async () => {
|
||||
const page = await bootOnAddress();
|
||||
await openToken(page);
|
||||
await backFromToken(page);
|
||||
|
||||
await page.click("btn-send");
|
||||
await chooseEth(page);
|
||||
|
||||
expect(page.text("send-balance")).toBe("Current balance: 1.5000 ETH");
|
||||
});
|
||||
|
||||
test("Send with ETH chosen reviews ETH", async () => {
|
||||
const page = await bootOnAddress();
|
||||
await openToken(page);
|
||||
await backFromToken(page);
|
||||
|
||||
await page.click("btn-send");
|
||||
await chooseEth(page);
|
||||
await review(page);
|
||||
|
||||
expect(page.text("confirm-type")).toBe("Native ETH transfer");
|
||||
});
|
||||
|
||||
test("Receive shows no token warning", async () => {
|
||||
const page = await bootOnAddress();
|
||||
await openToken(page);
|
||||
await backFromToken(page);
|
||||
|
||||
await page.click("btn-receive");
|
||||
|
||||
expect(page.visibleViews()).toEqual(["receive"]);
|
||||
expect(page.text("receive-erc20-warning")).toBe("");
|
||||
});
|
||||
|
||||
// AddressToken's Send sets the dropdown to the token, and nothing else
|
||||
// rebuilds it on the way back to AddressDetail.
|
||||
test("Send opens on ETH after AddressToken's Send has been used", async () => {
|
||||
const page = await bootOnAddress();
|
||||
await openToken(page);
|
||||
await page.click("btn-address-token-send");
|
||||
await page.click("btn-send-back");
|
||||
await backFromToken(page);
|
||||
|
||||
await page.click("btn-send");
|
||||
|
||||
expect(page.value("send-token")).toBe("ETH");
|
||||
expect(page.text("send-balance")).toBe("Current balance: 1.5000 ETH");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Send opened from AddressToken sends that token", () => {
|
||||
async function expectSendsToken(page) {
|
||||
await page.click("btn-address-token-send");
|
||||
expect(page.text("send-balance")).toBe("Current balance: 2.5000 TST");
|
||||
await review(page);
|
||||
expect(page.text("confirm-type")).toBe("ERC-20 token transfer (TST)");
|
||||
}
|
||||
|
||||
test("the first time", async () => {
|
||||
const page = await bootOnAddress();
|
||||
await openToken(page);
|
||||
|
||||
await expectSendsToken(page);
|
||||
});
|
||||
|
||||
test("after Back from Send", async () => {
|
||||
const page = await bootOnAddress();
|
||||
await openToken(page);
|
||||
await page.click("btn-address-token-send");
|
||||
await page.click("btn-send-back");
|
||||
|
||||
await expectSendsToken(page);
|
||||
});
|
||||
|
||||
test("when the token is opened again after Back from AddressToken", async () => {
|
||||
const page = await bootOnAddress();
|
||||
await openToken(page);
|
||||
await backFromToken(page);
|
||||
await openToken(page);
|
||||
|
||||
await expectSendsToken(page);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,185 @@
|
||||
// Home's "Send" and "Receive" open with no token selected. Three routes reach
|
||||
// Home with a token still selected, Send reads that token ahead of its dropdown,
|
||||
// and Receive warns about it
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/753). Driven through the real
|
||||
// popup over a stored profile.
|
||||
|
||||
const {
|
||||
bootPopup,
|
||||
cleanupPopup,
|
||||
unversionedValidProfile,
|
||||
TOKEN_ADDRESS,
|
||||
} = require("./support/popupBoot");
|
||||
|
||||
const RECIPIENT = "0x711e691962E5c054b78071C789A0C8CE657377aD";
|
||||
const OTHER_WALLET_ADDRESS = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
|
||||
const PRIVATE_KEY = "0x" + "11".repeat(32);
|
||||
const PASSWORD = "correct horse battery staple";
|
||||
const TOKEN = {
|
||||
address: TOKEN_ADDRESS,
|
||||
symbol: "TST",
|
||||
decimals: 18,
|
||||
name: "Test Token",
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
cleanupPopup();
|
||||
});
|
||||
|
||||
// AddressToken opened from AddressDetail, as tapping the token's row does.
|
||||
async function openToken(page) {
|
||||
const { state } = require("../src/shared/state");
|
||||
state.selectedToken = TOKEN_ADDRESS;
|
||||
require("../src/popup/views/helpers").pushCurrentView();
|
||||
require("../src/popup/views/addressToken").show();
|
||||
await page.settle();
|
||||
expect(page.visibleViews()).toEqual(["address-token"]);
|
||||
}
|
||||
|
||||
// The popup open on AddressToken, at an address holding 1.5 ETH and 2.5 TST.
|
||||
// A second wallet is there for the route that deletes one.
|
||||
async function bootOnToken() {
|
||||
const profile = unversionedValidProfile({
|
||||
currentView: "address",
|
||||
viewStack: ["main"],
|
||||
selectedWallet: 0,
|
||||
selectedAddress: 0,
|
||||
trackedTokens: [TOKEN],
|
||||
});
|
||||
profile.wallets[0].addresses[0].tokenBalances = [
|
||||
{ ...TOKEN, balance: "2.5" },
|
||||
];
|
||||
profile.wallets.push({
|
||||
type: "hd",
|
||||
name: "Wallet 2",
|
||||
xpub: "xpub-wallet-2",
|
||||
encryptedSecret: "encrypted-secret-2",
|
||||
nextIndex: 1,
|
||||
addresses: [
|
||||
{
|
||||
address: OTHER_WALLET_ADDRESS,
|
||||
balance: "0",
|
||||
tokenBalances: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
const page = await bootPopup(profile);
|
||||
await openToken(page);
|
||||
return page;
|
||||
}
|
||||
|
||||
async function reopenAfterTokenSend() {
|
||||
const closed = await bootOnToken();
|
||||
await closed.click("btn-address-token-send");
|
||||
cleanupPopup();
|
||||
return bootPopup(undefined, { storage: closed.storage });
|
||||
}
|
||||
|
||||
async function addWalletFromTokenSettings() {
|
||||
const page = await bootOnToken();
|
||||
await page.click("btn-settings");
|
||||
await page.click("btn-main-add-wallet");
|
||||
await page.click("tab-privkey");
|
||||
page.node("import-private-key").value = PRIVATE_KEY;
|
||||
page.node("add-wallet-password").value = PASSWORD;
|
||||
page.node("add-wallet-password-confirm").value = PASSWORD;
|
||||
await page.click("btn-add-wallet-confirm");
|
||||
return page;
|
||||
}
|
||||
|
||||
// Deletes through the lost-password screen. It asks for the wallet's name, and
|
||||
// the stored wallets here have no real password.
|
||||
async function deleteWalletFromTokenSettings() {
|
||||
const page = await bootOnToken();
|
||||
await page.click("btn-settings");
|
||||
// What the second wallet's [x] button in Settings does.
|
||||
require("../src/popup/views/helpers").pushCurrentView();
|
||||
require("../src/popup/views/deleteWallet").show(1);
|
||||
await page.click("btn-delete-wallet-lost-password");
|
||||
page.node("delete-wallet-lost-name-input").value = "Wallet 2";
|
||||
await page.click("btn-delete-wallet-lost-confirm");
|
||||
await page.click("btn-settings-back");
|
||||
return page;
|
||||
}
|
||||
|
||||
const ROUTES = [
|
||||
["reopening the popup after AddressToken's Send", reopenAfterTokenSend],
|
||||
[
|
||||
"the Settings gear on AddressToken, then adding a wallet",
|
||||
addWalletFromTokenSettings,
|
||||
],
|
||||
[
|
||||
"the Settings gear on AddressToken, then deleting another wallet, then Back",
|
||||
deleteWalletFromTokenSettings,
|
||||
],
|
||||
];
|
||||
|
||||
// A route that stopped reaching Home with the token selected would prove nothing
|
||||
// in the tests below, so it fails here.
|
||||
async function arriveOnHome(route) {
|
||||
const page = await route();
|
||||
const { state } = require("../src/shared/state");
|
||||
expect(page.visibleViews()).toEqual(["main"]);
|
||||
expect(state.selectedToken).toBe(TOKEN_ADDRESS);
|
||||
return page;
|
||||
}
|
||||
|
||||
// ETH chosen in Send's dropdown, as the user does.
|
||||
async function chooseEth(page) {
|
||||
const dropdown = page.node("send-token");
|
||||
dropdown.value = "ETH";
|
||||
for (const fn of dropdown.listeners.change) await fn();
|
||||
}
|
||||
|
||||
// Review for 1 of the token Send has, to RECIPIENT.
|
||||
async function review(page) {
|
||||
page.node("send-to").value = RECIPIENT;
|
||||
page.node("send-amount").value = "1";
|
||||
await page.click("btn-send-review");
|
||||
expect(page.visibleViews()).toEqual(["confirm-tx"]);
|
||||
}
|
||||
|
||||
describe.each(ROUTES)("after %s", (_name, route) => {
|
||||
test("choosing ETH on Home's Send reviews ETH", async () => {
|
||||
const page = await arriveOnHome(route);
|
||||
|
||||
await page.click("btn-main-send");
|
||||
await chooseEth(page);
|
||||
await review(page);
|
||||
|
||||
expect(page.text("confirm-type")).toBe("Native ETH transfer");
|
||||
expect(page.storage.read("autistmask").selectedToken).toBeNull();
|
||||
});
|
||||
|
||||
test("Home's Send starts on ETH", async () => {
|
||||
const page = await arriveOnHome(route);
|
||||
|
||||
await page.click("btn-main-send");
|
||||
|
||||
expect(page.text("send-balance")).toBe("Current balance: 1.5000 ETH");
|
||||
expect(page.value("send-token")).toBe("ETH");
|
||||
});
|
||||
|
||||
test("Home's Receive shows no token warning", async () => {
|
||||
const page = await arriveOnHome(route);
|
||||
|
||||
await page.click("btn-main-receive");
|
||||
|
||||
expect(page.visibleViews()).toEqual(["receive"]);
|
||||
expect(page.text("receive-erc20-warning")).toBe("");
|
||||
expect(page.storage.read("autistmask").selectedToken).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test("AddressToken's Send still sends the token after Home's Send has opened", async () => {
|
||||
const page = await arriveOnHome(deleteWalletFromTokenSettings);
|
||||
await page.click("btn-main-send");
|
||||
await page.click("btn-send-back");
|
||||
await openToken(page);
|
||||
|
||||
await page.click("btn-address-token-send");
|
||||
|
||||
expect(page.text("send-balance")).toBe("Current balance: 2.5000 TST");
|
||||
await review(page);
|
||||
expect(page.text("confirm-type")).toBe("ERC-20 token transfer (TST)");
|
||||
});
|
||||
+43
-21
@@ -2475,8 +2475,10 @@ describe("uniswap decoder", () => {
|
||||
// After an earlier step that may leave ETH in the router, the ETH that
|
||||
// the user pays a V4 step reads as a maximum, on lines of its own.
|
||||
// Every row reads at least the ETH that can leave the wallet. The row
|
||||
// marked "reads as on next" reads the same on next. On next every other
|
||||
// row added that ETH to the Amount line, or read it as an exact amount.
|
||||
// marked "reads as on next" reads the same on next. The row with a
|
||||
// SETTLE_ALL of at most 3 ETH read that ETH as not known on next. On
|
||||
// next every other row added that ETH to the Amount line, or read it as
|
||||
// an exact amount.
|
||||
//
|
||||
// ADDRESS_THIS is the router on every release.
|
||||
const ADDRESS_THIS = "0x0000000000000000000000000000000000000002";
|
||||
@@ -2915,10 +2917,10 @@ describe("uniswap decoder", () => {
|
||||
expect(linesBetweenTokens(result)).toEqual(lines);
|
||||
});
|
||||
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/747 — the WETH of a
|
||||
// WRAP_ETH into the router goes to a swap step only when a later swap
|
||||
// step paid from the router spends it. On next every row not marked
|
||||
// "reads as on next" read The swap step that spends it.
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/747 — a WRAP_ETH into the
|
||||
// router pays its WETH to the router, whichever step then takes it. On
|
||||
// next that Moved to line read The swap step that spends it in every
|
||||
// row.
|
||||
const SWAP_STEP = "The swap step that spends it";
|
||||
// Each pays 1 WETH from the router, for USDC, to the user.
|
||||
const V3_WETH_FROM_ROUTER = coder.encode(
|
||||
@@ -3003,24 +3005,30 @@ describe("uniswap decoder", () => {
|
||||
],
|
||||
lines: [
|
||||
["Amount", "600.0000 USDT"],
|
||||
...wrapLines(SWAP_STEP),
|
||||
...wrapLines("The router"),
|
||||
...wrapLines("The router"),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, a WRAP_ETH of 1 ETH into the router, then a V3 exact-in paid from the router's WETH, reads as on next",
|
||||
kind: "a V3 exact-in of 600 USDT, a WRAP_ETH of 1 ETH into the router, then a V3 exact-in paid from the router's WETH",
|
||||
commands: [0x00, 0x0b, 0x00],
|
||||
inputs: [V3_600, WRAP_INTO_ROUTER, V3_WETH_FROM_ROUTER],
|
||||
lines: [["Amount", "600.0000 USDT"], ...wrapLines(SWAP_STEP)],
|
||||
lines: [
|
||||
["Amount", "600.0000 USDT"],
|
||||
...wrapLines("The router"),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "the same with a V2 exact-in paid from the router's WETH, reads as on next",
|
||||
kind: "the same with a V2 exact-in paid from the router's WETH",
|
||||
commands: [0x00, 0x0b, 0x08],
|
||||
inputs: [V3_600, WRAP_INTO_ROUTER, V2_WETH_FROM_ROUTER],
|
||||
lines: [["Amount", "600.0000 USDT"], ...wrapLines(SWAP_STEP)],
|
||||
lines: [
|
||||
["Amount", "600.0000 USDT"],
|
||||
...wrapLines("The router"),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "the same with a V3 exact-out paid from the router's WETH, reads as on next",
|
||||
kind: "the same with a V3 exact-out paid from the router's WETH",
|
||||
commands: [0x00, 0x0b, 0x01],
|
||||
inputs: [
|
||||
V3_600,
|
||||
@@ -3037,14 +3045,17 @@ describe("uniswap decoder", () => {
|
||||
["Amount", "Up to 600.0000 USDT"],
|
||||
["Moved from the sender", "Up to 1.0000 ETH"],
|
||||
["Token moved", "ETH"],
|
||||
["Moved to", SWAP_STEP],
|
||||
["Moved to", "The router"],
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "the same with a V4 swap that the router's WETH settles, reads as on next",
|
||||
kind: "the same with a V4 swap that the router's WETH settles",
|
||||
commands: [0x00, 0x0b, 0x10],
|
||||
inputs: [V3_600, WRAP_INTO_ROUTER, V4_WETH_FROM_ROUTER],
|
||||
lines: [["Amount", "600.0000 USDT"], ...wrapLines(SWAP_STEP)],
|
||||
lines: [
|
||||
["Amount", "600.0000 USDT"],
|
||||
...wrapLines("The router"),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router that sets the input side, a SWEEP of that WETH to another address, then an EXECUTE_SUB_PLAN",
|
||||
@@ -3056,11 +3067,11 @@ describe("uniswap decoder", () => {
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router that sets the input side, a V3 exact-in paid from the router's WETH, then an EXECUTE_SUB_PLAN, reads as on next",
|
||||
kind: "a WRAP_ETH of 1 ETH into the router that sets the input side, a V3 exact-in paid from the router's WETH, then an EXECUTE_SUB_PLAN",
|
||||
commands: [0x0b, 0x00, 0x21],
|
||||
inputs: [WRAP_INTO_ROUTER, V3_WETH_FROM_ROUTER, SUB_PLAN],
|
||||
lines: [
|
||||
...wrapLines(SWAP_STEP),
|
||||
...wrapLines("The router"),
|
||||
["Moved from the sender", UNDECODED],
|
||||
],
|
||||
},
|
||||
@@ -3078,7 +3089,7 @@ describe("uniswap decoder", () => {
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "the same with a V3 exact-in paid from the router's WETH after the WRAP_ETH, reads as on next",
|
||||
kind: "the same with a V3 exact-in paid from the router's WETH after the WRAP_ETH",
|
||||
commands: [0x0a, 0x0b, 0x00, 0x00],
|
||||
inputs: [
|
||||
encodePermit2(USDT_ADDR, 5000000000n, ROUTER_ADDR),
|
||||
@@ -3086,7 +3097,10 @@ describe("uniswap decoder", () => {
|
||||
V3_WETH_FROM_ROUTER,
|
||||
V3_600,
|
||||
],
|
||||
lines: [["Amount", "5000.0000 USDT"], ...wrapLines(SWAP_STEP)],
|
||||
lines: [
|
||||
["Amount", "5000.0000 USDT"],
|
||||
...wrapLines("The router"),
|
||||
],
|
||||
},
|
||||
])("$kind", ({ commands, inputs, lines }) => {
|
||||
const result = decodeSteps(commands, inputs);
|
||||
@@ -3096,8 +3110,10 @@ describe("uniswap decoder", () => {
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/747 — a later WRAP_ETH
|
||||
// of CONTRACT_BALANCE wraps all the ETH the router holds. Unless an
|
||||
// earlier step paid ETH to the router, that is the ETH the transaction
|
||||
// carries. On next no line gave it. Uniswap's own routes pay back what
|
||||
// their swap did not spend this way, and carry no ETH, so in the rows
|
||||
// carries. On next no line gave it. Into the router it gets no lines.
|
||||
// It gets none after an UNWRAP_WETH into the router, or while the
|
||||
// router holds ETH from a V4 step: Uniswap's own routes pay back what
|
||||
// their swap did not spend this way, and carry no ETH. In the rows
|
||||
// marked "reads as on next" that WRAP_ETH gets no lines.
|
||||
const WRAP_ALL_TO_OTHER = encodeWrapEth(OTHER_ADDR, CONTRACT_BALANCE);
|
||||
const WRAP_ALL_TO_SENDER = encodeWrapEth(MSG_SENDER, CONTRACT_BALANCE);
|
||||
@@ -3181,6 +3197,12 @@ describe("uniswap decoder", () => {
|
||||
inputs: [WRAP_INTO_ROUTER, WRAP_ALL_TO_OTHER],
|
||||
lines: [["Amount", "1.0000 ETH"], ...allEthLines(OTHER_ADDR)],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH of CONTRACT_BALANCE into the router, reads as on next",
|
||||
commands: [0x00, 0x0b],
|
||||
inputs: [V3_600, encodeWrapEth(ADDRESS_THIS, CONTRACT_BALANCE)],
|
||||
lines: [["Amount", "600.0000 USDT"]],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, an UNWRAP_WETH to the sender, then a WRAP_ETH of CONTRACT_BALANCE to another address",
|
||||
commands: [0x00, 0x0c, 0x0b],
|
||||
|
||||
Reference in New Issue
Block a user