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
|
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
|
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
|
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
|
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,
|
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
|
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
|
the token uses and that the transaction cannot be sent; for any other
|
||||||
failure it asks the user to go back and try again
|
failure it asks the user to go back and try again
|
||||||
- Two more reserved error boxes, made visible when "Sign & Send" refuses a
|
- Three 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
|
send because the active network changed, because the selected wallet does
|
||||||
(see Transitions), so the refusal moves nothing
|
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
|
- Password: an inline field on this screen, not a modal, with its own error
|
||||||
line
|
line
|
||||||
- "Sign & Send" button (disabled if errors, while the balance is being read,
|
- "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
|
- "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
|
selected: the two are saved separately, so the popup can reopen onto this
|
||||||
screen with another address selected. When the selected wallet does not
|
screen with another address selected. When the selected wallet does not
|
||||||
hold the From address → full-sentence error on the password error line,
|
hold the From address → nothing is signed; "The selected wallet does not
|
||||||
nothing sent (no screen change)
|
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,
|
- "Sign & Send" (correct password, but the From address's key is refused,
|
||||||
see Data Model) → nothing is signed → **ErrorTx** saying why
|
see Data Model) → nothing is signed → **ErrorTx** saying why
|
||||||
- "Back" → **Send**
|
- "Back" → **Send**
|
||||||
@@ -3060,32 +3066,35 @@ Currently supported:
|
|||||||
or V3 exact-in swap step or V4 `SETTLE` that pays `CONTRACT_BALANCE` of that
|
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
|
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
|
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`
|
`WRAP_ETH` states, as a swap paid in ETH does. The router pays a `SETTLE` of
|
||||||
(zero) from the router counts as spending what its step's swap spends only
|
`OPEN_DELTA` (zero) only what its step still owes of that token when the
|
||||||
when it is the step's only `SETTLE` or `SETTLE_ALL` of that token, comes after
|
`SETTLE` runs, so one from the router counts as spending the amounts the
|
||||||
every swap action in the step, and no swap action in the step buys that token,
|
step's swap actions state they spend of that token only when every action in
|
||||||
because the router pays only what the step still owes of that token when the
|
the step could be read, every swap action names the token it spends and the
|
||||||
`SETTLE` runs; otherwise it counts as spending nothing. So the rest of a V4
|
token it buys (a swap action with an empty path swaps nothing), the `SETTLE`
|
||||||
`SETTLE` of more than its swap spends, taken to the router, the WETH of a
|
is the step's only `SETTLE` or `SETTLE_ALL` of that token and comes after
|
||||||
`WRAP_ETH`, and a token a swap pays the router on the way to the output, each
|
every swap action, no swap action buys that token, and the swap actions that
|
||||||
stays in the router when later steps do not spend all of it, where anyone can
|
spend it state a quantity together; otherwise it counts as spending nothing.
|
||||||
take it in a later transaction. The router sends ETH as written, so a
|
So the rest of a V4 `SETTLE` of more than its swap spends, taken to the
|
||||||
`TRANSFER` of `CONTRACT_BALANCE` of ETH always reverts and pays out nothing. A
|
router, the WETH of a `WRAP_ETH`, and a token a swap pays the router on the
|
||||||
step paying a token to the V2 pair that the very next step spends, as a V2
|
way to the output, each stays in the router when later steps do not spend all
|
||||||
exact-in of that token whose `amountIn` is `ALREADY_PAID` (zero) or
|
of it, where anyone can take it in a later transaction. The router sends ETH
|
||||||
`CONTRACT_BALANCE`, is a hop, not a payout: Uniswap's mixed routes pay the
|
as written, so a `TRANSFER` of `CONTRACT_BALANCE` of ETH always reverts and
|
||||||
next V2 pair directly, and the router enforces that swap's own minimum. The
|
pays out nothing. A step paying a token to the V2 pair that the very next step
|
||||||
decoder checks that the address paid is that pair, deriving it from the V2
|
spends, as a V2 exact-in of that token whose `amountIn` is `ALREADY_PAID`
|
||||||
factory and pair init code hash the network's routers were deployed with and
|
(zero) or `CONTRACT_BALANCE`, is a hop, not a payout: Uniswap's mixed routes
|
||||||
the first two tokens of that step's path; a payment to any other address is
|
pay the next V2 pair directly, and the router enforces that swap's own
|
||||||
named. (Sepolia's `UniversalRouterV1_2_V2Support` was deployed with
|
minimum. The decoder checks that the address paid is that pair, deriving it
|
||||||
placeholders for both values, so no V2 swap step can succeed on that router.)
|
from the V2 factory and pair init code hash the network's routers were
|
||||||
Every other payment counts, in whichever step and of whatever token, a share
|
deployed with and the first two tokens of that step's path; a payment to any
|
||||||
of the output included: an interface fee paid to another address by
|
other address is named. (Sepolia's `UniversalRouterV1_2_V2Support` was
|
||||||
`PAY_PORTION`, `TRANSFER` or `TAKE_PORTION` is named, and the minimum reads
|
deployed with placeholders for both values, so no V2 swap step can succeed on
|
||||||
`Min. paid out`, because the swap's minimum includes the fee. The minimum
|
that router.) Every other payment counts, in whichever step and of whatever
|
||||||
reads `Min. received` only when some step pays the transaction's sender (the
|
token, a share of the output included: an interface fee paid to another
|
||||||
From address on TxApproval, the sender the explorer reports on
|
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
|
TransactionDetail) and no `Paid to` line is shown. `Paid to` lines after
|
||||||
`Token Out` name each other address paid; one reads
|
`Token Out` name each other address paid; one reads
|
||||||
`The bridge, which pays the deposit's recipient on another network` for a
|
`The bridge, which pays the deposit's recipient on another network` for a
|
||||||
|
|||||||
+11
-3
@@ -697,9 +697,10 @@
|
|||||||
id="confirm-fee-unknown-error"
|
id="confirm-fee-unknown-error"
|
||||||
class="mb-2 border border-border border-dashed p-2 text-xs invisible"
|
class="mb-2 border border-border border-dashed p-2 text-xs invisible"
|
||||||
></div>
|
></div>
|
||||||
<!-- Shown when Sign & Send refuses a send for its network, or
|
<!-- Shown when Sign & Send refuses a send for its network, for
|
||||||
for its gas limit or fee. Each holds its space from the first
|
a From address the selected wallet does not hold, or for its
|
||||||
paint, so the refusal moves nothing. -->
|
gas limit or fee. Each holds its space from the first paint, so
|
||||||
|
the refusal moves nothing. -->
|
||||||
<div
|
<div
|
||||||
id="confirm-network-changed-error"
|
id="confirm-network-changed-error"
|
||||||
class="mb-2 border border-border border-dashed p-2 text-xs invisible"
|
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
|
reviewed, so it was not sent. Please go back and review it
|
||||||
again.
|
again.
|
||||||
</div>
|
</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
|
<div
|
||||||
id="confirm-fee-bound-error"
|
id="confirm-fee-bound-error"
|
||||||
class="mb-2 border border-border border-dashed p-2 text-xs invisible"
|
class="mb-2 border border-border border-dashed p-2 text-xs invisible"
|
||||||
|
|||||||
@@ -320,6 +320,7 @@ function show(txInfo) {
|
|||||||
$("confirm-tx-password").value = "";
|
$("confirm-tx-password").value = "";
|
||||||
hideError("confirm-tx-password-error");
|
hideError("confirm-tx-password-error");
|
||||||
setVisible("confirm-network-changed-error", false);
|
setVisible("confirm-network-changed-error", false);
|
||||||
|
setVisible("confirm-from-address-error", false);
|
||||||
setVisible("confirm-fee-bound-error", false);
|
setVisible("confirm-fee-bound-error", false);
|
||||||
|
|
||||||
// Gas estimate — show placeholder then fetch async
|
// Gas estimate — show placeholder then fetch async
|
||||||
@@ -661,6 +662,7 @@ function init(_ctx) {
|
|||||||
$("btn-confirm-send").addEventListener("click", async () => {
|
$("btn-confirm-send").addEventListener("click", async () => {
|
||||||
// A press shows only its own refusal, if any.
|
// A press shows only its own refusal, if any.
|
||||||
setVisible("confirm-network-changed-error", false);
|
setVisible("confirm-network-changed-error", false);
|
||||||
|
setVisible("confirm-from-address-error", false);
|
||||||
setVisible("confirm-fee-bound-error", false);
|
setVisible("confirm-fee-bound-error", false);
|
||||||
|
|
||||||
const wallet = state.wallets[state.selectedWallet];
|
const wallet = state.wallets[state.selectedWallet];
|
||||||
@@ -686,11 +688,7 @@ function init(_ctx) {
|
|||||||
// address selected.
|
// address selected.
|
||||||
const addr = wallet.addresses.find((a) => a.address === pendingTx.from);
|
const addr = wallet.addresses.find((a) => a.address === pendingTx.from);
|
||||||
if (!addr) {
|
if (!addr) {
|
||||||
showError(
|
setVisible("confirm-from-address-error", true);
|
||||||
"confirm-tx-password-error",
|
|
||||||
"The selected wallet does not hold the From address above, " +
|
|
||||||
"so nothing was sent. Please go back and send again.",
|
|
||||||
);
|
|
||||||
return;
|
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.
|
// 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
|
// `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
|
// read. `settledFromRouter` is every SETTLE that the router pays from its own
|
||||||
// balance (`payerIsUser` false), as the currency, the amount as written and
|
// balance (`payerIsUser` false), as its currency and the amount it pays: a
|
||||||
// its index among the step's actions, except that a SETTLE of OPEN_DELTA
|
// quantity, CONTRACT_BALANCE (all the router holds), or null when this
|
||||||
// whose payment this decoder cannot follow has no amount (null; see below).
|
// decoder cannot tell (see below).
|
||||||
function decodeV4Swap(input) {
|
function decodeV4Swap(input) {
|
||||||
try {
|
try {
|
||||||
const d = coder.decode(["bytes", "bytes[]"], input);
|
const d = coder.decode(["bytes", "bytes[]"], input);
|
||||||
@@ -899,11 +899,15 @@ function decodeV4Swap(input) {
|
|||||||
let settleToken = null;
|
let settleToken = null;
|
||||||
let takeToken = null;
|
let takeToken = null;
|
||||||
const takes = [];
|
const takes = [];
|
||||||
const settledFromRouter = [];
|
// Every SETTLE paid from the router, as its currency, its amount as
|
||||||
// The currency of every SETTLE and SETTLE_ALL, in order.
|
// written and its index among the step's actions.
|
||||||
|
const routerSettles = [];
|
||||||
|
// The currency of every SETTLE and SETTLE_ALL.
|
||||||
const settledCurrencies = [];
|
const settledCurrencies = [];
|
||||||
// The index of the step's last swap action, read or not, or -1.
|
// The index of the step's last swap action, read or not, or -1.
|
||||||
let lastSwapAction = -1;
|
let lastSwapAction = -1;
|
||||||
|
// Set when an action of the step could not be read.
|
||||||
|
let actionUnread = false;
|
||||||
let recipientUnread = false;
|
let recipientUnread = false;
|
||||||
// The step's swap actions that could be read, as decodeV4SwapAction()
|
// The step's swap actions that could be read, as decodeV4SwapAction()
|
||||||
// reads them.
|
// reads them.
|
||||||
@@ -925,7 +929,7 @@ function decodeV4Swap(input) {
|
|||||||
settleToken = s[0];
|
settleToken = s[0];
|
||||||
settledCurrencies.push(s[0]);
|
settledCurrencies.push(s[0]);
|
||||||
if (!s[2]) {
|
if (!s[2]) {
|
||||||
settledFromRouter.push({
|
routerSettles.push({
|
||||||
currency: s[0],
|
currency: s[0],
|
||||||
amount: s[1],
|
amount: s[1],
|
||||||
index: i,
|
index: i,
|
||||||
@@ -967,14 +971,18 @@ function decodeV4Swap(input) {
|
|||||||
swaps.push(decodeV4SwapAction(actionId, params[i]));
|
swaps.push(decodeV4SwapAction(actionId, params[i]));
|
||||||
} catch {
|
} catch {
|
||||||
swapUndecoded = true;
|
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 {
|
} catch {
|
||||||
// Skip sub-actions we can't decode. A TAKE, TAKE_ALL or
|
// Skip sub-actions we can't decode. A TAKE, TAKE_ALL or
|
||||||
// TAKE_PORTION among them is a payout this decoder did not
|
// TAKE_PORTION among them is a payout this decoder did not
|
||||||
// read.
|
// read.
|
||||||
|
actionUnread = true;
|
||||||
if (
|
if (
|
||||||
actionId === V4_TAKE ||
|
actionId === V4_TAKE ||
|
||||||
actionId === V4_TAKE_ALL ||
|
actionId === V4_TAKE_ALL ||
|
||||||
@@ -985,23 +993,40 @@ function decodeV4Swap(input) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A SETTLE of OPEN_DELTA (zero) paid from the router pays what the
|
// What each SETTLE paid from the router pays. A quantity or
|
||||||
// step still owes of its currency when it runs. That is what the
|
// CONTRACT_BALANCE pays as written. A SETTLE of OPEN_DELTA (zero)
|
||||||
// step's swap actions spend of it only when the SETTLE is the step's
|
// pays whatever the step still owes of its currency when it runs.
|
||||||
// one SETTLE or SETTLE_ALL of that currency, comes after every swap
|
// That is what the step's swap actions state they spend of it only
|
||||||
// action, and no swap action buys that currency (an unread one may).
|
// in the plain shape of Uniswap's own routes, so only then is it
|
||||||
// Any other such SETTLE pays an amount this decoder cannot tell.
|
// given that amount: every action in the step was read; every swap
|
||||||
for (const s of settledFromRouter) {
|
// 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 =
|
const paysWhatSwapsSpend =
|
||||||
|
!actionUnread &&
|
||||||
|
swaps.every((w) => present(w.tokenIn) && present(w.tokenOut)) &&
|
||||||
settledCurrencies.filter((c) => c === s.currency).length ===
|
settledCurrencies.filter((c) => c === s.currency).length ===
|
||||||
1 &&
|
1 &&
|
||||||
s.index > lastSwapAction &&
|
s.index > lastSwapAction &&
|
||||||
!swapUndecoded &&
|
!swaps.some((w) => w.tokenOut === s.currency) &&
|
||||||
!swaps.some((w) => w.tokenOut === s.currency);
|
typeof stated === "bigint";
|
||||||
if (s.amount === 0n && !paysWhatSwapsSpend) {
|
return {
|
||||||
s.amount = null;
|
currency: s.currency,
|
||||||
}
|
amount: paysWhatSwapsSpend ? stated : null,
|
||||||
}
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Rule 3.
|
// Rule 3.
|
||||||
if (swapUndecoded) {
|
if (swapUndecoded) {
|
||||||
@@ -1164,12 +1189,13 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
return r === ADDRESS_THIS || r === toAddress.toLowerCase();
|
return r === ADDRESS_THIS || r === toAddress.toLowerCase();
|
||||||
};
|
};
|
||||||
// A swap step or V4 SETTLE pays `amount` of `token` from the router's
|
// 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
|
// own balance. CONTRACT_BALANCE pays all of it. Any other quantity
|
||||||
// step states counts against the amount a WRAP_ETH stated, and the
|
// counts against the amount a WRAP_ETH stated, and the steps pay all
|
||||||
// steps pay all of it once their amounts reach that one: a swap paid
|
// of it once their amounts reach that one: a swap paid in ETH wraps
|
||||||
// in ETH wraps what its swap steps then spend. An amount the step
|
// what its swap steps then spend. An amount that is not a quantity,
|
||||||
// does not state, or one of a token whose amount is not known, pays
|
// such as the null decodeV4Swap() gives a SETTLE whose payment it
|
||||||
// out nothing this decoder can see.
|
// cannot tell, or one of a token whose amount is not known, pays out
|
||||||
|
// nothing this decoder can see.
|
||||||
const spendFromRouter = (token, amount) => {
|
const spendFromRouter = (token, amount) => {
|
||||||
const t = token.toLowerCase();
|
const t = token.toLowerCase();
|
||||||
const held = heldByRouter.get(t);
|
const held = heldByRouter.get(t);
|
||||||
@@ -1457,19 +1483,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
} else if (command === "V4_SWAP") {
|
} else if (command === "V4_SWAP") {
|
||||||
const v4 = decodeV4Swap(inputs[i]);
|
const v4 = decodeV4Swap(inputs[i]);
|
||||||
if (v4) {
|
if (v4) {
|
||||||
// A SETTLE of OPEN_DELTA (zero) that decodeV4Swap()
|
// Each SETTLE paid from the router spends the amount
|
||||||
// left at zero pays what the step's swap actions
|
// decodeV4Swap() says it pays.
|
||||||
// spend of its currency, which the step states as its
|
|
||||||
// amount when that currency is its Token In.
|
|
||||||
for (const s of v4.settledFromRouter) {
|
for (const s of v4.settledFromRouter) {
|
||||||
let amount = s.amount;
|
spendFromRouter(s.currency, s.amount);
|
||||||
if (amount === 0n) {
|
|
||||||
amount =
|
|
||||||
s.currency === v4.tokenIn
|
|
||||||
? v4.amountIn
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
spendFromRouter(s.currency, amount);
|
|
||||||
}
|
}
|
||||||
for (const t of v4.takes) {
|
for (const t of v4.takes) {
|
||||||
if (!isHop(i, t.currency, t.recipient)) {
|
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);
|
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 () => {
|
test("nothing is sent when the selected wallet does not hold it", async () => {
|
||||||
expect(await confirmSend(derived(5).address, 0)).toBeUndefined();
|
expect(await confirmSend(derived(5).address, 0)).toBeUndefined();
|
||||||
expect(node("confirm-tx-password-error").textContent).toBe(
|
expect(node("confirm-from-address-error").style.visibility).toBe(
|
||||||
"The selected wallet does not hold the From address above, " +
|
"visible",
|
||||||
"so nothing was sent. Please go back and send again.",
|
);
|
||||||
|
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)
|
// ------------------------------ the wait for a receipt ending in error (#315)
|
||||||
//
|
//
|
||||||
// README.md (WaitTx) documents two ways the wait ends on the error screen: a
|
// 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.
|
// The line giving a token left in the router.
|
||||||
const tokenLine = (token, symbol) => ({
|
const tokenLine = (token, symbol) => ({
|
||||||
label: "Token moved into the router",
|
label: "Token moved into the router",
|
||||||
@@ -709,6 +737,60 @@ describe("any token left in the router is not received", () => {
|
|||||||
),
|
),
|
||||||
left: tokenLine(WETH, "WETH"),
|
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",
|
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(
|
data: execute(
|
||||||
@@ -832,6 +914,18 @@ describe("any token left in the router is not received", () => {
|
|||||||
),
|
),
|
||||||
min: "1990.0000 USDC",
|
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",
|
kind: "a WRAP_ETH of all the router's ETH, then a swap of all its WETH",
|
||||||
data: execute(
|
data: execute(
|
||||||
|
|||||||
Reference in New Issue
Block a user