Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56014a264b | ||
|
|
2c882c75d6 |
@@ -433,7 +433,11 @@ previously correct by reading only.
|
||||
It also covers a send that Sign & Send refuses for its network fee: the stub
|
||||
node quotes an ordinary fee while the confirmation screen is drawn and one over
|
||||
the bound when Sign & Send asks again, and the refusal must broadcast nothing
|
||||
and leave the password field and Sign & Send where they were.
|
||||
and leave the password field and Sign & Send where they were. It checks the same
|
||||
for a send refused because the selected wallet does not hold its From address:
|
||||
with the confirmation screen showing a send from the HD wallet, the key wallet
|
||||
is selected by writing it straight into extension storage, as another popup
|
||||
selecting it would save it, and the popup is reopened onto that screen.
|
||||
|
||||
It also covers both ways the wait for a sent transaction's receipt ends on the
|
||||
error screen: lookups that still find no receipt 60 seconds after the broadcast,
|
||||
@@ -1814,9 +1818,10 @@ view would leave a wallet one click from deletion.
|
||||
estimated, and it says the wallet does not know how many decimal places
|
||||
the token uses and that the transaction cannot be sent; for any other
|
||||
failure it asks the user to go back and try again
|
||||
- Two more reserved error boxes, made visible when "Sign & Send" refuses a
|
||||
send because the active network changed or because of its gas limit or fee
|
||||
(see Transitions), so the refusal moves nothing
|
||||
- Three more reserved error boxes, made visible when "Sign & Send" refuses a
|
||||
send because the active network changed, because the selected wallet does
|
||||
not hold the From address, or because of its gas limit or fee (see
|
||||
Transitions), so the refusal moves nothing
|
||||
- Password: an inline field on this screen, not a modal, with its own error
|
||||
line
|
||||
- "Sign & Send" button (disabled if errors, while the balance is being read,
|
||||
@@ -1860,8 +1865,9 @@ view would leave a wallet one click from deletion.
|
||||
- "Sign & Send" signs as the From address above, not as whichever address is
|
||||
selected: the two are saved separately, so the popup can reopen onto this
|
||||
screen with another address selected. When the selected wallet does not
|
||||
hold the From address → full-sentence error on the password error line,
|
||||
nothing sent (no screen change)
|
||||
hold the From address → nothing is signed; "The selected wallet does not
|
||||
hold the From address above, so nothing was sent. Please go back and send
|
||||
again." in its own box, no screen change
|
||||
- "Sign & Send" (correct password, but the From address's key is refused,
|
||||
see Data Model) → nothing is signed → **ErrorTx** saying why
|
||||
- "Back" → **Send**
|
||||
@@ -3060,32 +3066,35 @@ Currently supported:
|
||||
or V3 exact-in swap step or V4 `SETTLE` that pays `CONTRACT_BALANCE` of that
|
||||
token from the router (`payerIsUser` false). Such steps also spend all the
|
||||
WETH a `WRAP_ETH` paid the router once together they spend the amount the
|
||||
`WRAP_ETH` states, as a swap paid in ETH does; a `SETTLE` of `OPEN_DELTA`
|
||||
(zero) from the router counts as spending what its step's swap spends only
|
||||
when it is the step's only `SETTLE` or `SETTLE_ALL` of that token, comes after
|
||||
every swap action in the step, and no swap action in the step buys that token,
|
||||
because the router pays only what the step still owes of that token when the
|
||||
`SETTLE` runs; otherwise it counts as spending nothing. So the rest of a V4
|
||||
`SETTLE` of more than its swap spends, taken to the router, the WETH of a
|
||||
`WRAP_ETH`, and a token a swap pays the router on the way to the output, each
|
||||
stays in the router when later steps do not spend all of it, where anyone can
|
||||
take it in a later transaction. The router sends ETH as written, so a
|
||||
`TRANSFER` of `CONTRACT_BALANCE` of ETH always reverts and pays out nothing. A
|
||||
step paying a token to the V2 pair that the very next step spends, as a V2
|
||||
exact-in of that token whose `amountIn` is `ALREADY_PAID` (zero) or
|
||||
`CONTRACT_BALANCE`, is a hop, not a payout: Uniswap's mixed routes pay the
|
||||
next V2 pair directly, and the router enforces that swap's own minimum. The
|
||||
decoder checks that the address paid is that pair, deriving it from the V2
|
||||
factory and pair init code hash the network's routers were deployed with and
|
||||
the first two tokens of that step's path; a payment to any other address is
|
||||
named. (Sepolia's `UniversalRouterV1_2_V2Support` was deployed with
|
||||
placeholders for both values, so no V2 swap step can succeed on that router.)
|
||||
Every other payment counts, in whichever step and of whatever token, a share
|
||||
of the output included: an interface fee paid to another address by
|
||||
`PAY_PORTION`, `TRANSFER` or `TAKE_PORTION` is named, and the minimum reads
|
||||
`Min. paid out`, because the swap's minimum includes the fee. The minimum
|
||||
reads `Min. received` only when some step pays the transaction's sender (the
|
||||
From address on TxApproval, the sender the explorer reports on
|
||||
`WRAP_ETH` states, as a swap paid in ETH does. The router pays a `SETTLE` of
|
||||
`OPEN_DELTA` (zero) only what its step still owes of that token when the
|
||||
`SETTLE` runs, so one from the router counts as spending the amounts the
|
||||
step's swap actions state they spend of that token only when every action in
|
||||
the step could be read, every swap action names the token it spends and the
|
||||
token it buys (a swap action with an empty path swaps nothing), the `SETTLE`
|
||||
is the step's only `SETTLE` or `SETTLE_ALL` of that token and comes after
|
||||
every swap action, no swap action buys that token, and the swap actions that
|
||||
spend it state a quantity together; otherwise it counts as spending nothing.
|
||||
So the rest of a V4 `SETTLE` of more than its swap spends, taken to the
|
||||
router, the WETH of a `WRAP_ETH`, and a token a swap pays the router on the
|
||||
way to the output, each stays in the router when later steps do not spend all
|
||||
of it, where anyone can take it in a later transaction. The router sends ETH
|
||||
as written, so a `TRANSFER` of `CONTRACT_BALANCE` of ETH always reverts and
|
||||
pays out nothing. A step paying a token to the V2 pair that the very next step
|
||||
spends, as a V2 exact-in of that token whose `amountIn` is `ALREADY_PAID`
|
||||
(zero) or `CONTRACT_BALANCE`, is a hop, not a payout: Uniswap's mixed routes
|
||||
pay the next V2 pair directly, and the router enforces that swap's own
|
||||
minimum. The decoder checks that the address paid is that pair, deriving it
|
||||
from the V2 factory and pair init code hash the network's routers were
|
||||
deployed with and the first two tokens of that step's path; a payment to any
|
||||
other address is named. (Sepolia's `UniversalRouterV1_2_V2Support` was
|
||||
deployed with placeholders for both values, so no V2 swap step can succeed on
|
||||
that router.) Every other payment counts, in whichever step and of whatever
|
||||
token, a share of the output included: an interface fee paid to another
|
||||
address by `PAY_PORTION`, `TRANSFER` or `TAKE_PORTION` is named, and the
|
||||
minimum reads `Min. paid out`, because the swap's minimum includes the fee.
|
||||
The minimum reads `Min. received` only when some step pays the transaction's
|
||||
sender (the From address on TxApproval, the sender the explorer reports on
|
||||
TransactionDetail) and no `Paid to` line is shown. `Paid to` lines after
|
||||
`Token Out` name each other address paid; one reads
|
||||
`The bridge, which pays the deposit's recipient on another network` for a
|
||||
|
||||
+11
-3
@@ -697,9 +697,10 @@
|
||||
id="confirm-fee-unknown-error"
|
||||
class="mb-2 border border-border border-dashed p-2 text-xs invisible"
|
||||
></div>
|
||||
<!-- Shown when Sign & Send refuses a send for its network, or
|
||||
for its gas limit or fee. Each holds its space from the first
|
||||
paint, so the refusal moves nothing. -->
|
||||
<!-- Shown when Sign & Send refuses a send for its network, for
|
||||
a From address the selected wallet does not hold, or for its
|
||||
gas limit or fee. Each holds its space from the first paint, so
|
||||
the refusal moves nothing. -->
|
||||
<div
|
||||
id="confirm-network-changed-error"
|
||||
class="mb-2 border border-border border-dashed p-2 text-xs invisible"
|
||||
@@ -708,6 +709,13 @@
|
||||
reviewed, so it was not sent. Please go back and review it
|
||||
again.
|
||||
</div>
|
||||
<div
|
||||
id="confirm-from-address-error"
|
||||
class="mb-2 border border-border border-dashed p-2 text-xs invisible"
|
||||
>
|
||||
The selected wallet does not hold the From address above, so
|
||||
nothing was sent. Please go back and send again.
|
||||
</div>
|
||||
<div
|
||||
id="confirm-fee-bound-error"
|
||||
class="mb-2 border border-border border-dashed p-2 text-xs invisible"
|
||||
|
||||
@@ -320,6 +320,7 @@ function show(txInfo) {
|
||||
$("confirm-tx-password").value = "";
|
||||
hideError("confirm-tx-password-error");
|
||||
setVisible("confirm-network-changed-error", false);
|
||||
setVisible("confirm-from-address-error", false);
|
||||
setVisible("confirm-fee-bound-error", false);
|
||||
|
||||
// Gas estimate — show placeholder then fetch async
|
||||
@@ -661,6 +662,7 @@ function init(_ctx) {
|
||||
$("btn-confirm-send").addEventListener("click", async () => {
|
||||
// A press shows only its own refusal, if any.
|
||||
setVisible("confirm-network-changed-error", false);
|
||||
setVisible("confirm-from-address-error", false);
|
||||
setVisible("confirm-fee-bound-error", false);
|
||||
|
||||
const wallet = state.wallets[state.selectedWallet];
|
||||
@@ -686,11 +688,7 @@ function init(_ctx) {
|
||||
// address selected.
|
||||
const addr = wallet.addresses.find((a) => a.address === pendingTx.from);
|
||||
if (!addr) {
|
||||
showError(
|
||||
"confirm-tx-password-error",
|
||||
"The selected wallet does not hold the From address above, " +
|
||||
"so nothing was sent. Please go back and send again.",
|
||||
);
|
||||
setVisible("confirm-from-address-error", true);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+56
-39
@@ -887,9 +887,9 @@ function totalAmount(amounts) {
|
||||
// a share of the currency, so it does not name the output side as TAKE does.
|
||||
// `recipientUnread` is set when the step has a payout this decoder did not
|
||||
// read. `settledFromRouter` is every SETTLE that the router pays from its own
|
||||
// balance (`payerIsUser` false), as the currency, the amount as written and
|
||||
// its index among the step's actions, except that a SETTLE of OPEN_DELTA
|
||||
// whose payment this decoder cannot follow has no amount (null; see below).
|
||||
// balance (`payerIsUser` false), as its currency and the amount it pays: a
|
||||
// quantity, CONTRACT_BALANCE (all the router holds), or null when this
|
||||
// decoder cannot tell (see below).
|
||||
function decodeV4Swap(input) {
|
||||
try {
|
||||
const d = coder.decode(["bytes", "bytes[]"], input);
|
||||
@@ -899,11 +899,15 @@ function decodeV4Swap(input) {
|
||||
let settleToken = null;
|
||||
let takeToken = null;
|
||||
const takes = [];
|
||||
const settledFromRouter = [];
|
||||
// The currency of every SETTLE and SETTLE_ALL, in order.
|
||||
// Every SETTLE paid from the router, as its currency, its amount as
|
||||
// written and its index among the step's actions.
|
||||
const routerSettles = [];
|
||||
// The currency of every SETTLE and SETTLE_ALL.
|
||||
const settledCurrencies = [];
|
||||
// The index of the step's last swap action, read or not, or -1.
|
||||
let lastSwapAction = -1;
|
||||
// Set when an action of the step could not be read.
|
||||
let actionUnread = false;
|
||||
let recipientUnread = false;
|
||||
// The step's swap actions that could be read, as decodeV4SwapAction()
|
||||
// reads them.
|
||||
@@ -925,7 +929,7 @@ function decodeV4Swap(input) {
|
||||
settleToken = s[0];
|
||||
settledCurrencies.push(s[0]);
|
||||
if (!s[2]) {
|
||||
settledFromRouter.push({
|
||||
routerSettles.push({
|
||||
currency: s[0],
|
||||
amount: s[1],
|
||||
index: i,
|
||||
@@ -967,14 +971,18 @@ function decodeV4Swap(input) {
|
||||
swaps.push(decodeV4SwapAction(actionId, params[i]));
|
||||
} catch {
|
||||
swapUndecoded = true;
|
||||
actionUnread = true;
|
||||
}
|
||||
} else {
|
||||
// V4Router reverts on any action not named above, so
|
||||
// none of them pays anyone. This decoder reads none.
|
||||
actionUnread = true;
|
||||
}
|
||||
// V4Router reverts on any action not named above, so none
|
||||
// of them pays anyone.
|
||||
} catch {
|
||||
// Skip sub-actions we can't decode. A TAKE, TAKE_ALL or
|
||||
// TAKE_PORTION among them is a payout this decoder did not
|
||||
// read.
|
||||
actionUnread = true;
|
||||
if (
|
||||
actionId === V4_TAKE ||
|
||||
actionId === V4_TAKE_ALL ||
|
||||
@@ -985,23 +993,40 @@ function decodeV4Swap(input) {
|
||||
}
|
||||
}
|
||||
|
||||
// A SETTLE of OPEN_DELTA (zero) paid from the router pays what the
|
||||
// step still owes of its currency when it runs. That is what the
|
||||
// step's swap actions spend of it only when the SETTLE is the step's
|
||||
// one SETTLE or SETTLE_ALL of that currency, comes after every swap
|
||||
// action, and no swap action buys that currency (an unread one may).
|
||||
// Any other such SETTLE pays an amount this decoder cannot tell.
|
||||
for (const s of settledFromRouter) {
|
||||
// What each SETTLE paid from the router pays. A quantity or
|
||||
// CONTRACT_BALANCE pays as written. A SETTLE of OPEN_DELTA (zero)
|
||||
// pays whatever the step still owes of its currency when it runs.
|
||||
// That is what the step's swap actions state they spend of it only
|
||||
// in the plain shape of Uniswap's own routes, so only then is it
|
||||
// given that amount: every action in the step was read; every swap
|
||||
// action names the currency it spends and the one it buys (an
|
||||
// exact-in action with an empty path swaps nothing); the SETTLE is
|
||||
// the step's only SETTLE or SETTLE_ALL of its currency and comes
|
||||
// after every swap action; no swap action buys that currency; and
|
||||
// the swap actions that spend it state a quantity together (see
|
||||
// totalAmount()). Otherwise it is given null.
|
||||
const settledFromRouter = routerSettles.map((s) => {
|
||||
if (s.amount !== 0n) {
|
||||
return { currency: s.currency, amount: s.amount };
|
||||
}
|
||||
const stated = totalAmount(
|
||||
swaps
|
||||
.filter((w) => w.tokenIn === s.currency)
|
||||
.map((w) => w.amountIn),
|
||||
);
|
||||
const paysWhatSwapsSpend =
|
||||
!actionUnread &&
|
||||
swaps.every((w) => present(w.tokenIn) && present(w.tokenOut)) &&
|
||||
settledCurrencies.filter((c) => c === s.currency).length ===
|
||||
1 &&
|
||||
s.index > lastSwapAction &&
|
||||
!swapUndecoded &&
|
||||
!swaps.some((w) => w.tokenOut === s.currency);
|
||||
if (s.amount === 0n && !paysWhatSwapsSpend) {
|
||||
s.amount = null;
|
||||
}
|
||||
}
|
||||
!swaps.some((w) => w.tokenOut === s.currency) &&
|
||||
typeof stated === "bigint";
|
||||
return {
|
||||
currency: s.currency,
|
||||
amount: paysWhatSwapsSpend ? stated : null,
|
||||
};
|
||||
});
|
||||
|
||||
// Rule 3.
|
||||
if (swapUndecoded) {
|
||||
@@ -1164,12 +1189,13 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
return r === ADDRESS_THIS || r === toAddress.toLowerCase();
|
||||
};
|
||||
// A swap step or V4 SETTLE pays `amount` of `token` from the router's
|
||||
// own balance. CONTRACT_BALANCE pays all of it. Another amount the
|
||||
// step states 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 the step
|
||||
// does not state, or one of a token whose amount is not known, pays
|
||||
// out nothing this decoder can see.
|
||||
// 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);
|
||||
@@ -1457,19 +1483,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
} else if (command === "V4_SWAP") {
|
||||
const v4 = decodeV4Swap(inputs[i]);
|
||||
if (v4) {
|
||||
// A SETTLE of OPEN_DELTA (zero) that decodeV4Swap()
|
||||
// left at zero pays what the step's swap actions
|
||||
// spend of its currency, which the step states as its
|
||||
// amount when that currency is its Token In.
|
||||
// Each SETTLE paid from the router spends the amount
|
||||
// decodeV4Swap() says it pays.
|
||||
for (const s of v4.settledFromRouter) {
|
||||
let amount = s.amount;
|
||||
if (amount === 0n) {
|
||||
amount =
|
||||
s.currency === v4.tokenIn
|
||||
? v4.amountIn
|
||||
: null;
|
||||
}
|
||||
spendFromRouter(s.currency, amount);
|
||||
spendFromRouter(s.currency, s.amount);
|
||||
}
|
||||
for (const t of v4.takes) {
|
||||
if (!isHop(i, t.currency, t.recipient)) {
|
||||
|
||||
@@ -442,11 +442,25 @@ describe("a confirmation screen whose From is not the selected address", () => {
|
||||
expect(Transaction.from(signed).from).toBe(derived(1).address);
|
||||
});
|
||||
|
||||
// The refusal's sentence is fixed in index.html, in a box of its own that
|
||||
// holds its space from the first paint, so showing it moves nothing.
|
||||
test("nothing is sent when the selected wallet does not hold it", async () => {
|
||||
expect(await confirmSend(derived(5).address, 0)).toBeUndefined();
|
||||
expect(node("confirm-tx-password-error").textContent).toBe(
|
||||
"The selected wallet does not hold the From address above, " +
|
||||
"so nothing was sent. Please go back and send again.",
|
||||
expect(node("confirm-from-address-error").style.visibility).toBe(
|
||||
"visible",
|
||||
);
|
||||
expect(node("confirm-tx-password-error").textContent).toBe("");
|
||||
|
||||
// Reviewing a send again draws the screen again, which hides it.
|
||||
confirmTx.show({
|
||||
from: derived(0).address,
|
||||
to: RECIPIENT,
|
||||
amount: "0.5",
|
||||
token: "ETH",
|
||||
});
|
||||
await settle();
|
||||
expect(node("confirm-from-address-error").style.visibility).toBe(
|
||||
"hidden",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3153,6 +3153,125 @@ test("a send refused for its fee leaves the password field and Sign & Send where
|
||||
}
|
||||
});
|
||||
|
||||
// --------- a send from an address the selected wallet does not hold (#609)
|
||||
//
|
||||
// The selection is saved apart from the confirmation screen, so the popup can
|
||||
// reopen onto the screen with a wallet selected that does not hold its From
|
||||
// address, and Sign & Send then refuses the send. Another popup selecting the
|
||||
// key wallet would save that selection; here it is written into storage, and
|
||||
// the popup reopened.
|
||||
|
||||
test("a send from an address the selected wallet does not hold leaves the password field and Sign & Send where they were (#609)", async (env) => {
|
||||
try {
|
||||
// From the HD wallet's address.
|
||||
await goToConfirm(env.page, {
|
||||
token: "ETH",
|
||||
balance: FUNDED_ETH_TEXT + " ETH",
|
||||
amount: COMFORTABLE_AMOUNT,
|
||||
});
|
||||
// The screen's own save lands first, so it cannot write over the
|
||||
// selection below.
|
||||
await waitForPersisted(
|
||||
env.page,
|
||||
"currentView",
|
||||
"confirm-tx",
|
||||
"before selecting the key wallet",
|
||||
);
|
||||
await env.page.evaluate(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
chrome.storage.local.get("autistmask", (r) => {
|
||||
const record = {
|
||||
...r.autistmask,
|
||||
selectedWallet: 1,
|
||||
selectedAddress: 0,
|
||||
};
|
||||
chrome.storage.local.set(
|
||||
{ autistmask: record },
|
||||
resolve,
|
||||
);
|
||||
});
|
||||
}),
|
||||
);
|
||||
await reopenPopup(env, "confirm-tx");
|
||||
await waitForEstimate(env.page);
|
||||
// The refusal shown anywhere on the screen, not only in its own box:
|
||||
// shown on the password error line instead, the test fails on the
|
||||
// moves checked below rather than on the wait for it.
|
||||
const refusal = env.page
|
||||
.locator("#view-confirm-tx")
|
||||
.getByText("does not hold the From address above")
|
||||
.filter({ visible: true });
|
||||
const before = await confirmState(env.page);
|
||||
assert(
|
||||
!before.sendDisabled && (await refusal.count()) === 0,
|
||||
"the screen is not ready to send: " + JSON.stringify(before),
|
||||
);
|
||||
|
||||
const sent = env.routeOpts.broadcastTransactions.length;
|
||||
await fillPasswordAndSend(env.page);
|
||||
await refusal.waitFor({ timeout: 60000 });
|
||||
const after = await confirmState(env.page);
|
||||
|
||||
// Printed pass or fail, as the dust threshold test does.
|
||||
console.log(
|
||||
"# refused From address: password field top " +
|
||||
before.passwordTop +
|
||||
" -> " +
|
||||
after.passwordTop +
|
||||
"px, Sign & Send top " +
|
||||
before.sendTop +
|
||||
" -> " +
|
||||
after.sendTop +
|
||||
"px",
|
||||
);
|
||||
assert(
|
||||
env.routeOpts.broadcastTransactions.length === sent,
|
||||
"a send from an address the selected wallet does not hold" +
|
||||
" reached the RPC",
|
||||
);
|
||||
assert(
|
||||
after.passwordTop === before.passwordTop,
|
||||
"the password field moved " +
|
||||
(after.passwordTop - before.passwordTop) +
|
||||
"px when the send was refused",
|
||||
);
|
||||
assert(
|
||||
after.sendTop === before.sendTop,
|
||||
"Sign & Send moved " +
|
||||
(after.sendTop - before.sendTop) +
|
||||
"px when the send was refused",
|
||||
);
|
||||
// Only now, where the refusal is shown. Its sentence is wrapped across
|
||||
// lines in index.html.
|
||||
const shownIn = await refusal.evaluate((el) => ({
|
||||
id: el.id,
|
||||
text: el.textContent.replace(/\s+/g, " ").trim(),
|
||||
}));
|
||||
assert(
|
||||
shownIn.id === "confirm-from-address-error" &&
|
||||
shownIn.text ===
|
||||
"The selected wallet does not hold the From address above," +
|
||||
" so nothing was sent. Please go back and send again.",
|
||||
"wrong From address refusal: " + JSON.stringify(shownIn),
|
||||
);
|
||||
} finally {
|
||||
// A reopened popup restores no Send screen beneath this one, so Back
|
||||
// lands on the selected address. Then Home, and the HD wallet's
|
||||
// address, which the later tests send from.
|
||||
if (await env.page.isVisible("#view-confirm-tx")) {
|
||||
await env.page.click("#btn-confirm-back");
|
||||
}
|
||||
if (await env.page.isVisible("#view-send")) {
|
||||
await env.page.click("#btn-send-back");
|
||||
}
|
||||
await visible(env.page, "#view-address");
|
||||
await env.page.click("#btn-address-back");
|
||||
await visible(env.page, "#view-main");
|
||||
await openAddressDetail(env.page);
|
||||
}
|
||||
});
|
||||
|
||||
// ------------------------------ the wait for a receipt ending in error (#315)
|
||||
//
|
||||
// README.md (WaitTx) documents two ways the wait ends on the error screen: a
|
||||
|
||||
@@ -594,6 +594,34 @@ describe("any token left in the router is not received", () => {
|
||||
),
|
||||
];
|
||||
}
|
||||
// V4 SWAP_EXACT_IN of `amountIn` of `currencyIn` through one pool for
|
||||
// each currency of `path` in turn, for at least `amountOutMin` of the
|
||||
// last. An empty path swaps nothing.
|
||||
function swapExactIn(currencyIn, path, amountIn, amountOutMin) {
|
||||
return [
|
||||
0x07,
|
||||
coder.encode(
|
||||
[
|
||||
"tuple(address,tuple(address,uint24,int24,address,bytes)[],uint128,uint128)",
|
||||
],
|
||||
[
|
||||
[
|
||||
currencyIn,
|
||||
path.map((c) => [c, 500, 10, ZERO, "0x"]),
|
||||
amountIn,
|
||||
amountOutMin,
|
||||
],
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
// V4 SETTLE_ALL of `currency`, paid by the user.
|
||||
function settleAll(currency, maxAmount) {
|
||||
return [
|
||||
0x0c,
|
||||
coder.encode(["address", "uint256"], [currency, maxAmount]),
|
||||
];
|
||||
}
|
||||
// The line giving a token left in the router.
|
||||
const tokenLine = (token, symbol) => ({
|
||||
label: "Token moved into the router",
|
||||
@@ -709,6 +737,60 @@ describe("any token left in the router is not received", () => {
|
||||
),
|
||||
left: tokenLine(WETH, "WETH"),
|
||||
},
|
||||
// The user pays for the swap by a SETTLE_ALL or SETTLE whose currency
|
||||
// the decoder cannot read, so the SETTLE from the router pays nothing.
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 swap of 1 WETH with a SETTLE_ALL that cannot be read and a SETTLE of OPEN_DELTA from the router",
|
||||
data: execute(
|
||||
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||
v4Swap(
|
||||
swapExactInSingle(WETH, USDC, ONE_WETH, USDC_1990),
|
||||
withDirtyWord(settleAll(WETH, ONE_WETH), 0),
|
||||
settle(WETH, 0n, false),
|
||||
takeAll(USDC),
|
||||
),
|
||||
),
|
||||
left: tokenLine(WETH, "WETH"),
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 swap of 1 WETH with a SETTLE by the user that cannot be read and a SETTLE of OPEN_DELTA from the router",
|
||||
data: execute(
|
||||
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||
v4Swap(
|
||||
swapExactInSingle(WETH, USDC, ONE_WETH, USDC_1990),
|
||||
withDirtyWord(settle(WETH, ONE_WETH, true), 0),
|
||||
settle(WETH, 0n, false),
|
||||
takeAll(USDC),
|
||||
),
|
||||
),
|
||||
left: tokenLine(WETH, "WETH"),
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 step with an exact-in of 1 WETH less 1,000 wei and an empty path, a swap of 1,000 wei, and a SETTLE of OPEN_DELTA from the router",
|
||||
data: execute(
|
||||
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||
v4Swap(
|
||||
swapExactIn(WETH, [], ONE_WETH - 1000n, 0n),
|
||||
swapExactInSingle(WETH, USDC, 1000n, USDC_1990),
|
||||
settle(WETH, 0n, false),
|
||||
takeAll(USDC),
|
||||
),
|
||||
),
|
||||
left: tokenLine(WETH, "WETH"),
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 swap of all of it settled from the router, with an action the decoder does not read",
|
||||
data: execute(
|
||||
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||
v4Swap(
|
||||
swapExactInSingle(WETH, USDC, ONE_WETH, USDC_1990),
|
||||
settle(WETH, 0n, false),
|
||||
takeAll(USDC),
|
||||
[0x14, coder.encode(["address"], [WETH])],
|
||||
),
|
||||
),
|
||||
left: tokenLine(WETH, "WETH"),
|
||||
},
|
||||
{
|
||||
kind: "a swap paying WETH to the router, a TRANSFER of 1 wei of it to the WETH/USDC pair, then a V2 swap of what the pair holds",
|
||||
data: execute(
|
||||
@@ -832,6 +914,18 @@ describe("any token left in the router is not received", () => {
|
||||
),
|
||||
min: "1990.0000 USDC",
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH into the router, then a V4 swap of all of it through two pools, settled from the router",
|
||||
data: execute(
|
||||
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||
v4Swap(
|
||||
swapExactIn(WETH, [USDT, USDC], ONE_WETH, USDC_1990),
|
||||
settle(WETH, 0n, false),
|
||||
takeAll(USDC),
|
||||
),
|
||||
),
|
||||
min: "1990.0000 USDC",
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of all the router's ETH, then a swap of all its WETH",
|
||||
data: execute(
|
||||
|
||||
Reference in New Issue
Block a user