Compare commits

...
Author SHA1 Message Date
sneak cc9fdfac99 fix: a token symbol read off a contract is never stored cut mid-emoji (closes #458)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
lookupTokenInfo() cut the symbol at 12 and the name at 64 UTF-16 units,
so an emoji outside the Basic Multilingual Plane could be cut between its
two halves and the half left over stored and shown as U+FFFD. Both are
now cut on code points, as displaySymbol() does. A symbol already stored
broken is not repaired.

Model: opus-5-5
2026-10-06 20:41:52 +00:00
3 changed files with 63 additions and 3 deletions
+9
View File
@@ -45,6 +45,15 @@ but the review is broader than any of them.
# Completed Steps
- 2026-10-06: A token symbol or name read off a contract is no longer stored cut
between the two halves of an emoji
([#458](https://git.eeqj.de/sneak/AutistMask/issues/458)). `lookupTokenInfo()`
cut both by UTF-16 units; it now counts code points, as `displaySymbol()` has
since [#329](https://git.eeqj.de/sneak/AutistMask/issues/329).
`tests/tokenLookupTruncation.test.js` looks up a token whose symbol and name
are made of emoji outside the Basic Multilingual Plane. A symbol already
stored broken is not repaired.
- 2026-10-06: Reloading or closing the popup no longer logs a request it cancels
as a failure in the transaction lists and ENS name lookups on the address and
token screens, the address scan after a wallet is created, the endpoint checks
+6 -3
View File
@@ -334,9 +334,12 @@ async function lookupTokenInfo(contractAddress, rpcUrl, networkId, signal) {
name = symbol;
}
// Truncate to prevent storage of excessively long values from RPC
name = String(name).slice(0, 64);
symbol = String(symbol).slice(0, 12);
// Truncate to prevent storage of excessively long values from RPC.
// Counted in code points, as displaySymbol() counts them, so the cut never
// falls between the two halves of an emoji and stores a half that renders
// as U+FFFD.
name = Array.from(String(name)).slice(0, 64).join("");
symbol = Array.from(String(symbol)).slice(0, 12).join("");
log.infof("Token resolved:", symbol, "decimals", Number(decimals));
return { name, symbol, decimals: Number(decimals) };
+48
View File
@@ -0,0 +1,48 @@
// lookupTokenInfo() cuts the symbol and name it reads off a contract on
// code-point boundaries, as displaySymbol() counts them, so an emoji outside
// the Basic Multilingual Plane is never stored cut in half
// (https://git.eeqj.de/sneak/AutistMask/issues/458).
// An emoji outside the Basic Multilingual Plane: two UTF-16 units.
const FOX = "\u{1F98A}";
// The leading "A" puts every emoji at an odd UTF-16 offset, so a cut that
// counts UTF-16 units lands between the two halves of one.
const SYMBOL = "A" + FOX.repeat(12);
const NAME = "A" + FOX.repeat(64);
// The contract answers symbol(), decimals() and name() with the values above.
jest.doMock("ethers", () => ({
...jest.requireActual("ethers"),
Contract: function () {
return {
symbol: async () => SYMBOL,
decimals: async () => 18n,
name: async () => NAME,
};
},
}));
const { lookupTokenInfo } = require("../src/shared/balances");
const ADDRESS = "0x1111111111111111111111111111111111111111";
const RPC_URL = "https://rpc.example.invalid";
beforeEach(() => {
// A token found is logged at info level, which is not under test.
jest.spyOn(console, "log").mockImplementation(() => {});
});
afterEach(() => {
jest.restoreAllMocks();
});
test("the symbol is cut to 12 code points, never inside an emoji", async () => {
const { symbol } = await lookupTokenInfo(ADDRESS, RPC_URL, "mainnet");
expect(symbol).toBe("A" + FOX.repeat(11));
});
test("the name is cut to 64 code points, never inside an emoji", async () => {
const { name } = await lookupTokenInfo(ADDRESS, RPC_URL, "mainnet");
expect(name).toBe("A" + FOX.repeat(63));
});