Files
AutistMask/tests/uniswapTokenOut.test.js
clawbot 2fe6447625
check / check (push) Failing after 0s
e2e / e2e-firefox (push) Failing after 0s
e2e / e2e-chrome (push) Failing after 1m27s
fix: name a tracked or explorer-known token instead of "Unknown token" (closes #323)
The approval and transaction-status screens read a token's scale from the
bundled list, the tokens the user tracks, then the block explorer, but read
its symbol from the bundled list alone. A token the user added by hand was
scaled correctly yet labelled "Unknown token", and a non-bundled ERC-20 was
carried onto the wait screen as ETH.

resolveTokenSymbol() now draws the symbol through the same sources and
precedence as the scale, and the ERC-20 and Uniswap swap lines both use it. A
tracked or explorer-reported name stays subject to the spoof rule, so it
cannot claim a bundled or native ticker.

Folds in #354.

Model: opus-4-8
Co-authored-by: clawbot <clawbot@noreply.example.org>
2026-09-21 21:28:06 +02:00

113 lines
4.5 KiB
JavaScript

// 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 tracked symbol alongside the address (issue #323)", () => {
// The tracked entry supplies both halves now: the scale, and the
// symbol the output line is named by. Before #323 the symbol was read
// from the bundled list alone, so this line fell back to the address.
const sources = {
trackedTokens: [
{ address: NOVEL_OUT, symbol: "NOVEL", decimals: 6 },
],
};
expect(detail(data(), "Token Out", sources).value).toBe(
"NOVEL (" + NOVEL_OUT + ")",
);
expect(detail(data(), "Min. received", sources).value).toBe(
"1000.0000 NOVEL",
);
});
});
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();
});
});