// 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(); }); });