1 Commits
Author SHA1 Message Date
sneak bccc86b6ea fix: Settings refuses an endpoint URL whose network changed before it was stored (closes #663)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
The RPC and Blockscout "Save" buttons in Settings now set the URL
inside the save, holding the lock. The save is refused if the network
active when the user tapped "Save" is no longer active, on the page or
in the stored record. The flash message then says that the network
changed, and "Saved." is shown only for a stored URL.

Before, a popup that followed a switch saved elsewhere could replace
the URL before the write and still show "Saved.". It could also write
the previous network's URL as the new network's.

Model: opus-5-5
2026-10-27 03:31:40 +00:00
5 changed files with 80 additions and 331 deletions
+52 -56
View File
@@ -3508,36 +3508,34 @@ Currently supported:
the router take `Token In` (see below): a permit of 5,000 DAI to another the router take `Token In` (see below): a permit of 5,000 DAI to another
address and then a V3 exact-in of 600 USDT read `Swap USDT → USDC` with address and then a V3 exact-in of 600 USDT read `Swap USDT → USDC` with
`Amount 600.0000 USDT`, and so do that exact-in and then a permit of 5,000 DAI `Amount 600.0000 USDT`, and so do that exact-in and then a permit of 5,000 DAI
to the router. A permit on the zero address never stands as the `Amount` to the router. When a permit's amount stands and the step that sets the input
figure, because Permit2 has no allowance of ETH. When a permit's amount stands side spends a token other than the permit's, or one the decoder does not know,
and the step that sets the input side spends a token other than the permit's, that step's amount gets lines of its own, unless it states no amount and the
or one the decoder does not know, that step's amount gets lines of its own, user pays it none of its input token. Each take the total does not count gives
unless it states no amount and the user pays it none of its input token. Each three lines after the `Amount` line (after `Token In` when there is none): one
take the total does not count gives three lines after the `Amount` line (after of a token other than `Token In`; one of `Token In` after an amount that is
`Token In` when there is none): one of a token other than `Token In`; one of not a quantity (a V4 open delta, `ALREADY_PAID`, which keep their wording on
`Token In` after an amount that is not a quantity (a V4 open delta, the `Amount` line, or no stated amount, as of a V4 exact-out step); and one
`ALREADY_PAID`, which keep their wording on the `Amount` line, or no stated whose amount the decoder does not know. When there is no `Amount` line, each
amount, as of a V4 exact-out step); and one whose amount the decoder does not take the total counts gets them too, so that every amount the decoder reads is
know. When there is no `Amount` line, each take the total counts gets them shown: the amount the step that sets the input side states, when it is a
too, so that every amount the decoder reads is shown: the amount the step that quantity and the user pays that step (a V2 or V3 step whose `payerIsUser` is
sets the input side states, when it is a quantity and the user pays that step true, a V4 step the user pays some of its input token, or a `WRAP_ETH`, whose
(a V2 or V3 step whose `payerIsUser` is true, a V4 step the user pays some of ETH comes from the transaction's value), and each later take of `Token In`
its input token, or a `WRAP_ETH`, whose ETH comes from the transaction's added to it. A step that sets the input side and is paid from the router takes
value), and each later take of `Token In` added to it. A step that sets the nothing from the user's wallet and gets none. A `PERMIT2_PERMIT` the decoder
input side and is paid from the router takes nothing from the user's wallet cannot read, then V3 exact-ins of 600 USDT and 400 DAI paid by the user, give
and gets none. A `PERMIT2_PERMIT` the decoder cannot read, then V3 exact-ins lines for both amounts. When the step that sets the input side states no
of 600 USDT and 400 DAI paid by the user, give lines for both amounts. When amount, the user pays it some of its input token, and there is no `Amount`
the step that sets the input side states no amount, the user pays it some of line, that step gets them too whenever another take's lines give a figure, or
its input token, and there is no `Amount` line, that step gets them too a `PERMIT2_TRANSFER_FROM` of its token gives one, on the lines below or, into
whenever another take's lines give a figure, or a `PERMIT2_TRANSFER_FROM` of the router, on its `Paid to` line, so that figure cannot read as all that
its token gives one, on the lines below or, into the router, on its `Paid to` leaves. When it states no amount and the user pays it none of its input token,
line, so that figure cannot read as all that leaves. When it states no amount as when the router pays it, that step takes none of that token from the user's
and the user pays it none of its input token, as when the router pays it, that wallet and never gets them, with a permit or without. They come in step order,
step takes none of that token from the user's wallet and never gets them, with except that the step that sets the input side comes first when it has them,
a permit or without. They come in step order, except that the step that sets followed, when there is no `Amount` line, by the other takes the total counts:
the input side comes first when it has them, followed, when there is no `Moved from the sender`, its amount, worded as the `Amount` line words one, or
`Amount` line, by the other takes the total counts: `Moved from the sender`,
its amount, worded as the `Amount` line words one, or
`Unknown (could not be decoded)` when the decoder does not know it; `Unknown (could not be decoded)` when the decoder does not know it;
`Token moved`, giving the token's address as the `Token In` line does; and `Token moved`, giving the token's address as the `Token In` line does; and
`Moved to`, which reads `The swap step that spends it`. A token the decoder `Moved to`, which reads `The swap step that spends it`. A token the decoder
@@ -3570,32 +3568,30 @@ Currently supported:
amount stands gives none: the `Amount` line gives its amount, `Token In` its amount stands gives none: the `Amount` line gives its amount, `Token In` its
token, and its spender is the router. Each allowance a `PERMIT2_PERMIT_BATCH` token, and its spender is the router. Each allowance a `PERMIT2_PERMIT_BATCH`
grants, one for each token it names, all for its one spender, gives the same grants, one for each token it names, all for its one spender, gives the same
three lines, in step order with those of each `PERMIT2_PERMIT`. For an three lines, in step order with those of each `PERMIT2_PERMIT`. A
allowance of the zero address, `Permit2 allowance` gives the amount in base `PERMIT2_PERMIT` or `PERMIT2_PERMIT_BATCH` whose input the decoder cannot
units and `Allowance token` gives the zero address. A `PERMIT2_PERMIT` or read, such as one whose token or spender word has a nonzero top byte, may let
`PERMIT2_PERMIT_BATCH` whose input the decoder cannot read, such as one whose any spender take any amount of any token: it gives one `Permit2 allowance`
token or spender word has a nonzero top byte, may let any spender take any line alone, reading `Unknown (could not be decoded)`. These lines change no
amount of any token: it gives one `Permit2 allowance` line alone, reading `Paid to` line: a permit or batch pays nobody. But code that a step runs, such
`Unknown (could not be decoded)`. These lines change no `Paid to` line: a as a V4 pool's hook or a token's own code, can call a spender other than the
permit or batch pays nobody. But code that a step runs, such as a V4 pool's router, which can then take up to its allowance from the user's wallet before
hook or a token's own code, can call a spender other than the router, which the transaction ends. So an allowance to any spender but the router, or a
can then take up to its allowance from the user's wallet before the permit or batch the decoder cannot read, makes the minimum read
transaction ends. So an allowance to any spender but the router, or a permit `Min. paid out`, and when its token is `Token In`, or the decoder does not
or batch the decoder cannot read, makes the minimum read `Min. paid out`, and know its token or `Token In`, it leaves no `Amount` line, whose figure might
when its token is `Token In`, or the decoder does not know its token or be too low: a permit of 5,000 USDT to another address and then a V3 exact-in
`Token In`, it leaves no `Amount` line, whose figure might be too low: a of 600 USDT read with no `Amount` line, and the 600 USDT gets lines of its
permit of 5,000 USDT to another address and then a V3 exact-in of 600 USDT own. A step goes unread when its recipient does not decode as an address, when
read with no `Amount` line, and the 600 USDT gets lines of its own. A step it is a V3 swap step whose path is shorter than one token address or has bytes
goes unread when its recipient does not decode as an address, when it is a V3 after its last hop, or when it is a command the decoder does not read: every
swap step whose path is shorter than one token address or has bytes after its command other than `PERMIT2_PERMIT`, `PERMIT2_PERMIT_BATCH`,
last hop, or when it is a command the decoder does not read: every command `PERMIT2_TRANSFER_FROM`, `V2_SWAP_EXACT_IN`, `V2_SWAP_EXACT_OUT`,
other than `PERMIT2_PERMIT`, `PERMIT2_PERMIT_BATCH`, `PERMIT2_TRANSFER_FROM`, `V3_SWAP_EXACT_IN`, `V3_SWAP_EXACT_OUT`, `V4_SWAP`, `SWEEP`, `TRANSFER`,
`V2_SWAP_EXACT_IN`, `V2_SWAP_EXACT_OUT`, `V3_SWAP_EXACT_IN`, `PAY_PORTION`, `PAY_PORTION_FULL_PRECISION`, `ACROSS_V4_DEPOSIT_V3`,
`V3_SWAP_EXACT_OUT`, `V4_SWAP`, `SWEEP`, `TRANSFER`, `PAY_PORTION`, `WRAP_ETH`, `UNWRAP_WETH` and `BALANCE_CHECK_ERC20`
`PAY_PORTION_FULL_PRECISION`, `ACROSS_V4_DEPOSIT_V3`, `WRAP_ETH`, (`PERMIT2_TRANSFER_FROM_BATCH`, `EXECUTE_SUB_PLAN` and the NFT commands among
`UNWRAP_WETH` and `BALANCE_CHECK_ERC20` (`PERMIT2_TRANSFER_FROM_BATCH`, them), or a type its release names no command for.
`EXECUTE_SUB_PLAN` and the NFT commands among them), or a type its release
names no command for.
### Non-Goals Forever ### Non-Goals Forever
+7 -15
View File
@@ -44,12 +44,6 @@ let ctx;
let versionClickCount = 0; let versionClickCount = 0;
let versionClickTimer = null; let versionClickTimer = null;
// The network Settings was last drawn for. A follow of a switch saved elsewhere
// sets `state` to the new network first. Settings is drawn again only once the
// follow's write returns. So both "Save" buttons take this network, not the one
// in `state`.
let shownNetwork = null;
// One row per site origin, however many addresses it appears under, each with // One row per site origin, however many addresses it appears under, each with
// an [x] that hands it to onRemove. // an [x] that hands it to onRemove.
function renderSiteList(containerId, origins, onRemove) { function renderSiteList(containerId, origins, onRemove) {
@@ -205,7 +199,6 @@ function renderWalletListSettings() {
} }
function show() { function show() {
shownNetwork = currentNetwork();
$("settings-rpc").value = state.rpcUrl; $("settings-rpc").value = state.rpcUrl;
$("settings-blockscout").value = state.blockscoutUrl; $("settings-blockscout").value = state.blockscoutUrl;
$("settings-network").value = state.networkId; $("settings-network").value = state.networkId;
@@ -313,7 +306,6 @@ function showNewSettings() {
state.checkRecipientsOnEtherscan; state.checkRecipientsOnEtherscan;
if (state.currentView === "settings") { if (state.currentView === "settings") {
markViewRendered("settings"); markViewRendered("settings");
shownNetwork = currentNetwork();
$("settings-network").value = state.networkId; $("settings-network").value = state.networkId;
$("settings-rpc").value = state.rpcUrl; $("settings-rpc").value = state.rpcUrl;
$("settings-blockscout").value = state.blockscoutUrl; $("settings-blockscout").value = state.blockscoutUrl;
@@ -328,9 +320,9 @@ function showNewSettings() {
} }
// Saves `url` as `field` ("rpcUrl" or "blockscoutUrl") of the network // Saves `url` as `field` ("rpcUrl" or "blockscoutUrl") of the network
// `networkId`, the one Settings showed when the user tapped "Save". The URL is // `networkId`, the one active when the user tapped "Save". The URL is set in
// set in `state` inside the save, holding the lock. So the page cannot follow a // `state` inside the save, holding the lock. So the page cannot follow a switch
// switch saved elsewhere (followStoredSettings() in src/popup/index.js) between // saved elsewhere (followStoredSettings() in src/popup/index.js) between
// setting the URL and writing it. If the active network is no longer // setting the URL and writing it. If the active network is no longer
// `networkId`, on this page or in the stored record, nothing is saved. The // `networkId`, on this page or in the stored record, nothing is saved. The
// flash message then says that the network changed. // flash message then says that the network changed.
@@ -360,8 +352,8 @@ function init(_ctx) {
showFlash("Please enter an RPC URL."); showFlash("Please enter an RPC URL.");
return; return;
} }
// The URL is for the network Settings shows when "Save" is tapped. // The URL is for the network active when "Save" is tapped.
const net = shownNetwork; const net = currentNetwork();
showFlash("Testing endpoint..."); showFlash("Testing endpoint...");
try { try {
const resp = await debugFetch(url, { const resp = await debugFetch(url, {
@@ -404,7 +396,7 @@ function init(_ctx) {
showFlash("Please enter a Blockscout API URL."); showFlash("Please enter a Blockscout API URL.");
return; return;
} }
const net = shownNetwork; const networkId = state.networkId;
showFlash("Testing endpoint..."); showFlash("Testing endpoint...");
try { try {
const resp = await debugFetch(url + "/stats"); const resp = await debugFetch(url + "/stats");
@@ -419,7 +411,7 @@ function init(_ctx) {
showFlash("Could not reach endpoint."); showFlash("Could not reach endpoint.");
return; return;
} }
await saveEndpoint("blockscoutUrl", url, net.id); await saveEndpoint("blockscoutUrl", url, networkId);
}); });
// Each change is made inside the save, holding the lock. So // Each change is made inside the save, holding the lock. So
+21 -47
View File
@@ -2032,14 +2032,12 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// it may take more. A later permit or batch, or a later step this // it may take more. A later permit or batch, or a later step this
// decoder did not read, may raise what Permit2 lets the router take. // decoder did not read, may raise what Permit2 lets the router take.
// Every other PERMIT2_PERMIT leaves Token In and the Amount line as // Every other PERMIT2_PERMIT leaves Token In and the Amount line as
// they read with no permit, and gets lines of its own below. A permit // they read with no permit, and gets lines of its own below. A step
// on the zero address never stands as the Amount figure, because // that may take from the sender what this decoder did not read leaves
// Permit2 has no allowance of ETH. A step that may take from the // no figure, permit or not (see spentUnread()), and so do a permit
// sender what this decoder did not read leaves no figure, permit or // or batch whose input it cannot read (see permitUnread()) and an
// not (see spentUnread()), and so do a permit or batch whose input it // allowance of Token In to another spender (see toAnotherSpender). An
// cannot read (see permitUnread()) and an allowance of Token In to // EXECUTE_SUB_PLAN step leaves both sides unknown (below).
// another spender (see toAnotherSpender). An EXECUTE_SUB_PLAN step
// leaves both sides unknown (below).
const stepCommands = Array.from( const stepCommands = Array.from(
commandsBytes, commandsBytes,
(b) => commands[b & commandTypeMask], (b) => commands[b & commandTypeMask],
@@ -2049,7 +2047,6 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
stepCommands.filter((c) => c === "PERMIT2_PERMIT").length === 1 && stepCommands.filter((c) => c === "PERMIT2_PERMIT").length === 1 &&
!stepCommands.includes("PERMIT2_PERMIT_BATCH") && !stepCommands.includes("PERMIT2_PERMIT_BATCH") &&
sameAddress(permit.spender, toAddress) && sameAddress(permit.spender, toAddress) &&
permit.token !== "0x0000000000000000000000000000000000000000" &&
!permit.allowedToFail && !permit.allowedToFail &&
permit.beforeEveryTake && permit.beforeEveryTake &&
!stepsUnread.some((step) => step > permit.step); !stepsUnread.some((step) => step > permit.step);
@@ -2237,19 +2234,17 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// A null token in `spentFromSender`, a step this decoder did not read // A null token in `spentFromSender`, a step this decoder did not read
// or a token it does not know, and an allowance to another spender // or a token it does not know, and an allowance to another spender
// that may be of Token In, leave no Amount figure, which may then be // that may be of Token In, leave no Amount figure, which may then be
// too low. An allowance of the zero address is never one of Token In, // too low. With no Amount line, each take the total adds up gets
// because Permit2 has no allowance of ETH. With no Amount line, each // lines of its own, first, so that every amount this decoder read is
// take the total adds up gets lines of its own, first, so that every // shown.
// amount this decoder read is shown.
if ( if (
present(inputAmount) && present(inputAmount) &&
spentFromSender.every((s) => present(s.token)) && spentFromSender.every((s) => present(s.token)) &&
toAnotherSpender.every( toAnotherSpender.every(
(p) => (p) =>
p.token === "0x0000000000000000000000000000000000000000" || present(p.token) &&
(present(p.token) && present(inputToken) &&
present(inputToken) && !sameAddress(p.token, inputToken),
!sameAddress(p.token, inputToken)),
) )
) { ) {
const amount = spentText(inputAmount, inInfo); const amount = spentText(inputAmount, inInfo);
@@ -2320,36 +2315,15 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
}); });
continue; continue;
} }
// Permit2 has no allowance of ETH. An allowance of the zero details.push({
// address reads as one of a token nothing knows: its amount in label: "Permit2 allowance",
// base units, and the zero address as its token. value:
if (p.token === "0x0000000000000000000000000000000000000000") { p.amount >= MAX_UINT160
details.push({ ? "Unlimited"
label: "Permit2 allowance", : amountText(p.amount, tokenInfo(p.token, sources))
value: .display,
p.amount >= MAX_UINT160 });
? "Unlimited" details.push(tokenMovedLine("Allowance token", p.token, sources));
: unknownDecimalsAmount(p.amount),
});
details.push({
label: "Allowance token",
value: p.token,
address: p.token,
isToken: true,
});
} else {
details.push({
label: "Permit2 allowance",
value:
p.amount >= MAX_UINT160
? "Unlimited"
: amountText(p.amount, tokenInfo(p.token, sources))
.display,
});
details.push(
tokenMovedLine("Allowance token", p.token, sources),
);
}
details.push({ details.push({
label: "Allowance spender", label: "Allowance spender",
value: p.spender, value: p.spender,
-84
View File
@@ -319,90 +319,6 @@ test.each([
}, },
); );
// The popup's follow of the background's switch to Sepolia has set `state` to
// Sepolia. The follow's write has not returned yet, so Settings still shows
// Ethereum Mainnet. The user taps "Save" there. The URL is for Ethereum
// Mainnet, so the save stores the URL for neither network.
test.each([
{ button: "btn-save-rpc", field: "settings-rpc" },
{ button: "btn-save-blockscout", field: "settings-blockscout" },
])(
"$button saves nothing when tapped while the follow's write waits",
async ({ button, field }) => {
const page = await bootOnAddress();
await page.click("btn-settings");
// This answer passes both endpoint checks on Ethereum Mainnet.
globalThis.fetch.mockResolvedValue({
ok: true,
json: async () => ({ result: NETWORKS.mainnet.chainId }),
});
const url = "https://own-node.example.invalid";
const { set } = page.storage.local;
const write = set.getMockImplementation();
let releaseWrite;
const writeHeld = new Promise((resolve) => {
releaseWrite = resolve;
});
// The background's write lands; the next, the follow's, waits.
set.mockImplementationOnce(write).mockImplementationOnce(
async (items) => {
await writeHeld;
await write(items);
},
);
await backgroundSwitch(page, "sepolia");
const { state } = require("../src/shared/state");
expect(state.networkId).toBe("sepolia");
expect(page.value("settings-network")).toBe("mainnet");
page.node(field).value = url;
const saving = page.node(button).listeners.click.map((fn) => fn());
await page.settle();
releaseWrite();
await Promise.all(saving);
await page.settle();
expect(JSON.stringify(page.storage.read("autistmask"))).not.toContain(
url,
);
expect(page.text("flash-msg")).toBe(
"The URL was not saved because the network changed.",
);
},
);
// The user taps the RPC "Save" on Ethereum Mainnet. While the check waits for
// the node's answer, the popup follows the background's switch to Sepolia. The
// node answers with Sepolia's chain id. So the check refuses the node for
// Ethereum Mainnet, and nothing is stored.
test("the RPC check compares the answer with the network at the tap", async () => {
const page = await bootOnAddress();
await page.click("btn-settings");
let answer;
globalThis.fetch.mockReturnValue(
new Promise((resolve) => {
answer = resolve;
}),
);
const url = "https://sepolia-node.example.invalid";
page.node("settings-rpc").value = url;
const saving = page.node("btn-save-rpc").listeners.click.map((fn) => fn());
await page.settle();
await backgroundSwitch(page, "sepolia");
const { state } = require("../src/shared/state");
expect(state.networkId).toBe("sepolia");
answer({ json: async () => ({ result: NETWORKS.sepolia.chainId }) });
await Promise.all(saving);
await page.settle();
expect(JSON.stringify(page.storage.read("autistmask"))).not.toContain(url);
expect(page.text("flash-msg")).toBe(
"Wrong network: expected Ethereum Mainnet.",
);
});
// Such as one a newer build wrote, which this build cannot read. Nothing reads // Such as one a newer build wrote, which this build cannot read. Nothing reads
// or writes the stored record for it: the popup's next save finds it, as // or writes the stored record for it: the popup's next save finds it, as
// before. // before.
-129
View File
@@ -3310,135 +3310,6 @@ describe("uniswap decoder", () => {
expect(detail(result, "Amount").value).toBe("Up to 1000.0000 USDT"); expect(detail(result, "Amount").value).toBe("Up to 1000.0000 USDT");
}); });
// https://git.eeqj.de/sneak/AutistMask/issues/660 — Permit2 has no
// allowance of ETH, so a permit on the zero address reads as one on a
// token nothing knows. On next at 53551c6 such a permit's Allowance
// token line read ETH. A permit of 1 wei of the zero address before a
// V4 step of 5 ETH made the Amount line read 0.000000000000000001 ETH.
// With that permit to another spender and a SETTLE the router pays, no
// line gave the 5 ETH.
describe("a permit on the zero address", () => {
const FIVE_ETH = 5000000000000000000n;
// The lines of a permit of `amount` base units of the zero
// address for `spender`.
const zeroAddressLines = (amount, spender) => [
[
"Permit2 allowance",
amount + " base units (decimals unknown)",
],
["Allowance token", ZERO_ADDR],
["Allowance spender", getAddress(spender)],
];
// A V4 swap of 5 ETH for USDC. The user pays its SETTLE when
// `payerIsUser` is true; otherwise the router pays it from the
// transaction's value.
const v4FiveEth = (payerIsUser) =>
encodeV4Swap(
new Uint8Array([
V4_SWAP_EXACT_IN_SINGLE_ID,
V4_SETTLE_ID,
V4_TAKE_ALL_ID,
]),
[
encodeV4ExactInSingle(
ZERO_ADDR,
USDC_ADDR,
FIVE_ETH,
1n,
),
settle(ZERO_ADDR, FIVE_ETH, payerIsUser),
encodeV4TakeAll(USDC_ADDR, 1n),
],
);
test("a PERMIT2_PERMIT on the zero address names the address", () => {
const result = decodeSteps(
[0x00, 0x0a],
[
V3_600,
encodePermit2(
ZERO_ADDR,
1000000000000000000n,
ROUTER_ADDR,
),
],
);
expect(linesBetweenTokens(result)).toEqual([
["Amount", "600.0000 USDT"],
...zeroAddressLines("1000000000000000000", ROUTER_ADDR),
]);
expect(detail(result, "Allowance token").address).toBe(
ZERO_ADDR,
);
});
test("a PERMIT2_PERMIT_BATCH entry on the zero address names the address", () => {
const result = decodeSteps(
[0x03, 0x00],
[
encodePermit2Batch(
[
[USDT_ADDR, USDT_600],
[ZERO_ADDR, 1000000000000000000n],
],
ROUTER_ADDR,
),
V3_600,
],
);
expect(linesBetweenTokens(result)).toEqual([
["Amount", "600.0000 USDT"],
...USDT_600_LINES,
...zeroAddressLines("1000000000000000000", ROUTER_ADDR),
]);
});
test("a PERMIT2_PERMIT of 1 wei of the zero address, then a V4 step of 5 ETH the user settles, shows the 5 ETH", () => {
const result = decodeSteps(
[0x0a, 0x10],
[
encodePermit2(ZERO_ADDR, 1n, ROUTER_ADDR),
v4FiveEth(true),
],
);
expect(detail(result, "Token In").value).toBe("ETH (native)");
expect(linesBetweenTokens(result)).toEqual([
["Amount", "5.0000 ETH"],
...zeroAddressLines("1", ROUTER_ADDR),
]);
});
test("a PERMIT2_PERMIT of 1 wei of the zero address to another spender, then a V4 step of 5 ETH the router settles, shows the 5 ETH", () => {
const result = decodeSteps(
[0x0a, 0x10],
[
encodePermit2(ZERO_ADDR, 1n, OTHER_ADDR),
v4FiveEth(false),
],
);
expect(detail(result, "Token In").value).toBe("ETH (native)");
expect(linesBetweenTokens(result)).toEqual([
["Amount", "5.0000 ETH"],
...zeroAddressLines("1", OTHER_ADDR),
]);
});
test("a PERMIT2_PERMIT_BATCH entry of 1 wei of the zero address to another spender, then a V4 step of 5 ETH the router settles, shows the 5 ETH", () => {
const result = decodeSteps(
[0x03, 0x10],
[
encodePermit2Batch([[ZERO_ADDR, 1n]], OTHER_ADDR),
v4FiveEth(false),
],
);
expect(detail(result, "Token In").value).toBe("ETH (native)");
expect(linesBetweenTokens(result)).toEqual([
["Amount", "5.0000 ETH"],
...zeroAddressLines("1", OTHER_ADDR),
]);
});
});
}); });
// https://git.eeqj.de/sneak/AutistMask/issues/359 — the output half, in // https://git.eeqj.de/sneak/AutistMask/issues/359 — the output half, in