fix: store an absent explorer decimals as unknown instead of fabricating 18 (closes #349)
parseInt(decimals || "18") ran before writing stored tokenBalances[].decimals, so an explorer reporting no decimals produced a fabricated 18 indistinguishable from a real one at read time. That defeated the resolve-or-refuse guarantees of #306 and #340: their refusal paths were intact but never fired, because the guess was laundered upstream of them. An absent scale is now stored as unknown, and a holding whose scale nothing knows carries a null balance -- unknown, never zero -- with six reader sites saying so rather than printing 0.0000. The Send screen resolves the display scale rather than reading the stored one, so a bundled token whose explorer row omits decimals still sends; when the scale cannot be resolved the stored quantity is withdrawn too, so the user is told the balance is unknown rather than only that the fee failed. Existing fabricated 18s cannot be told apart retroactively and are replaced wholesale on the next balance refresh. An explorer-sourced scale stays trusted -- only fabrication is removed; the reasoning is recorded on the issue.
This commit was merged in pull request #367.
This commit is contained in:
@@ -23,33 +23,14 @@
|
||||
// disputed is refused rather than guessed at.
|
||||
|
||||
// Solidity's decimals() is a uint8, and every source here is ultimately
|
||||
// reporting that call's result.
|
||||
const { MAX_DECIMALS } = require("./transferAmount");
|
||||
// reporting that call's result. toDecimals() is that check, shared with the
|
||||
// send path rather than copied: the bundled list stores numbers, the
|
||||
// explorer's copy arrives as a string, and a token the user added by hand
|
||||
// carries whatever lookupTokenInfo() got back, so the accepted types are
|
||||
// enumerated rather than coerced.
|
||||
const { toDecimals } = require("./transferAmount");
|
||||
const { TOKEN_BY_ADDRESS } = require("./tokenList");
|
||||
|
||||
// A decimals value as a number, or null if it is not one. The bundled list
|
||||
// stores numbers, the explorer's copy arrives as a string, and a token the
|
||||
// user added by hand can carry whatever lookupTokenInfo() got back, so the
|
||||
// accepted types are enumerated rather than coerced: Number([]) is 0 and
|
||||
// Number(true) is 1, so a coercing check would read an empty array as a scale
|
||||
// of zero and format the amount as whole tokens.
|
||||
function toDecimals(value) {
|
||||
let n;
|
||||
if (typeof value === "number") {
|
||||
n = value;
|
||||
} else if (typeof value === "bigint") {
|
||||
if (value < 0n || value > BigInt(MAX_DECIMALS)) return null;
|
||||
n = Number(value);
|
||||
} else if (typeof value === "string") {
|
||||
if (!/^[0-9]+$/.test(value)) return null;
|
||||
n = Number(value);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
if (!Number.isInteger(n) || n < 0 || n > MAX_DECIMALS) return null;
|
||||
return n;
|
||||
}
|
||||
|
||||
// Every decimals the explorer reported for this contract, across all the
|
||||
// addresses whose balances have been fetched. They describe one contract, so
|
||||
// they should agree; a set that does not agree is a scale in dispute, and this
|
||||
|
||||
Reference in New Issue
Block a user