Out of the box the wallet sent the user's addresses to Blockscout and each recipient's address to Etherscan. Two settings, "Use Blockscout" and "Check recipients on Etherscan", now gate every request that sends the user's data to those services. Both start off and are offered unticked on Welcome and in Settings. Each request reads its setting from storage before it asks. An answer that lands after the setting went off is dropped. While a setting is off, the screens that rely on it say so. ENS lookups ask only the RPC endpoint, so a name resolved off-chain does not resolve. RULES.md's External Communication item is now out of date. Its new wording will be proposed on #211 once this lands. Model: opus-5-5 (implementation, summary); fable-5-1 (prose)
128 lines
4.4 KiB
JavaScript
128 lines
4.4 KiB
JavaScript
// Tests for the token filtering in the Send view's token selector
|
|
// (src/popup/views/send.js).
|
|
//
|
|
// The selector decides which of the user's tokens can be spent at all, so
|
|
// over-filtering here is worse than in the history list: the asset is not
|
|
// merely hidden, it becomes unspendable through the UI. Issue #230: an
|
|
// explorer that omits holders_count was read as "zero holders" and the token
|
|
// disappeared from this list.
|
|
//
|
|
// renderSendTokenSelect only ever touches getElementById, createElement,
|
|
// innerHTML, value, textContent and appendChild, so a small stub document is
|
|
// enough to drive it; the real DOM behaviour of the view is covered by
|
|
// tests/e2e/run.js.
|
|
|
|
globalThis.chrome = {
|
|
storage: { local: { get: async () => ({}), set: async () => {} } },
|
|
};
|
|
|
|
const { state } = require("../src/shared/state");
|
|
const { renderSendTokenSelect } = require("../src/popup/views/send");
|
|
|
|
const USDC_CONTRACT = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
|
|
const NOVEL_TOKEN = "0x1111111111111111111111111111111111111111";
|
|
|
|
let select;
|
|
|
|
function installStubDocument() {
|
|
select = { innerHTML: "", children: [] };
|
|
select.appendChild = (child) => select.children.push(child);
|
|
const elements = {
|
|
"send-token": select,
|
|
"send-tokens-note": { classList: { toggle: () => {} } },
|
|
};
|
|
globalThis.document = {
|
|
getElementById: (id) => elements[id] || null,
|
|
createElement: () => ({ value: "", textContent: "" }),
|
|
};
|
|
}
|
|
|
|
// The symbols offered for sending, excluding the hardcoded ETH option that
|
|
// renderSendTokenSelect writes straight into innerHTML.
|
|
function offeredTokens() {
|
|
return select.children.map((opt) => opt.value.toLowerCase());
|
|
}
|
|
|
|
function tokenBalance(overrides) {
|
|
return {
|
|
address: NOVEL_TOKEN,
|
|
symbol: "SPAMTKN",
|
|
decimals: 18,
|
|
balance: "12.5",
|
|
holders: 50000,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function render(tokenBalances) {
|
|
installStubDocument();
|
|
renderSendTokenSelect({ address: "0x" + "a".repeat(40), tokenBalances });
|
|
}
|
|
|
|
beforeEach(() => {
|
|
state.fraudContracts = [];
|
|
state.hideLowHolderTokens = true;
|
|
});
|
|
|
|
describe("the low-holder rule in the send token selector", () => {
|
|
test("ETH is always offered", () => {
|
|
render([]);
|
|
expect(select.innerHTML).toBe('<option value="ETH">ETH</option>');
|
|
expect(offeredTokens()).toEqual([]);
|
|
});
|
|
|
|
test("a token with plenty of holders is offered", () => {
|
|
render([tokenBalance()]);
|
|
expect(offeredTokens()).toEqual([NOVEL_TOKEN]);
|
|
});
|
|
|
|
test("a token reporting zero holders is withheld", () => {
|
|
render([tokenBalance({ holders: 0 })]);
|
|
expect(offeredTokens()).toEqual([]);
|
|
});
|
|
|
|
test("boundary: 999 holders is withheld, 1000 is offered", () => {
|
|
render([tokenBalance({ holders: 999 })]);
|
|
expect(offeredTokens()).toEqual([]);
|
|
render([tokenBalance({ holders: 1000 })]);
|
|
expect(offeredTokens()).toEqual([NOVEL_TOKEN]);
|
|
});
|
|
|
|
// Issue #230: an unknown holder count must not read as zero. A token the
|
|
// user demonstrably holds — it has a balance — cannot be made unspendable
|
|
// by a field the block explorer failed to report.
|
|
test("a token whose holder count is unknown is still offered", () => {
|
|
render([tokenBalance({ holders: null })]);
|
|
expect(offeredTokens()).toEqual([NOVEL_TOKEN]);
|
|
});
|
|
|
|
test("a token balance carrying no holders field at all is offered", () => {
|
|
const t = tokenBalance();
|
|
delete t.holders;
|
|
render([t]);
|
|
expect(offeredTokens()).toEqual([NOVEL_TOKEN]);
|
|
});
|
|
|
|
test("the rule is bypassed entirely when the setting is off", () => {
|
|
state.hideLowHolderTokens = false;
|
|
render([tokenBalance({ holders: 0 })]);
|
|
expect(offeredTokens()).toEqual([NOVEL_TOKEN]);
|
|
});
|
|
});
|
|
|
|
describe("the other send-selector rules are unaffected", () => {
|
|
test("a token spoofing a known symbol from a wrong address is withheld", () => {
|
|
render([
|
|
tokenBalance({ symbol: "USDC", holders: null }),
|
|
tokenBalance({ address: USDC_CONTRACT, symbol: "USDC" }),
|
|
]);
|
|
expect(offeredTokens()).toEqual([USDC_CONTRACT.toLowerCase()]);
|
|
});
|
|
|
|
test("a blocklisted fraud contract is withheld even with an unknown count", () => {
|
|
state.fraudContracts = [NOVEL_TOKEN.toUpperCase()];
|
|
render([tokenBalance({ holders: null })]);
|
|
expect(offeredTokens()).toEqual([]);
|
|
});
|
|
});
|