chore: the native token's label follows the network (closes #372)
networks.js gives each network a nativeCurrency (ETH, SepoliaETH) and nothing read it: every screen wrote ETH. The wallet's balances and the Send and confirmation screens now use the active network's. A transaction's figures use the network its chain id names, through nativeCurrencyByChainId(): the approval value and fee, the wait, success and error screens, history entries, the detail screen and the fee-limit refusal, so a site switching networks cannot make one read as another network's coin. The "ETH" that selectedToken and txInfo.token hold is the native token's id and is unchanged. A token reporting any network's nativeCurrency is a spoof, and the detail screen calls an entry a token transfer when it has a token contract. Model: opus-5-5
This commit was merged in pull request #442.
This commit is contained in:
@@ -599,6 +599,24 @@ describe("verifySignedTx field comparison", () => {
|
||||
expect(e.message).toContain("1.0 ETH");
|
||||
}
|
||||
});
|
||||
|
||||
// The fee is in the native currency of the network the transaction is
|
||||
// for, whether its chain id is the hex string the background prepares
|
||||
// or the number ethers parses from a signed transaction.
|
||||
test.each([
|
||||
["0x1", "ETH"],
|
||||
[1n, "ETH"],
|
||||
["0xaa36a7", "SepoliaETH"],
|
||||
[11155111n, "SepoliaETH"],
|
||||
])("the refusal on chain %p names %s", (chainId, nativeCurrency) => {
|
||||
expect(() => assertWithinCeilings({ ...OVER, chainId })).toThrow(
|
||||
"up to 3000.0 " +
|
||||
nativeCurrency +
|
||||
", which is more than the 1.0 " +
|
||||
nativeCurrency +
|
||||
" this wallet",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("every field mismatch is a refusal, not a warning", async () => {
|
||||
|
||||
@@ -0,0 +1,461 @@
|
||||
// The native token's label on the screens that show a native amount.
|
||||
//
|
||||
// src/shared/networks.js gives each network a nativeCurrency, `ETH` on mainnet
|
||||
// and `SepoliaETH` on Sepolia, and nothing read it: every screen wrote a
|
||||
// hardcoded "ETH", so on Sepolia the balance, the value and the fee all read
|
||||
// ETH (https://git.eeqj.de/sneak/AutistMask/issues/372). Each line is asserted
|
||||
// on both networks, through the real Send, confirmation and approval screens,
|
||||
// with only the node and the DOM stubbed. So is that a token cannot pass for
|
||||
// the native token by reporting its label, and that a transaction's figures
|
||||
// carry its own network's label when another network is active.
|
||||
|
||||
"use strict";
|
||||
|
||||
jest.mock("ethers", () => {
|
||||
const actual = jest.requireActual("ethers");
|
||||
class StubProvider {
|
||||
async lookupAddress() {
|
||||
return null;
|
||||
}
|
||||
// 10 gwei expected, 20 gwei reserved per gas.
|
||||
async getFeeData() {
|
||||
return { maxFeePerGas: 20000000000n, gasPrice: 10000000000n };
|
||||
}
|
||||
async estimateGas() {
|
||||
return 21000n;
|
||||
}
|
||||
async getCode() {
|
||||
return "0x";
|
||||
}
|
||||
async getTransactionCount() {
|
||||
return 1;
|
||||
}
|
||||
async getTransactionReceipt() {
|
||||
return { blockNumber: 21000000 };
|
||||
}
|
||||
}
|
||||
return {
|
||||
...actual,
|
||||
JsonRpcProvider: StubProvider,
|
||||
Network: { from: () => ({}) },
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock("../src/shared/log", () => ({
|
||||
log: {
|
||||
debugf: () => {},
|
||||
infof: () => {},
|
||||
warnf: () => {},
|
||||
errorf: () => {},
|
||||
},
|
||||
debugFetch: jest.fn(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ items: [] }),
|
||||
})),
|
||||
urlOrigin: () => "",
|
||||
setRuntimeDebug: () => {},
|
||||
isDebug: () => false,
|
||||
}));
|
||||
|
||||
// Signing a send succeeds without a key, and sending answers with a hash.
|
||||
jest.mock("../src/shared/vault", () => ({
|
||||
...jest.requireActual("../src/shared/vault"),
|
||||
decryptWithPassword: async () => "secret",
|
||||
}));
|
||||
jest.mock("../src/shared/wallet", () => ({
|
||||
...jest.requireActual("../src/shared/wallet"),
|
||||
getSignerForAddress: () => ({
|
||||
connect: () => ({
|
||||
populateTransaction: async (request) => request,
|
||||
sendTransaction: async () => ({ hash: "0x" + "3".repeat(64) }),
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
|
||||
global.fetch = jest.fn(() => {
|
||||
throw new Error("tests must not perform network requests");
|
||||
});
|
||||
|
||||
// The approval the background hands the approval screen. Set per test.
|
||||
let approvalDetails = null;
|
||||
|
||||
const { makeStorageStub } = require("./support/storageStub");
|
||||
global.chrome = {
|
||||
storage: makeStorageStub(),
|
||||
runtime: {
|
||||
connect: () => ({
|
||||
postMessage() {},
|
||||
disconnect() {},
|
||||
onDisconnect: { addListener() {} },
|
||||
}),
|
||||
sendMessage(message, callback) {
|
||||
callback(
|
||||
message.type === "AUTISTMASK_GET_APPROVAL"
|
||||
? approvalDetails
|
||||
: undefined,
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// A stub DOM: every id resolves to a recording element.
|
||||
const elements = new Map();
|
||||
|
||||
function makeEl(id) {
|
||||
const handlers = new Map();
|
||||
return {
|
||||
id,
|
||||
textContent: "",
|
||||
innerHTML: "",
|
||||
value: "",
|
||||
disabled: false,
|
||||
style: {},
|
||||
dataset: {},
|
||||
classList: {
|
||||
add() {},
|
||||
remove() {},
|
||||
toggle() {},
|
||||
contains: () => false,
|
||||
},
|
||||
handlers,
|
||||
children: [],
|
||||
addEventListener(name, fn) {
|
||||
handlers.set(name, fn);
|
||||
},
|
||||
appendChild(child) {
|
||||
this.children.push(child);
|
||||
return child;
|
||||
},
|
||||
querySelectorAll: () => [],
|
||||
querySelector: () => null,
|
||||
remove() {},
|
||||
focus() {},
|
||||
// Views reach for .parentElement to hide whole sections.
|
||||
get parentElement() {
|
||||
return global.document.getElementById(id + "-parent");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
global.document = {
|
||||
getElementById(id) {
|
||||
if (!elements.has(id)) elements.set(id, makeEl(id));
|
||||
return elements.get(id);
|
||||
},
|
||||
createElement: (tag) => makeEl(tag),
|
||||
body: { prepend() {}, appendChild() {} },
|
||||
addEventListener() {},
|
||||
};
|
||||
global.navigator = { clipboard: { writeText() {} } };
|
||||
|
||||
const { state } = require("../src/shared/state");
|
||||
const { NETWORKS } = require("../src/shared/networks");
|
||||
const { clearPrices } = require("../src/shared/prices");
|
||||
const send = require("../src/popup/views/send");
|
||||
const confirmTx = require("../src/popup/views/confirmTx");
|
||||
const approval = require("../src/popup/views/approval");
|
||||
const transactionDetail = require("../src/popup/views/transactionDetail");
|
||||
const txStatus = require("../src/popup/views/txStatus");
|
||||
const { balanceLinesForAddress } = require("../src/popup/views/helpers");
|
||||
const { filterTransactions } = require("../src/shared/transactions");
|
||||
const { debugFetch } = require("../src/shared/log");
|
||||
|
||||
const HOLDER = "0x" + "a".repeat(40);
|
||||
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
|
||||
// A token contract that is not in the bundled token list.
|
||||
const TOKEN_CONTRACT = "0xd05339f9ea5ab9d9f03b9d57f671d2abd1f55c82";
|
||||
|
||||
function text(id) {
|
||||
return global.document.getElementById(id).textContent;
|
||||
}
|
||||
|
||||
// Press Review on the Send screen for a native send of `amount`, and show the
|
||||
// confirmation screen it leads to with its fee estimate settled.
|
||||
async function confirmSend(amount) {
|
||||
let txInfo = null;
|
||||
send.init({ showConfirmTx: (info) => (txInfo = info) });
|
||||
state.selectedToken = "ETH";
|
||||
global.document.getElementById("send-to").value = RECIPIENT;
|
||||
global.document.getElementById("send-amount").value = amount;
|
||||
await global.document
|
||||
.getElementById("btn-send-review")
|
||||
.handlers.get("click")();
|
||||
confirmTx.show(txInfo);
|
||||
for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0));
|
||||
}
|
||||
|
||||
// The approval screen for a dApp transaction sending 0.01 of the native token
|
||||
// with 21000 gas at up to 20 gwei, on the network with `chainId`.
|
||||
async function approveTx(chainId) {
|
||||
approvalDetails = {
|
||||
type: "tx",
|
||||
origin: "https://dapp.example",
|
||||
approvedFrom: HOLDER,
|
||||
approvedTx: {
|
||||
to: RECIPIENT,
|
||||
value: "10000000000000000",
|
||||
data: "0x",
|
||||
chainId,
|
||||
gasLimit: "21000",
|
||||
maxFeePerGas: "20000000000",
|
||||
nonce: 0,
|
||||
},
|
||||
};
|
||||
await approval.show("1");
|
||||
}
|
||||
|
||||
describe.each([
|
||||
["mainnet", "ETH"],
|
||||
["sepolia", "SepoliaETH"],
|
||||
])("on %s the native token reads %s", (networkId, symbol) => {
|
||||
beforeEach(() => {
|
||||
elements.clear();
|
||||
clearPrices();
|
||||
state.networkId = networkId;
|
||||
state.wallets = [
|
||||
{
|
||||
name: "Wallet 1",
|
||||
addresses: [{ address: HOLDER, balance: "1.5" }],
|
||||
},
|
||||
];
|
||||
state.selectedWallet = 0;
|
||||
state.selectedAddress = 0;
|
||||
state.trackedTokens = [];
|
||||
state.fraudContracts = [];
|
||||
state.currentView = null;
|
||||
});
|
||||
|
||||
test("the balance", async () => {
|
||||
const addr = state.wallets[0].addresses[0];
|
||||
expect(balanceLinesForAddress(addr, [], false)).toContain(
|
||||
`<span>${symbol}</span><span>1.5000</span>`,
|
||||
);
|
||||
state.selectedToken = "ETH";
|
||||
send.updateSendBalance();
|
||||
expect(text("send-balance")).toBe("Current balance: 1.5000 " + symbol);
|
||||
await confirmSend("0.1");
|
||||
expect(text("confirm-balance")).toBe("1.5000 " + symbol);
|
||||
});
|
||||
|
||||
test("the value", async () => {
|
||||
await confirmSend("0.1");
|
||||
expect(text("confirm-type")).toBe("Native " + symbol + " transfer");
|
||||
expect(text("confirm-amount")).toBe("0.1 " + symbol);
|
||||
await approveTx(NETWORKS[networkId].chainId);
|
||||
expect(text("approve-tx-value")).toBe("0.0100 " + symbol);
|
||||
});
|
||||
|
||||
test("the fee", async () => {
|
||||
await confirmSend("0.1");
|
||||
// 21000 gas at 10 gwei expected, at 20 gwei reserved.
|
||||
expect(text("confirm-fee-amount")).toBe("~0.0002 " + symbol);
|
||||
expect(text("confirm-fee-reserve")).toBe(
|
||||
"up to 0.0004 " + symbol + " reserved",
|
||||
);
|
||||
expect(text("confirm-gas-error")).toContain(
|
||||
"You do not have enough " + symbol + " to pay the network fee",
|
||||
);
|
||||
await approveTx(NETWORKS[networkId].chainId);
|
||||
expect(text("approve-tx-fee")).toBe("0.0004 " + symbol);
|
||||
});
|
||||
|
||||
test("the contract-recipient warning", async () => {
|
||||
await confirmSend("0.1");
|
||||
expect(text("confirm-contract-warning")).toContain(
|
||||
"Sending " + symbol + " or tokens directly to a contract",
|
||||
);
|
||||
});
|
||||
|
||||
// A token reports whatever symbol it likes. One reporting the label the
|
||||
// wallet shows its native token under, on this network or any other, is
|
||||
// a fake, exactly as one reporting `ETH` always was.
|
||||
test.each(["ETH", symbol])(
|
||||
"a token claiming %s is dropped from the history and the Send selector",
|
||||
(claim) => {
|
||||
const result = filterTransactions(
|
||||
[
|
||||
{
|
||||
hash: "0x" + "1".repeat(64),
|
||||
symbol: claim,
|
||||
contractAddress: TOKEN_CONTRACT,
|
||||
holders: 900000,
|
||||
valueGwei: null,
|
||||
isContractCall: false,
|
||||
},
|
||||
],
|
||||
{ hideSpoofedSymbols: true },
|
||||
);
|
||||
expect(result.transactions).toEqual([]);
|
||||
expect(result.newFraudContracts).toEqual([TOKEN_CONTRACT]);
|
||||
|
||||
send.renderSendTokenSelect({
|
||||
address: HOLDER,
|
||||
tokenBalances: [
|
||||
{
|
||||
address: TOKEN_CONTRACT,
|
||||
symbol: claim,
|
||||
decimals: 18,
|
||||
balance: "5",
|
||||
holders: 900000,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(
|
||||
global.document.getElementById("send-token").children,
|
||||
).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
// The detail screen tells the two apart by the token contract, which only
|
||||
// a token transfer has, so a token reporting the native label still reads
|
||||
// as a token transfer.
|
||||
test("the transaction detail screen's type line", () => {
|
||||
const entry = {
|
||||
hash: "0x" + "2".repeat(64),
|
||||
from: RECIPIENT,
|
||||
to: HOLDER,
|
||||
value: "1.0000",
|
||||
exactValue: "1.0",
|
||||
symbol,
|
||||
timestamp: 1790000000,
|
||||
isError: false,
|
||||
direction: "received",
|
||||
directionLabel: "Received",
|
||||
chainId: NETWORKS[networkId].chainId,
|
||||
};
|
||||
transactionDetail.show({ ...entry, contractAddress: null });
|
||||
expect(text("tx-detail-type")).toBe("Native " + symbol + " Transfer");
|
||||
transactionDetail.show({ ...entry, contractAddress: TOKEN_CONTRACT });
|
||||
expect(text("tx-detail-type")).toBe("ERC-20 Token Transfer");
|
||||
});
|
||||
|
||||
test("the insufficient-balance error", async () => {
|
||||
await confirmSend("2");
|
||||
expect(
|
||||
global.document.getElementById("confirm-errors").innerHTML,
|
||||
).toContain(
|
||||
"You have 1.5000 " +
|
||||
symbol +
|
||||
" but are trying to send 2 " +
|
||||
symbol +
|
||||
".",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// A transaction's value and fee are in the native currency of the network the
|
||||
// transaction is on, which need not be the active one. A site can switch the
|
||||
// active network after its transaction is prepared and back before it is
|
||||
// signed, and a popup opened after a switch shows a sent or listed transaction
|
||||
// again. The wallet's balances follow the active network; these do not.
|
||||
describe.each([
|
||||
["mainnet", "sepolia", "ETH"],
|
||||
["sepolia", "mainnet", "SepoliaETH"],
|
||||
])(
|
||||
"a %s transaction shown with %s active reads %s",
|
||||
(txNetworkId, activeNetworkId, symbol) => {
|
||||
const chainId = NETWORKS[txNetworkId].chainId;
|
||||
const hash = "0x" + "3".repeat(64);
|
||||
|
||||
beforeEach(() => {
|
||||
elements.clear();
|
||||
clearPrices();
|
||||
state.networkId = activeNetworkId;
|
||||
state.wallets = [
|
||||
{
|
||||
name: "Wallet 1",
|
||||
addresses: [{ address: HOLDER, balance: "1.5" }],
|
||||
},
|
||||
];
|
||||
state.selectedWallet = 0;
|
||||
state.selectedAddress = 0;
|
||||
state.trackedTokens = [];
|
||||
state.fraudContracts = [];
|
||||
state.currentView = null;
|
||||
txStatus.init({ doRefreshAndRender() {} });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
txStatus.endWait();
|
||||
});
|
||||
|
||||
test("the approval screen's value and fee", async () => {
|
||||
await approveTx(chainId);
|
||||
expect(text("approve-tx-network")).toBe(NETWORKS[txNetworkId].name);
|
||||
expect(text("approve-tx-value")).toBe("0.0100 " + symbol);
|
||||
expect(text("approve-tx-fee")).toBe("0.0004 " + symbol);
|
||||
});
|
||||
|
||||
test("the wait, success and error screens", async () => {
|
||||
const txInfo = {
|
||||
from: HOLDER,
|
||||
to: RECIPIENT,
|
||||
amount: "0.0100",
|
||||
token: "ETH",
|
||||
tokenSymbol: null,
|
||||
chainId,
|
||||
};
|
||||
txStatus.showWait(txInfo, hash);
|
||||
expect(text("wait-tx-summary")).toBe("0.0100 " + symbol);
|
||||
txStatus.showError(txInfo, hash, "Failed.");
|
||||
expect(text("error-tx-summary")).toBe("0.0100 " + symbol);
|
||||
// A later popup resumes the wait, and the receipt is there.
|
||||
state.viewData = {
|
||||
pendingWait: { txInfo, hash, broadcastTime: Date.now() },
|
||||
};
|
||||
txStatus.restoreWait();
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
}
|
||||
expect(text("success-tx-summary")).toBe("0.0100 " + symbol);
|
||||
});
|
||||
|
||||
// Sent from the Send screen on the transaction's network, then
|
||||
// resumed by a popup that opens after the active network changed.
|
||||
test("the wait screen after a send", async () => {
|
||||
state.networkId = txNetworkId;
|
||||
await confirmSend("0.1");
|
||||
confirmTx.init({});
|
||||
global.document.getElementById("confirm-tx-password").value = "pw";
|
||||
await global.document
|
||||
.getElementById("btn-confirm-send")
|
||||
.handlers.get("click")();
|
||||
expect(text("wait-tx-summary")).toBe("0.1 " + symbol);
|
||||
state.networkId = activeNetworkId;
|
||||
txStatus.restoreWait();
|
||||
expect(text("wait-tx-summary")).toBe("0.1 " + symbol);
|
||||
});
|
||||
|
||||
test("the transaction detail screen's type line and fee", async () => {
|
||||
debugFetch.mockImplementationOnce(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ fee: { value: "21000000000000" } }),
|
||||
}));
|
||||
transactionDetail.show({
|
||||
hash,
|
||||
from: RECIPIENT,
|
||||
to: HOLDER,
|
||||
value: "1.0000",
|
||||
exactValue: "1.0",
|
||||
symbol,
|
||||
timestamp: 1790000000,
|
||||
isError: false,
|
||||
direction: "received",
|
||||
directionLabel: "Received",
|
||||
contractAddress: null,
|
||||
chainId,
|
||||
});
|
||||
expect(text("tx-detail-type")).toBe(
|
||||
"Native " + symbol + " Transfer",
|
||||
);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
}
|
||||
expect(
|
||||
global.document.getElementById("tx-detail-fee").innerHTML,
|
||||
).toContain("0.000021 " + symbol);
|
||||
});
|
||||
},
|
||||
);
|
||||
+66
-13
@@ -1219,7 +1219,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
|
||||
test("queries only the two Blockscout endpoints for the address", async () => {
|
||||
respondWith([], []);
|
||||
await fetchRecentTransactions(VICTIM, BLOCKSCOUT);
|
||||
await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||
expect(debugFetch).toHaveBeenCalledTimes(2);
|
||||
const urls = debugFetch.mock.calls.map((c) => c[0]);
|
||||
expect(urls).toContain(
|
||||
@@ -1283,7 +1283,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
],
|
||||
);
|
||||
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT);
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||
expect(txs).toHaveLength(1);
|
||||
const merged = txs[0];
|
||||
// The received leg (the swap output) supplies the display amount.
|
||||
@@ -1320,7 +1320,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
],
|
||||
);
|
||||
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT);
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||
expect(txs).toHaveLength(1);
|
||||
expect(txs[0].symbol).toBe("USDC");
|
||||
expect(txs[0].value).toBe("1500.5000");
|
||||
@@ -1346,7 +1346,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
});
|
||||
respondWith([], [leg("1000000"), leg("2000000")]);
|
||||
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT);
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||
expect(txs).toHaveLength(1);
|
||||
// Keyed by hash plus contract, so the later leg wins.
|
||||
expect(txs[0].exactValue).toBe("2.0");
|
||||
@@ -1386,7 +1386,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
],
|
||||
);
|
||||
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT);
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||
expect(txs.map((t) => t.symbol).sort()).toEqual(["USDC", "WETH"]);
|
||||
});
|
||||
|
||||
@@ -1427,12 +1427,13 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
],
|
||||
);
|
||||
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT);
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||
expect(txs).toHaveLength(1);
|
||||
expect(txs[0].symbol).toBe("USDC");
|
||||
expect(txs[0].exactValue).toBe("1.0");
|
||||
expect(txs[0].direction).toBe("sent");
|
||||
expect(txs[0].contractAddress).toBe(USDC_CONTRACT);
|
||||
expect(txs[0].chainId).toBe("0x1");
|
||||
// The surviving row is the token row, and the filters keep it.
|
||||
const kept = filterTransactions(txs, filters()).transactions;
|
||||
expect(kept).toHaveLength(1);
|
||||
@@ -1452,10 +1453,46 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
});
|
||||
respondWith([item(6), item(8), item(7)], []);
|
||||
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, 2);
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1", 2);
|
||||
expect(txs.map((t) => t.blockNumber)).toEqual([21000008, 21000007]);
|
||||
});
|
||||
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/372: the caller hands in
|
||||
// the chain id of the network the explorer serves. Every entry carries
|
||||
// it, and a native entry is labelled with that network's nativeCurrency.
|
||||
test("a native entry is labelled by the chain id handed in", async () => {
|
||||
respondWith(
|
||||
[
|
||||
{
|
||||
hash: "0x" + "a".repeat(64),
|
||||
block_number: 21000090,
|
||||
timestamp: TS,
|
||||
from: { hash: ORDINARY_PEER },
|
||||
to: { hash: VICTIM, is_contract: false },
|
||||
value: "10000000000000000",
|
||||
method: null,
|
||||
status: "ok",
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
const sepolia = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
"0xaa36a7",
|
||||
);
|
||||
expect(sepolia[0].symbol).toBe("SepoliaETH");
|
||||
expect(sepolia[0].value).toBe("0.0100");
|
||||
expect(sepolia[0].chainId).toBe("0xaa36a7");
|
||||
const mainnet = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
"0x1",
|
||||
);
|
||||
expect(mainnet[0].symbol).toBe("ETH");
|
||||
expect(mainnet[0].chainId).toBe("0x1");
|
||||
});
|
||||
|
||||
test("the fake token transfer survives fetching and is then filtered", async () => {
|
||||
respondWith(
|
||||
[],
|
||||
@@ -1476,7 +1513,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
],
|
||||
);
|
||||
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT);
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||
expect(txs).toHaveLength(1);
|
||||
expect(txs[0].contractAddress).toBe(FAKE_ETH_CONTRACT);
|
||||
expect(txs[0].holders).toBe(0);
|
||||
@@ -1515,19 +1552,31 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
|
||||
test("an omitted holders_count parses to null", async () => {
|
||||
respondWith([], spamTransferWithToken(OMITTED));
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT);
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
"0x1",
|
||||
);
|
||||
expect(txs[0].holders).toBeNull();
|
||||
});
|
||||
|
||||
test("a null holders_count parses to null", async () => {
|
||||
respondWith([], spamTransferWithToken(NULLED));
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT);
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
"0x1",
|
||||
);
|
||||
expect(txs[0].holders).toBeNull();
|
||||
});
|
||||
|
||||
test("the transfer survives the low-holder filter", async () => {
|
||||
respondWith([], spamTransferWithToken(OMITTED));
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT);
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
"0x1",
|
||||
);
|
||||
expect(filterTransactions(txs, filters()).transactions).toEqual(
|
||||
txs,
|
||||
);
|
||||
@@ -1539,7 +1588,11 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
// holder count is the only rule that can catch it.
|
||||
test('a reported holders_count of "0" still parses to 0 and is filtered', async () => {
|
||||
respondWith([], spamTransferWithToken(ZERO));
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT);
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
"0x1",
|
||||
);
|
||||
expect(txs[0].holders).toBe(0);
|
||||
expect(filterTransactions(txs, filters()).transactions).toEqual([]);
|
||||
});
|
||||
@@ -1555,7 +1608,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
},
|
||||
}));
|
||||
await expect(
|
||||
fetchRecentTransactions(VICTIM, BLOCKSCOUT),
|
||||
fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1"),
|
||||
).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user