fix: always name a swap's output token, by address when no symbol is known (closes #346) #352
11
TODO.md
11
TODO.md
@@ -45,6 +45,17 @@ but the review is broader than any of them.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-08-23: A swap always names its output token
|
||||||
|
([#346](https://git.eeqj.de/sneak/AutistMask/issues/346)). The `Token Out`
|
||||||
|
detail line in `src/shared/uniswap.js` was pushed only when a symbol was
|
||||||
|
known, so a swap whose output token is absent from the bundled list — every
|
||||||
|
newly listed token — showed a `Min. received` figure with nothing saying what
|
||||||
|
was being received. The line is now keyed on the token's address and falls
|
||||||
|
back to it when there is no symbol, exactly as the `Token In` line already
|
||||||
|
did. It composes with the unknown-scale refusal from
|
||||||
|
[#340](https://git.eeqj.de/sneak/AutistMask/issues/340): the address says
|
||||||
|
which token, the base-unit figure says how much and states that the scale is
|
||||||
|
unknown.
|
||||||
- 2026-08-23: The swap approval screen no longer guesses 18 decimals for a token
|
- 2026-08-23: The swap approval screen no longer guesses 18 decimals for a token
|
||||||
outside the bundled list
|
outside the bundled list
|
||||||
([#340](https://git.eeqj.de/sneak/AutistMask/issues/340)). `tokenInfo()` in
|
([#340](https://git.eeqj.de/sneak/AutistMask/issues/340)). `tokenInfo()` in
|
||||||
|
|||||||
@@ -496,21 +496,24 @@ function decode(data, toAddress, sources) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outSymbol) {
|
// Keyed on the address, not the symbol: a token absent from the
|
||||||
|
// bundled list has no symbol, and gating the line on one dropped it
|
||||||
|
// entirely, leaving a Min. received figure with nothing saying what is
|
||||||
|
// being received. The Token In line above already falls back to the
|
||||||
|
// address; this does the same.
|
||||||
if (outInfo.address) {
|
if (outInfo.address) {
|
||||||
const label = outSymbol
|
const label = outSymbol
|
||||||
? outSymbol + " (" + outputToken + ")"
|
? outSymbol + " (" + outInfo.address + ")"
|
||||||
: outputToken;
|
: outInfo.address;
|
||||||
details.push({
|
details.push({
|
||||||
label: "Token Out",
|
label: "Token Out",
|
||||||
value: label,
|
value: label,
|
||||||
address: outputToken,
|
address: outInfo.address,
|
||||||
isToken: true,
|
isToken: true,
|
||||||
});
|
});
|
||||||
} else {
|
} else if (outSymbol) {
|
||||||
details.push({ label: "Token Out", value: outSymbol });
|
details.push({ label: "Token Out", value: outSymbol });
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (minOutput !== null && minOutput !== undefined) {
|
if (minOutput !== null && minOutput !== undefined) {
|
||||||
details.push({
|
details.push({
|
||||||
|
|||||||
107
tests/uniswapTokenOut.test.js
Normal file
107
tests/uniswapTokenOut.test.js
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
// The output token of a swap, as the dApp approval screen names it.
|
||||||
|
//
|
||||||
|
// Issue #346: the `Token Out` detail line in `src/shared/uniswap.js` was
|
||||||
|
// pushed only `if (outSymbol)`, and a token absent from the bundled list has
|
||||||
|
// no symbol. The line was therefore dropped entirely for exactly that
|
||||||
|
// population — which is every newly listed token — leaving a `Min. received`
|
||||||
|
// figure with nothing on the screen saying what is being received. The
|
||||||
|
// `Token In` line already falls back to the address; the rule asserted here is
|
||||||
|
// that the output side does too, always.
|
||||||
|
|
||||||
|
const { AbiCoder, Interface, getAddress } = require("ethers");
|
||||||
|
const uniswap = require("../src/shared/uniswap");
|
||||||
|
const { unknownDecimalsAmount } = require("../src/shared/approvalAmount");
|
||||||
|
|
||||||
|
const ROUTER = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af";
|
||||||
|
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
|
||||||
|
// In the bundled list, at 18 decimals.
|
||||||
|
const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
|
||||||
|
// Outside it, as every newly listed token is. Checksummed, because that is
|
||||||
|
// the form the decoder gets back from the ABI decode and puts on the line.
|
||||||
|
const NOVEL_OUT = getAddress("0xd0d0000000000000000000000000000000000d0d");
|
||||||
|
|
||||||
|
const HALF_WETH = 500000000000000000n;
|
||||||
|
// 1,000.00 of a 6-decimal token.
|
||||||
|
const THOUSAND_AT_SIX = 1000000000n;
|
||||||
|
|
||||||
|
const coder = AbiCoder.defaultAbiCoder();
|
||||||
|
const routerIface = new Interface([
|
||||||
|
"function execute(bytes commands, bytes[] inputs, uint256 deadline)",
|
||||||
|
]);
|
||||||
|
|
||||||
|
// A V2_SWAP_EXACT_IN (command 0x08) execute() call.
|
||||||
|
function swapData(tokenIn, amountIn, tokenOut, amountOutMin) {
|
||||||
|
const input = coder.encode(
|
||||||
|
["address", "uint256", "uint256", "address[]", "bool"],
|
||||||
|
[RECIPIENT, amountIn, amountOutMin, [tokenIn, tokenOut], true],
|
||||||
|
);
|
||||||
|
return routerIface.encodeFunctionData("execute", [
|
||||||
|
"0x08",
|
||||||
|
[input],
|
||||||
|
9999999999n,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// An UNWRAP_WETH (command 0x0c) execute() call: the output side is native ETH,
|
||||||
|
// which has no contract address to name.
|
||||||
|
function unwrapData() {
|
||||||
|
return routerIface.encodeFunctionData("execute", [
|
||||||
|
"0x0c",
|
||||||
|
[coder.encode(["address", "uint256"], [RECIPIENT, HALF_WETH])],
|
||||||
|
9999999999n,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function detail(data, label, sources) {
|
||||||
|
const decoded = uniswap.decode(data, ROUTER, sources || {});
|
||||||
|
return decoded.details.find((d) => d.label === label);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("a swap to a token absent from the bundled list", () => {
|
||||||
|
const data = () => swapData(WETH, HALF_WETH, NOVEL_OUT, THOUSAND_AT_SIX);
|
||||||
|
|
||||||
|
test("still renders a Token Out line, naming the address", () => {
|
||||||
|
const out = detail(data(), "Token Out");
|
||||||
|
expect(out).toBeDefined();
|
||||||
|
expect(out.value).toBe(NOVEL_OUT);
|
||||||
|
expect(out.address).toBe(NOVEL_OUT);
|
||||||
|
expect(out.isToken).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("names the address alongside the unknown-scale refusal", () => {
|
||||||
|
// The same population hits both: no symbol, and no scale either. The
|
||||||
|
// address says which token, the base units say how much and admit the
|
||||||
|
// scale is unknown — neither line silently means something else.
|
||||||
|
expect(detail(data(), "Token Out").value).toBe(NOVEL_OUT);
|
||||||
|
expect(detail(data(), "Min. received").value).toBe(
|
||||||
|
unknownDecimalsAmount(THOUSAND_AT_SIX),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("names the address when the scale is known but the symbol is not", () => {
|
||||||
|
const sources = {
|
||||||
|
trackedTokens: [
|
||||||
|
{ address: NOVEL_OUT, symbol: "NOVEL", decimals: 6 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
expect(detail(data(), "Token Out", sources).value).toBe(NOVEL_OUT);
|
||||||
|
expect(detail(data(), "Min. received", sources).value).toBe(
|
||||||
|
"1000.0000",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("the output tokens that already had a line keep it unchanged", () => {
|
||||||
|
test("a bundled token is named by symbol and address", () => {
|
||||||
|
const data = swapData(NOVEL_OUT, THOUSAND_AT_SIX, WETH, HALF_WETH);
|
||||||
|
const out = detail(data, "Token Out");
|
||||||
|
expect(out.value).toBe("WETH (" + WETH + ")");
|
||||||
|
expect(out.address).toBe(WETH);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an unwrap to native ETH is named ETH, with no address", () => {
|
||||||
|
const out = detail(unwrapData(), "Token Out");
|
||||||
|
expect(out.value).toBe("ETH");
|
||||||
|
expect(out.address).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user